snow-ai 0.3.5 → 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 +65 -9
- 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 +27 -7
- 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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search utilities for ACE Code Search
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { EOL } from 'os';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Check if a command is available in the system PATH
|
|
9
|
+
* @param command - Command to check
|
|
10
|
+
* @returns Promise resolving to true if command is available
|
|
11
|
+
*/
|
|
12
|
+
export function isCommandAvailable(command) {
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
try {
|
|
15
|
+
let child;
|
|
16
|
+
if (process.platform === 'win32') {
|
|
17
|
+
// Windows: where is an executable, no shell needed
|
|
18
|
+
child = spawn('where', [command], {
|
|
19
|
+
stdio: 'ignore',
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Unix/Linux: Use 'which' command instead of 'command -v'
|
|
25
|
+
// 'which' is an external executable, not a shell builtin
|
|
26
|
+
child = spawn('which', [command], {
|
|
27
|
+
stdio: 'ignore',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
child.on('close', code => resolve(code === 0));
|
|
31
|
+
child.on('error', () => resolve(false));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
resolve(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse grep output (format: filePath:lineNumber:lineContent)
|
|
40
|
+
* @param output - Grep output string
|
|
41
|
+
* @param basePath - Base path for relative path calculation
|
|
42
|
+
* @returns Array of search results
|
|
43
|
+
*/
|
|
44
|
+
export function parseGrepOutput(output, basePath) {
|
|
45
|
+
const results = [];
|
|
46
|
+
if (!output)
|
|
47
|
+
return results;
|
|
48
|
+
const lines = output.split(EOL);
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
continue;
|
|
52
|
+
// Find first and second colon indices
|
|
53
|
+
const firstColonIndex = line.indexOf(':');
|
|
54
|
+
if (firstColonIndex === -1)
|
|
55
|
+
continue;
|
|
56
|
+
const secondColonIndex = line.indexOf(':', firstColonIndex + 1);
|
|
57
|
+
if (secondColonIndex === -1)
|
|
58
|
+
continue;
|
|
59
|
+
// Extract parts
|
|
60
|
+
const filePathRaw = line.substring(0, firstColonIndex);
|
|
61
|
+
const lineNumberStr = line.substring(firstColonIndex + 1, secondColonIndex);
|
|
62
|
+
const lineContent = line.substring(secondColonIndex + 1);
|
|
63
|
+
const lineNumber = parseInt(lineNumberStr, 10);
|
|
64
|
+
if (isNaN(lineNumber))
|
|
65
|
+
continue;
|
|
66
|
+
const absoluteFilePath = path.resolve(basePath, filePathRaw);
|
|
67
|
+
const relativeFilePath = path.relative(basePath, absoluteFilePath);
|
|
68
|
+
results.push({
|
|
69
|
+
filePath: relativeFilePath || path.basename(absoluteFilePath),
|
|
70
|
+
line: lineNumber,
|
|
71
|
+
column: 1, // grep doesn't provide column info, default to 1
|
|
72
|
+
content: lineContent.trim(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Convert glob pattern to RegExp
|
|
79
|
+
* Supports: *, **, ?, [abc], {js,ts}
|
|
80
|
+
* @param glob - Glob pattern
|
|
81
|
+
* @returns Regular expression
|
|
82
|
+
*/
|
|
83
|
+
export function globToRegex(glob) {
|
|
84
|
+
// Escape special regex characters except glob wildcards
|
|
85
|
+
let pattern = glob
|
|
86
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
87
|
+
.replace(/\*\*/g, '<<<DOUBLESTAR>>>') // Temporarily replace **
|
|
88
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
89
|
+
.replace(/<<<DOUBLESTAR>>>/g, '.*') // ** matches everything
|
|
90
|
+
.replace(/\?/g, '[^/]'); // ? matches single char except /
|
|
91
|
+
// Handle {js,ts} alternatives
|
|
92
|
+
pattern = pattern.replace(/\\{([^}]+)\\}/g, (_, alternatives) => {
|
|
93
|
+
return '(' + alternatives.split(',').join('|') + ')';
|
|
94
|
+
});
|
|
95
|
+
// Handle [abc] character classes (already valid regex)
|
|
96
|
+
pattern = pattern.replace(/\\\[([^\]]+)\\\]/g, '[$1]');
|
|
97
|
+
return new RegExp(pattern, 'i');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Calculate fuzzy match score for symbol name
|
|
101
|
+
* @param symbolName - Symbol name to score
|
|
102
|
+
* @param query - Search query
|
|
103
|
+
* @returns Score (0-100, higher is better)
|
|
104
|
+
*/
|
|
105
|
+
export function calculateFuzzyScore(symbolName, query) {
|
|
106
|
+
const nameLower = symbolName.toLowerCase();
|
|
107
|
+
const queryLower = query.toLowerCase();
|
|
108
|
+
// Exact match
|
|
109
|
+
if (nameLower === queryLower)
|
|
110
|
+
return 100;
|
|
111
|
+
// Starts with
|
|
112
|
+
if (nameLower.startsWith(queryLower))
|
|
113
|
+
return 80;
|
|
114
|
+
// Contains
|
|
115
|
+
if (nameLower.includes(queryLower))
|
|
116
|
+
return 60;
|
|
117
|
+
// Camel case match (e.g., "gfc" matches "getFileContent")
|
|
118
|
+
const camelCaseMatch = symbolName
|
|
119
|
+
.split(/(?=[A-Z])/)
|
|
120
|
+
.map(s => s[0]?.toLowerCase() || '')
|
|
121
|
+
.join('');
|
|
122
|
+
if (camelCaseMatch.includes(queryLower))
|
|
123
|
+
return 40;
|
|
124
|
+
// Fuzzy match
|
|
125
|
+
let score = 0;
|
|
126
|
+
let queryIndex = 0;
|
|
127
|
+
for (let i = 0; i < nameLower.length && queryIndex < queryLower.length; i++) {
|
|
128
|
+
if (nameLower[i] === queryLower[queryIndex]) {
|
|
129
|
+
score += 20;
|
|
130
|
+
queryIndex++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (queryIndex === queryLower.length)
|
|
134
|
+
return score;
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol parsing utilities for ACE Code Search
|
|
3
|
+
*/
|
|
4
|
+
import type { CodeSymbol } from '../../types/aceCodeSearch.types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Get context lines around a specific line
|
|
7
|
+
* @param lines - All lines in file
|
|
8
|
+
* @param lineIndex - Target line index (0-based)
|
|
9
|
+
* @param contextSize - Number of lines before and after
|
|
10
|
+
* @returns Context string
|
|
11
|
+
*/
|
|
12
|
+
export declare function getContext(lines: string[], lineIndex: number, contextSize: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse file content to extract code symbols using regex patterns
|
|
15
|
+
* @param filePath - Path to file
|
|
16
|
+
* @param content - File content
|
|
17
|
+
* @param basePath - Base path for relative path calculation
|
|
18
|
+
* @returns Array of code symbols
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseFileSymbols(filePath: string, content: string, basePath: string): Promise<CodeSymbol[]>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol parsing utilities for ACE Code Search
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { LANGUAGE_CONFIG, detectLanguage } from './language.utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get context lines around a specific line
|
|
8
|
+
* @param lines - All lines in file
|
|
9
|
+
* @param lineIndex - Target line index (0-based)
|
|
10
|
+
* @param contextSize - Number of lines before and after
|
|
11
|
+
* @returns Context string
|
|
12
|
+
*/
|
|
13
|
+
export function getContext(lines, lineIndex, contextSize) {
|
|
14
|
+
const start = Math.max(0, lineIndex - contextSize);
|
|
15
|
+
const end = Math.min(lines.length, lineIndex + contextSize + 1);
|
|
16
|
+
return lines
|
|
17
|
+
.slice(start, end)
|
|
18
|
+
.filter(l => l !== undefined)
|
|
19
|
+
.join('\n')
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse file content to extract code symbols using regex patterns
|
|
24
|
+
* @param filePath - Path to file
|
|
25
|
+
* @param content - File content
|
|
26
|
+
* @param basePath - Base path for relative path calculation
|
|
27
|
+
* @returns Array of code symbols
|
|
28
|
+
*/
|
|
29
|
+
export async function parseFileSymbols(filePath, content, basePath) {
|
|
30
|
+
const symbols = [];
|
|
31
|
+
const language = detectLanguage(filePath);
|
|
32
|
+
if (!language || !LANGUAGE_CONFIG[language]) {
|
|
33
|
+
return symbols;
|
|
34
|
+
}
|
|
35
|
+
const config = LANGUAGE_CONFIG[language];
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
// Parse each line for symbols
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i];
|
|
40
|
+
if (!line)
|
|
41
|
+
continue;
|
|
42
|
+
const lineNumber = i + 1;
|
|
43
|
+
// Extract functions
|
|
44
|
+
if (config.symbolPatterns.function) {
|
|
45
|
+
const match = line.match(config.symbolPatterns.function);
|
|
46
|
+
if (match) {
|
|
47
|
+
const name = match[1] || match[2] || match[3];
|
|
48
|
+
if (name) {
|
|
49
|
+
// Get function signature (current line + next few lines)
|
|
50
|
+
const contextLines = lines.slice(i, Math.min(i + 3, lines.length));
|
|
51
|
+
const signature = contextLines.join('\n').trim();
|
|
52
|
+
symbols.push({
|
|
53
|
+
name,
|
|
54
|
+
type: 'function',
|
|
55
|
+
filePath: path.relative(basePath, filePath),
|
|
56
|
+
line: lineNumber,
|
|
57
|
+
column: line.indexOf(name) + 1,
|
|
58
|
+
signature,
|
|
59
|
+
language,
|
|
60
|
+
context: getContext(lines, i, 2),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Extract classes
|
|
66
|
+
if (config.symbolPatterns.class) {
|
|
67
|
+
const match = line.match(config.symbolPatterns.class);
|
|
68
|
+
if (match) {
|
|
69
|
+
const name = match[1] || match[2] || match[3];
|
|
70
|
+
if (name) {
|
|
71
|
+
symbols.push({
|
|
72
|
+
name,
|
|
73
|
+
type: 'class',
|
|
74
|
+
filePath: path.relative(basePath, filePath),
|
|
75
|
+
line: lineNumber,
|
|
76
|
+
column: line.indexOf(name) + 1,
|
|
77
|
+
signature: line.trim(),
|
|
78
|
+
language,
|
|
79
|
+
context: getContext(lines, i, 2),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Extract variables
|
|
85
|
+
if (config.symbolPatterns.variable) {
|
|
86
|
+
const match = line.match(config.symbolPatterns.variable);
|
|
87
|
+
if (match) {
|
|
88
|
+
const name = match[1];
|
|
89
|
+
if (name) {
|
|
90
|
+
symbols.push({
|
|
91
|
+
name,
|
|
92
|
+
type: 'variable',
|
|
93
|
+
filePath: path.relative(basePath, filePath),
|
|
94
|
+
line: lineNumber,
|
|
95
|
+
column: line.indexOf(name) + 1,
|
|
96
|
+
signature: line.trim(),
|
|
97
|
+
language,
|
|
98
|
+
context: getContext(lines, i, 1),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Extract imports
|
|
104
|
+
if (config.symbolPatterns.import) {
|
|
105
|
+
const match = line.match(config.symbolPatterns.import);
|
|
106
|
+
if (match) {
|
|
107
|
+
const name = match[1] || match[2];
|
|
108
|
+
if (name) {
|
|
109
|
+
symbols.push({
|
|
110
|
+
name,
|
|
111
|
+
type: 'import',
|
|
112
|
+
filePath: path.relative(basePath, filePath),
|
|
113
|
+
line: lineNumber,
|
|
114
|
+
column: line.indexOf(name) + 1,
|
|
115
|
+
signature: line.trim(),
|
|
116
|
+
language,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Extract exports
|
|
122
|
+
if (config.symbolPatterns.export) {
|
|
123
|
+
const match = line.match(config.symbolPatterns.export);
|
|
124
|
+
if (match) {
|
|
125
|
+
const name = match[1];
|
|
126
|
+
if (name) {
|
|
127
|
+
symbols.push({
|
|
128
|
+
name,
|
|
129
|
+
type: 'export',
|
|
130
|
+
filePath: path.relative(basePath, filePath),
|
|
131
|
+
line: lineNumber,
|
|
132
|
+
column: line.indexOf(name) + 1,
|
|
133
|
+
signature: line.trim(),
|
|
134
|
+
language,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return symbols;
|
|
141
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for terminal command execution
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Dangerous command patterns that should be blocked
|
|
6
|
+
*/
|
|
7
|
+
export declare const DANGEROUS_PATTERNS: RegExp[];
|
|
8
|
+
/**
|
|
9
|
+
* Check if a command contains dangerous patterns
|
|
10
|
+
* @param command - Command to check
|
|
11
|
+
* @returns true if command is dangerous
|
|
12
|
+
*/
|
|
13
|
+
export declare function isDangerousCommand(command: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Truncate output if it exceeds maximum length
|
|
16
|
+
* @param output - Output string to truncate
|
|
17
|
+
* @param maxLength - Maximum allowed length
|
|
18
|
+
* @returns Truncated output with indicator if truncated
|
|
19
|
+
*/
|
|
20
|
+
export declare function truncateOutput(output: string, maxLength: number): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for terminal command execution
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Dangerous command patterns that should be blocked
|
|
6
|
+
*/
|
|
7
|
+
export const DANGEROUS_PATTERNS = [
|
|
8
|
+
/rm\s+-rf\s+\/[^/\s]*/i, // rm -rf / or /path
|
|
9
|
+
/>\s*\/dev\/sda/i, // writing to disk devices
|
|
10
|
+
/mkfs/i, // format filesystem
|
|
11
|
+
/dd\s+if=/i, // disk operations
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Check if a command contains dangerous patterns
|
|
15
|
+
* @param command - Command to check
|
|
16
|
+
* @returns true if command is dangerous
|
|
17
|
+
*/
|
|
18
|
+
export function isDangerousCommand(command) {
|
|
19
|
+
return DANGEROUS_PATTERNS.some(pattern => pattern.test(command));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Truncate output if it exceeds maximum length
|
|
23
|
+
* @param output - Output string to truncate
|
|
24
|
+
* @param maxLength - Maximum allowed length
|
|
25
|
+
* @returns Truncated output with indicator if truncated
|
|
26
|
+
*/
|
|
27
|
+
export function truncateOutput(output, maxLength) {
|
|
28
|
+
if (!output)
|
|
29
|
+
return '';
|
|
30
|
+
if (output.length > maxLength) {
|
|
31
|
+
return output.slice(0, maxLength) + '\n... (output truncated)';
|
|
32
|
+
}
|
|
33
|
+
return output;
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code analysis utilities for structure validation
|
|
3
|
+
*/
|
|
4
|
+
import type { StructureAnalysis } from '../../types/filesystem.types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Analyze code structure for balance and completeness
|
|
7
|
+
* Helps AI identify bracket mismatches, unclosed tags, and boundary issues
|
|
8
|
+
*/
|
|
9
|
+
export declare function analyzeCodeStructure(_content: string, filePath: string, editedLines: string[]): StructureAnalysis;
|
|
10
|
+
/**
|
|
11
|
+
* Find smart context boundaries for editing
|
|
12
|
+
* Expands context to include complete code blocks when possible
|
|
13
|
+
*/
|
|
14
|
+
export declare function findSmartContextBoundaries(lines: string[], startLine: number, endLine: number, requestedContext: number): {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
extended: boolean;
|
|
18
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code analysis utilities for structure validation
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Analyze code structure for balance and completeness
|
|
6
|
+
* Helps AI identify bracket mismatches, unclosed tags, and boundary issues
|
|
7
|
+
*/
|
|
8
|
+
export function analyzeCodeStructure(_content, filePath, editedLines) {
|
|
9
|
+
const analysis = {
|
|
10
|
+
bracketBalance: {
|
|
11
|
+
curly: { open: 0, close: 0, balanced: true },
|
|
12
|
+
round: { open: 0, close: 0, balanced: true },
|
|
13
|
+
square: { open: 0, close: 0, balanced: true },
|
|
14
|
+
},
|
|
15
|
+
indentationWarnings: [],
|
|
16
|
+
};
|
|
17
|
+
// Count brackets in the edited content
|
|
18
|
+
const editedContent = editedLines.join('\n');
|
|
19
|
+
// Remove string literals and comments to avoid false positives
|
|
20
|
+
const cleanContent = editedContent
|
|
21
|
+
.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, '""') // Remove strings
|
|
22
|
+
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
|
23
|
+
.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments
|
|
24
|
+
// Count brackets
|
|
25
|
+
analysis.bracketBalance.curly.open = (cleanContent.match(/\{/g) || []).length;
|
|
26
|
+
analysis.bracketBalance.curly.close = (cleanContent.match(/\}/g) || []).length;
|
|
27
|
+
analysis.bracketBalance.curly.balanced =
|
|
28
|
+
analysis.bracketBalance.curly.open === analysis.bracketBalance.curly.close;
|
|
29
|
+
analysis.bracketBalance.round.open = (cleanContent.match(/\(/g) || []).length;
|
|
30
|
+
analysis.bracketBalance.round.close = (cleanContent.match(/\)/g) || []).length;
|
|
31
|
+
analysis.bracketBalance.round.balanced =
|
|
32
|
+
analysis.bracketBalance.round.open === analysis.bracketBalance.round.close;
|
|
33
|
+
analysis.bracketBalance.square.open = (cleanContent.match(/\[/g) || []).length;
|
|
34
|
+
analysis.bracketBalance.square.close = (cleanContent.match(/\]/g) || []).length;
|
|
35
|
+
analysis.bracketBalance.square.balanced =
|
|
36
|
+
analysis.bracketBalance.square.open ===
|
|
37
|
+
analysis.bracketBalance.square.close;
|
|
38
|
+
// HTML/JSX tag analysis (for .html, .jsx, .tsx, .vue files)
|
|
39
|
+
const isMarkupFile = /\.(html|jsx|tsx|vue)$/i.test(filePath);
|
|
40
|
+
if (isMarkupFile) {
|
|
41
|
+
const tagPattern = /<\/?([a-zA-Z][a-zA-Z0-9-]*)[^>]*>/g;
|
|
42
|
+
const selfClosingPattern = /<[a-zA-Z][a-zA-Z0-9-]*[^>]*\/>/g;
|
|
43
|
+
// Remove self-closing tags
|
|
44
|
+
const contentWithoutSelfClosing = cleanContent.replace(selfClosingPattern, '');
|
|
45
|
+
const tags = [];
|
|
46
|
+
const unclosedTags = [];
|
|
47
|
+
const unopenedTags = [];
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = tagPattern.exec(contentWithoutSelfClosing)) !== null) {
|
|
50
|
+
const isClosing = match[0]?.startsWith('</');
|
|
51
|
+
const tagName = match[1]?.toLowerCase();
|
|
52
|
+
if (!tagName)
|
|
53
|
+
continue;
|
|
54
|
+
if (isClosing) {
|
|
55
|
+
const lastOpenTag = tags.pop();
|
|
56
|
+
if (!lastOpenTag || lastOpenTag !== tagName) {
|
|
57
|
+
unopenedTags.push(tagName);
|
|
58
|
+
if (lastOpenTag)
|
|
59
|
+
tags.push(lastOpenTag); // Put it back
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
tags.push(tagName);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
unclosedTags.push(...tags);
|
|
67
|
+
analysis.htmlTags = {
|
|
68
|
+
unclosedTags,
|
|
69
|
+
unopenedTags,
|
|
70
|
+
balanced: unclosedTags.length === 0 && unopenedTags.length === 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Check indentation consistency
|
|
74
|
+
const lines = editedContent.split('\n');
|
|
75
|
+
const indents = lines
|
|
76
|
+
.filter(line => line.trim().length > 0)
|
|
77
|
+
.map(line => {
|
|
78
|
+
const match = line.match(/^(\s*)/);
|
|
79
|
+
return match ? match[1] : '';
|
|
80
|
+
})
|
|
81
|
+
.filter((indent) => indent !== undefined);
|
|
82
|
+
// Detect mixed tabs/spaces
|
|
83
|
+
const hasTabs = indents.some(indent => indent.includes('\t'));
|
|
84
|
+
const hasSpaces = indents.some(indent => indent.includes(' '));
|
|
85
|
+
if (hasTabs && hasSpaces) {
|
|
86
|
+
analysis.indentationWarnings.push('Mixed tabs and spaces detected');
|
|
87
|
+
}
|
|
88
|
+
// Detect inconsistent indentation levels (spaces only)
|
|
89
|
+
if (!hasTabs && hasSpaces) {
|
|
90
|
+
const spaceCounts = indents
|
|
91
|
+
.filter(indent => indent.length > 0)
|
|
92
|
+
.map(indent => indent.length);
|
|
93
|
+
if (spaceCounts.length > 1) {
|
|
94
|
+
const gcd = spaceCounts.reduce((a, b) => {
|
|
95
|
+
while (b !== 0) {
|
|
96
|
+
const temp = b;
|
|
97
|
+
b = a % b;
|
|
98
|
+
a = temp;
|
|
99
|
+
}
|
|
100
|
+
return a;
|
|
101
|
+
});
|
|
102
|
+
const hasInconsistent = spaceCounts.some(count => count % gcd !== 0 && gcd > 1);
|
|
103
|
+
if (hasInconsistent) {
|
|
104
|
+
analysis.indentationWarnings.push(`Inconsistent indentation (expected multiples of ${gcd} spaces)`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Note: Boundary checking removed - AI should be free to edit partial code blocks
|
|
109
|
+
// The bracket balance check above is sufficient for detecting real issues
|
|
110
|
+
return analysis;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Find smart context boundaries for editing
|
|
114
|
+
* Expands context to include complete code blocks when possible
|
|
115
|
+
*/
|
|
116
|
+
export function findSmartContextBoundaries(lines, startLine, endLine, requestedContext) {
|
|
117
|
+
const totalLines = lines.length;
|
|
118
|
+
let contextStart = Math.max(1, startLine - requestedContext);
|
|
119
|
+
let contextEnd = Math.min(totalLines, endLine + requestedContext);
|
|
120
|
+
let extended = false;
|
|
121
|
+
// Try to find the start of the enclosing block
|
|
122
|
+
let bracketDepth = 0;
|
|
123
|
+
for (let i = startLine - 1; i >= Math.max(0, startLine - 50); i--) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (!line)
|
|
126
|
+
continue;
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
// Count brackets (simple approach)
|
|
129
|
+
const openBrackets = (line.match(/\{/g) || []).length;
|
|
130
|
+
const closeBrackets = (line.match(/\}/g) || []).length;
|
|
131
|
+
bracketDepth += closeBrackets - openBrackets;
|
|
132
|
+
// If we find a function/class/block definition with balanced brackets
|
|
133
|
+
if (bracketDepth === 0 &&
|
|
134
|
+
(trimmed.match(/^(function|class|const|let|var|if|for|while|async|export)\s/i) ||
|
|
135
|
+
trimmed.match(/=>\s*\{/) ||
|
|
136
|
+
trimmed.match(/^\w+\s*\(/))) {
|
|
137
|
+
if (i + 1 < contextStart) {
|
|
138
|
+
contextStart = i + 1;
|
|
139
|
+
extended = true;
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Try to find the end of the enclosing block
|
|
145
|
+
bracketDepth = 0;
|
|
146
|
+
for (let i = endLine - 1; i < Math.min(totalLines, endLine + 50); i++) {
|
|
147
|
+
const line = lines[i];
|
|
148
|
+
if (!line)
|
|
149
|
+
continue;
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
// Count brackets
|
|
152
|
+
const openBrackets = (line.match(/\{/g) || []).length;
|
|
153
|
+
const closeBrackets = (line.match(/\}/g) || []).length;
|
|
154
|
+
bracketDepth += openBrackets - closeBrackets;
|
|
155
|
+
// If we find a closing bracket at depth 0
|
|
156
|
+
if (bracketDepth === 0 && trimmed.startsWith('}')) {
|
|
157
|
+
if (i + 1 > contextEnd) {
|
|
158
|
+
contextEnd = i + 1;
|
|
159
|
+
extended = true;
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { start: contextStart, end: contextEnd, extended };
|
|
165
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match finding utilities for fuzzy search
|
|
3
|
+
*/
|
|
4
|
+
import type { MatchCandidate } from '../../types/filesystem.types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Find the closest matching candidates in the file content
|
|
7
|
+
* Returns top N candidates sorted by similarity
|
|
8
|
+
* Optimized with safe pre-filtering and early exit
|
|
9
|
+
* ASYNC to prevent terminal freeze during search
|
|
10
|
+
*/
|
|
11
|
+
export declare function findClosestMatches(searchContent: string, fileLines: string[], topN?: number): Promise<MatchCandidate[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Generate a helpful diff message showing differences between search and actual content
|
|
14
|
+
* Note: This is ONLY for display purposes. Tabs/spaces are normalized for better readability.
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateDiffMessage(searchContent: string, actualContent: string, maxLines?: number): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match finding utilities for fuzzy search
|
|
3
|
+
*/
|
|
4
|
+
import { calculateSimilarity, normalizeForDisplay } from './similarity.utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Find the closest matching candidates in the file content
|
|
7
|
+
* Returns top N candidates sorted by similarity
|
|
8
|
+
* Optimized with safe pre-filtering and early exit
|
|
9
|
+
* ASYNC to prevent terminal freeze during search
|
|
10
|
+
*/
|
|
11
|
+
export async function findClosestMatches(searchContent, fileLines, topN = 3) {
|
|
12
|
+
const searchLines = searchContent.split('\n');
|
|
13
|
+
const candidates = [];
|
|
14
|
+
// Fast pre-filter: use first line as anchor (only for multi-line searches)
|
|
15
|
+
const searchFirstLine = searchLines[0]?.replace(/\s+/g, ' ').trim() || '';
|
|
16
|
+
const threshold = 0.5;
|
|
17
|
+
const usePreFilter = searchLines.length >= 5; // Only for 5+ line searches
|
|
18
|
+
const preFilterThreshold = 0.2; // Very conservative - only skip completely unrelated lines
|
|
19
|
+
// Try to find candidates by sliding window with optimizations
|
|
20
|
+
const maxCandidates = topN * 3; // Collect more candidates, then pick best
|
|
21
|
+
const YIELD_INTERVAL = 100; // Yield control every 100 iterations
|
|
22
|
+
for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
|
|
23
|
+
// Yield control periodically to prevent UI freeze
|
|
24
|
+
if (i % YIELD_INTERVAL === 0) {
|
|
25
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
26
|
+
}
|
|
27
|
+
// Quick pre-filter: check first line similarity (only for multi-line)
|
|
28
|
+
if (usePreFilter) {
|
|
29
|
+
const firstLineCandidate = fileLines[i]?.replace(/\s+/g, ' ').trim() || '';
|
|
30
|
+
const firstLineSimilarity = calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
|
|
31
|
+
// Skip only if first line is very different
|
|
32
|
+
if (firstLineSimilarity < preFilterThreshold) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Full candidate check
|
|
37
|
+
const candidateLines = fileLines.slice(i, i + searchLines.length);
|
|
38
|
+
const candidateContent = candidateLines.join('\n');
|
|
39
|
+
const similarity = calculateSimilarity(searchContent, candidateContent, threshold);
|
|
40
|
+
// Only consider candidates with >50% similarity
|
|
41
|
+
if (similarity > threshold) {
|
|
42
|
+
candidates.push({
|
|
43
|
+
startLine: i + 1,
|
|
44
|
+
endLine: i + searchLines.length,
|
|
45
|
+
similarity,
|
|
46
|
+
preview: candidateLines
|
|
47
|
+
.map((line, idx) => `${i + idx + 1}→${normalizeForDisplay(line)}`)
|
|
48
|
+
.join('\n'),
|
|
49
|
+
});
|
|
50
|
+
// Early exit if we found a nearly perfect match
|
|
51
|
+
if (similarity >= 0.95) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
// Limit candidates to avoid excessive computation
|
|
55
|
+
if (candidates.length >= maxCandidates) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Sort by similarity descending and return top N
|
|
61
|
+
return candidates.sort((a, b) => b.similarity - a.similarity).slice(0, topN);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate a helpful diff message showing differences between search and actual content
|
|
65
|
+
* Note: This is ONLY for display purposes. Tabs/spaces are normalized for better readability.
|
|
66
|
+
*/
|
|
67
|
+
export function generateDiffMessage(searchContent, actualContent, maxLines = 10) {
|
|
68
|
+
const searchLines = searchContent.split('\n');
|
|
69
|
+
const actualLines = actualContent.split('\n');
|
|
70
|
+
const diffLines = [];
|
|
71
|
+
const maxLen = Math.max(searchLines.length, actualLines.length);
|
|
72
|
+
for (let i = 0; i < Math.min(maxLen, maxLines); i++) {
|
|
73
|
+
const searchLine = searchLines[i] || '';
|
|
74
|
+
const actualLine = actualLines[i] || '';
|
|
75
|
+
if (searchLine !== actualLine) {
|
|
76
|
+
diffLines.push(`Line ${i + 1}:`);
|
|
77
|
+
diffLines.push(` Search: ${JSON.stringify(normalizeForDisplay(searchLine))}`);
|
|
78
|
+
diffLines.push(` Actual: ${JSON.stringify(normalizeForDisplay(actualLine))}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (maxLen > maxLines) {
|
|
82
|
+
diffLines.push(`... (${maxLen - maxLines} more lines)`);
|
|
83
|
+
}
|
|
84
|
+
return diffLines.join('\n');
|
|
85
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Similarity calculation utilities for fuzzy matching
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Calculate similarity between two strings using a smarter algorithm
|
|
6
|
+
* This normalizes whitespace first to avoid false negatives from spacing differences
|
|
7
|
+
* Returns a value between 0 (completely different) and 1 (identical)
|
|
8
|
+
*/
|
|
9
|
+
export declare function calculateSimilarity(str1: string, str2: string, threshold?: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Calculate Levenshtein distance between two strings with early termination
|
|
12
|
+
* @param str1 First string
|
|
13
|
+
* @param str2 Second string
|
|
14
|
+
* @param maxDistance Maximum distance to compute (early exit if exceeded)
|
|
15
|
+
* @returns Levenshtein distance, or maxDistance+1 if exceeded
|
|
16
|
+
*/
|
|
17
|
+
export declare function levenshteinDistance(str1: string, str2: string, maxDistance?: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* Normalize whitespace for display purposes
|
|
20
|
+
* Makes preview more readable by collapsing whitespace
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalizeForDisplay(line: string): string;
|