snow-ai 0.2.9 → 0.2.10

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.
@@ -11,6 +11,35 @@ interface SearchResult {
11
11
  matches: SearchMatch[];
12
12
  searchedFiles: number;
13
13
  }
14
+ interface StructureAnalysis {
15
+ bracketBalance: {
16
+ curly: {
17
+ open: number;
18
+ close: number;
19
+ balanced: boolean;
20
+ };
21
+ round: {
22
+ open: number;
23
+ close: number;
24
+ balanced: boolean;
25
+ };
26
+ square: {
27
+ open: number;
28
+ close: number;
29
+ balanced: boolean;
30
+ };
31
+ };
32
+ htmlTags?: {
33
+ unclosedTags: string[];
34
+ unopenedTags: string[];
35
+ balanced: boolean;
36
+ };
37
+ indentationWarnings: string[];
38
+ codeBlockBoundary?: {
39
+ isInCompleteBlock: boolean;
40
+ suggestion?: string;
41
+ };
42
+ }
14
43
  /**
15
44
  * Filesystem MCP Service
16
45
  * Provides basic file operations: read, create, and delete files
@@ -18,6 +47,16 @@ interface SearchResult {
18
47
  export declare class FilesystemMCPService {
19
48
  private basePath;
20
49
  constructor(basePath?: string);
50
+ /**
51
+ * Analyze code structure for balance and completeness
52
+ * Helps AI identify bracket mismatches, unclosed tags, and boundary issues
53
+ */
54
+ private analyzeCodeStructure;
55
+ /**
56
+ * Find smart context boundaries for editing
57
+ * Expands context to include complete code blocks when possible
58
+ */
59
+ private findSmartContextBoundaries;
21
60
  /**
22
61
  * Get the content of a file with specified line range
23
62
  * @param filePath - Path to the file (relative to base path or absolute)
@@ -96,6 +135,7 @@ export declare class FilesystemMCPService {
96
135
  contextEndLine: number;
97
136
  totalLines: number;
98
137
  linesModified: number;
138
+ structureAnalysis?: StructureAnalysis;
99
139
  diagnostics?: Diagnostic[];
100
140
  }>;
101
141
  /**
@@ -17,6 +17,178 @@ export class FilesystemMCPService {
17
17
  });
18
18
  this.basePath = resolve(basePath);
19
19
  }
20
+ /**
21
+ * Analyze code structure for balance and completeness
22
+ * Helps AI identify bracket mismatches, unclosed tags, and boundary issues
23
+ */
24
+ analyzeCodeStructure(_content, filePath, editedLines) {
25
+ const analysis = {
26
+ bracketBalance: {
27
+ curly: { open: 0, close: 0, balanced: true },
28
+ round: { open: 0, close: 0, balanced: true },
29
+ square: { open: 0, close: 0, balanced: true }
30
+ },
31
+ indentationWarnings: []
32
+ };
33
+ // Count brackets in the edited content
34
+ const editedContent = editedLines.join('\n');
35
+ // Remove string literals and comments to avoid false positives
36
+ const cleanContent = editedContent
37
+ .replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, '""') // Remove strings
38
+ .replace(/\/\/.*$/gm, '') // Remove single-line comments
39
+ .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments
40
+ // Count brackets
41
+ analysis.bracketBalance.curly.open = (cleanContent.match(/\{/g) || []).length;
42
+ analysis.bracketBalance.curly.close = (cleanContent.match(/\}/g) || []).length;
43
+ analysis.bracketBalance.curly.balanced =
44
+ analysis.bracketBalance.curly.open === analysis.bracketBalance.curly.close;
45
+ analysis.bracketBalance.round.open = (cleanContent.match(/\(/g) || []).length;
46
+ analysis.bracketBalance.round.close = (cleanContent.match(/\)/g) || []).length;
47
+ analysis.bracketBalance.round.balanced =
48
+ analysis.bracketBalance.round.open === analysis.bracketBalance.round.close;
49
+ analysis.bracketBalance.square.open = (cleanContent.match(/\[/g) || []).length;
50
+ analysis.bracketBalance.square.close = (cleanContent.match(/\]/g) || []).length;
51
+ analysis.bracketBalance.square.balanced =
52
+ analysis.bracketBalance.square.open === analysis.bracketBalance.square.close;
53
+ // HTML/JSX tag analysis (for .html, .jsx, .tsx, .vue files)
54
+ const isMarkupFile = /\.(html|jsx|tsx|vue)$/i.test(filePath);
55
+ if (isMarkupFile) {
56
+ const tagPattern = /<\/?([a-zA-Z][a-zA-Z0-9-]*)[^>]*>/g;
57
+ const selfClosingPattern = /<[a-zA-Z][a-zA-Z0-9-]*[^>]*\/>/g;
58
+ // Remove self-closing tags
59
+ const contentWithoutSelfClosing = cleanContent.replace(selfClosingPattern, '');
60
+ const tags = [];
61
+ const unclosedTags = [];
62
+ const unopenedTags = [];
63
+ let match;
64
+ while ((match = tagPattern.exec(contentWithoutSelfClosing)) !== null) {
65
+ const isClosing = match[0]?.startsWith('</');
66
+ const tagName = match[1]?.toLowerCase();
67
+ if (!tagName)
68
+ continue;
69
+ if (isClosing) {
70
+ const lastOpenTag = tags.pop();
71
+ if (!lastOpenTag || lastOpenTag !== tagName) {
72
+ unopenedTags.push(tagName);
73
+ if (lastOpenTag)
74
+ tags.push(lastOpenTag); // Put it back
75
+ }
76
+ }
77
+ else {
78
+ tags.push(tagName);
79
+ }
80
+ }
81
+ unclosedTags.push(...tags);
82
+ analysis.htmlTags = {
83
+ unclosedTags,
84
+ unopenedTags,
85
+ balanced: unclosedTags.length === 0 && unopenedTags.length === 0
86
+ };
87
+ }
88
+ // Check indentation consistency
89
+ const lines = editedContent.split('\n');
90
+ const indents = lines
91
+ .filter(line => line.trim().length > 0)
92
+ .map(line => {
93
+ const match = line.match(/^(\s*)/);
94
+ return match ? match[1] : '';
95
+ })
96
+ .filter((indent) => indent !== undefined);
97
+ // Detect mixed tabs/spaces
98
+ const hasTabs = indents.some(indent => indent.includes('\t'));
99
+ const hasSpaces = indents.some(indent => indent.includes(' '));
100
+ if (hasTabs && hasSpaces) {
101
+ analysis.indentationWarnings.push('Mixed tabs and spaces detected');
102
+ }
103
+ // Detect inconsistent indentation levels (spaces only)
104
+ if (!hasTabs && hasSpaces) {
105
+ const spaceCounts = indents
106
+ .filter(indent => indent.length > 0)
107
+ .map(indent => indent.length);
108
+ if (spaceCounts.length > 1) {
109
+ const gcd = spaceCounts.reduce((a, b) => {
110
+ while (b !== 0) {
111
+ const temp = b;
112
+ b = a % b;
113
+ a = temp;
114
+ }
115
+ return a;
116
+ });
117
+ const hasInconsistent = spaceCounts.some(count => count % gcd !== 0 && gcd > 1);
118
+ if (hasInconsistent) {
119
+ analysis.indentationWarnings.push(`Inconsistent indentation (expected multiples of ${gcd} spaces)`);
120
+ }
121
+ }
122
+ }
123
+ // Check if edit is at a code block boundary
124
+ const lastLine = editedLines[editedLines.length - 1]?.trim() || '';
125
+ const firstLine = editedLines[0]?.trim() || '';
126
+ const endsWithOpenBrace = lastLine.endsWith('{') || lastLine.endsWith('(') || lastLine.endsWith('[');
127
+ const startsWithCloseBrace = firstLine.startsWith('}') || firstLine.startsWith(')') || firstLine.startsWith(']');
128
+ if (endsWithOpenBrace || startsWithCloseBrace) {
129
+ analysis.codeBlockBoundary = {
130
+ isInCompleteBlock: false,
131
+ suggestion: endsWithOpenBrace
132
+ ? 'Edit ends with an opening bracket - ensure the closing bracket is included in a subsequent edit or already exists in the file'
133
+ : 'Edit starts with a closing bracket - ensure the opening bracket exists before this edit'
134
+ };
135
+ }
136
+ return analysis;
137
+ }
138
+ /**
139
+ * Find smart context boundaries for editing
140
+ * Expands context to include complete code blocks when possible
141
+ */
142
+ findSmartContextBoundaries(lines, startLine, endLine, requestedContext) {
143
+ const totalLines = lines.length;
144
+ let contextStart = Math.max(1, startLine - requestedContext);
145
+ let contextEnd = Math.min(totalLines, endLine + requestedContext);
146
+ let extended = false;
147
+ // Try to find the start of the enclosing block
148
+ let bracketDepth = 0;
149
+ for (let i = startLine - 1; i >= Math.max(0, startLine - 50); i--) {
150
+ const line = lines[i];
151
+ if (!line)
152
+ continue;
153
+ const trimmed = line.trim();
154
+ // Count brackets (simple approach)
155
+ const openBrackets = (line.match(/\{/g) || []).length;
156
+ const closeBrackets = (line.match(/\}/g) || []).length;
157
+ bracketDepth += closeBrackets - openBrackets;
158
+ // If we find a function/class/block definition with balanced brackets
159
+ if (bracketDepth === 0 &&
160
+ (trimmed.match(/^(function|class|const|let|var|if|for|while|async|export)\s/i) ||
161
+ trimmed.match(/=>\s*\{/) ||
162
+ trimmed.match(/^\w+\s*\(/))) {
163
+ if (i + 1 < contextStart) {
164
+ contextStart = i + 1;
165
+ extended = true;
166
+ }
167
+ break;
168
+ }
169
+ }
170
+ // Try to find the end of the enclosing block
171
+ bracketDepth = 0;
172
+ for (let i = endLine - 1; i < Math.min(totalLines, endLine + 50); i++) {
173
+ const line = lines[i];
174
+ if (!line)
175
+ continue;
176
+ const trimmed = line.trim();
177
+ // Count brackets
178
+ const openBrackets = (line.match(/\{/g) || []).length;
179
+ const closeBrackets = (line.match(/\}/g) || []).length;
180
+ bracketDepth += openBrackets - closeBrackets;
181
+ // If we find a closing bracket at depth 0
182
+ if (bracketDepth === 0 && trimmed.startsWith('}')) {
183
+ if (i + 1 > contextEnd) {
184
+ contextEnd = i + 1;
185
+ extended = true;
186
+ }
187
+ break;
188
+ }
189
+ }
190
+ return { start: contextStart, end: contextEnd, extended };
191
+ }
20
192
  /**
21
193
  * Get the content of a file with specified line range
22
194
  * @param filePath - Path to the file (relative to base path or absolute)
@@ -250,9 +422,10 @@ export class FilesystemMCPService {
250
422
  const paddedNum = String(lineNum).padStart(String(adjustedEndLine).length, ' ');
251
423
  return `${paddedNum}→${line}`;
252
424
  }).join('\n');
253
- // Calculate context range (smaller context for focused edits)
254
- const contextStart = Math.max(1, startLine - contextLines);
255
- const contextEnd = Math.min(totalLines, adjustedEndLine + contextLines);
425
+ // Calculate context range using smart boundary detection
426
+ const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, adjustedEndLine, contextLines);
427
+ const contextStart = smartBoundaries.start;
428
+ const contextEnd = smartBoundaries.end;
256
429
  // Extract old content for context (including the lines to be replaced)
257
430
  const oldContextLines = lines.slice(contextStart - 1, contextEnd);
258
431
  const oldContent = oldContextLines.map((line, idx) => {
@@ -278,6 +451,8 @@ export class FilesystemMCPService {
278
451
  }).join('\n');
279
452
  // Write the modified content back to file
280
453
  await fs.writeFile(fullPath, modifiedLines.join('\n'), 'utf-8');
454
+ // Analyze code structure of the edited content
455
+ const structureAnalysis = this.analyzeCodeStructure(modifiedLines.join('\n'), filePath, newContentLines);
281
456
  // Try to get diagnostics from VS Code after editing
282
457
  let diagnostics = [];
283
458
  try {
@@ -291,14 +466,16 @@ export class FilesystemMCPService {
291
466
  const result = {
292
467
  message: `✅ File edited successfully: ${filePath}\n` +
293
468
  ` Replaced: lines ${startLine}-${adjustedEndLine} (${linesToModify} lines)\n` +
294
- ` Result: ${newContentLines.length} new lines`,
469
+ ` Result: ${newContentLines.length} new lines` +
470
+ (smartBoundaries.extended ? `\n 📍 Context auto-extended to show complete code block (lines ${contextStart}-${newContextEnd})` : ''),
295
471
  oldContent,
296
472
  newContent: newContextContent,
297
473
  replacedLines: replacedContent,
298
474
  contextStartLine: contextStart,
299
475
  contextEndLine: newContextEnd,
300
476
  totalLines: newTotalLines,
301
- linesModified: linesToModify
477
+ linesModified: linesToModify,
478
+ structureAnalysis
302
479
  };
303
480
  // Add diagnostics if any were found
304
481
  if (diagnostics.length > 0) {
@@ -320,6 +497,46 @@ export class FilesystemMCPService {
320
497
  result.message += `\n\n ⚡ TIP: Review the errors above and make another small edit to fix them`;
321
498
  }
322
499
  }
500
+ // Add structure analysis warnings to the message
501
+ const structureWarnings = [];
502
+ // Check bracket balance
503
+ if (!structureAnalysis.bracketBalance.curly.balanced) {
504
+ const diff = structureAnalysis.bracketBalance.curly.open - structureAnalysis.bracketBalance.curly.close;
505
+ structureWarnings.push(`Curly brackets: ${diff > 0 ? `${diff} unclosed {` : `${Math.abs(diff)} extra }`}`);
506
+ }
507
+ if (!structureAnalysis.bracketBalance.round.balanced) {
508
+ const diff = structureAnalysis.bracketBalance.round.open - structureAnalysis.bracketBalance.round.close;
509
+ structureWarnings.push(`Round brackets: ${diff > 0 ? `${diff} unclosed (` : `${Math.abs(diff)} extra )`}`);
510
+ }
511
+ if (!structureAnalysis.bracketBalance.square.balanced) {
512
+ const diff = structureAnalysis.bracketBalance.square.open - structureAnalysis.bracketBalance.square.close;
513
+ structureWarnings.push(`Square brackets: ${diff > 0 ? `${diff} unclosed [` : `${Math.abs(diff)} extra ]`}`);
514
+ }
515
+ // Check HTML tags
516
+ if (structureAnalysis.htmlTags && !structureAnalysis.htmlTags.balanced) {
517
+ if (structureAnalysis.htmlTags.unclosedTags.length > 0) {
518
+ structureWarnings.push(`Unclosed HTML tags: ${structureAnalysis.htmlTags.unclosedTags.join(', ')}`);
519
+ }
520
+ if (structureAnalysis.htmlTags.unopenedTags.length > 0) {
521
+ structureWarnings.push(`Unopened closing tags: ${structureAnalysis.htmlTags.unopenedTags.join(', ')}`);
522
+ }
523
+ }
524
+ // Check indentation
525
+ if (structureAnalysis.indentationWarnings.length > 0) {
526
+ structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
527
+ }
528
+ // Add code block boundary warnings
529
+ if (structureAnalysis.codeBlockBoundary && structureAnalysis.codeBlockBoundary.suggestion) {
530
+ structureWarnings.push(`Boundary: ${structureAnalysis.codeBlockBoundary.suggestion}`);
531
+ }
532
+ // Format structure warnings
533
+ if (structureWarnings.length > 0) {
534
+ result.message += `\n\n🔍 Structure Analysis:\n`;
535
+ structureWarnings.forEach(warning => {
536
+ result.message += ` ⚠️ ${warning}\n`;
537
+ });
538
+ result.message += `\n 💡 TIP: These warnings help identify potential issues. If intentional (e.g., opening a block), you can ignore them.`;
539
+ }
323
540
  return result;
324
541
  }
325
542
  catch (error) {
@@ -513,7 +730,7 @@ export const mcpTools = [
513
730
  },
514
731
  {
515
732
  name: 'filesystem_edit',
516
- description: '🎯 PREFERRED tool for precise file editing. **BEST PRACTICES**: (1) Use SMALL, INCREMENTAL edits (recommended ≤15 lines per edit) instead of large changes - this is SAFER and MORE ACCURATE, preventing syntax errors and bracket mismatches. (2) For large changes, make MULTIPLE PARALLEL edits to different sections of the file instead of one large edit. (3) Must use exact line numbers from filesystem_read output. **WORKFLOW**: (1) Read target section with filesystem_read to get exact line numbers, (2) Edit small sections, (3) Verify with diagnostics, (4) If editing multiple sections, you can make parallel edits to non-overlapping line ranges. Returns precise before/after comparison with line numbers and VS Code diagnostics.',
733
+ description: '🎯 PREFERRED tool for precise file editing with intelligent feedback. **BEST PRACTICES**: (1) Use SMALL, INCREMENTAL edits (recommended ≤15 lines per edit) - SAFER and MORE ACCURATE, preventing syntax errors. (2) For large changes, make MULTIPLE PARALLEL edits to different sections instead of one large edit. (3) Must use exact line numbers Code boundaries should not be redundant or missing, such as `{}` or HTML tags causing syntax errors. **WORKFLOW**: (1) Read target section with filesystem_read, (2) Edit small sections, (3) Review auto-generated structure analysis and diagnostics, (4) Make parallel edits to non-overlapping ranges if needed. **SMART FEATURES**: Auto-detects bracket/tag mismatches, indentation issues, and code block boundaries. Context auto-extends to show complete functions/classes when detected.',
517
734
  inputSchema: {
518
735
  type: 'object',
519
736
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {