snow-ai 0.2.25 → 0.2.26
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/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +20 -10
- package/dist/cli.js +8 -0
- package/dist/hooks/useClipboard.js +4 -4
- package/dist/hooks/useKeyboardInput.d.ts +1 -0
- package/dist/hooks/useKeyboardInput.js +8 -4
- package/dist/hooks/useTerminalFocus.d.ts +5 -0
- package/dist/hooks/useTerminalFocus.js +22 -2
- package/dist/mcp/aceCodeSearch.d.ts +58 -4
- package/dist/mcp/aceCodeSearch.js +563 -20
- package/dist/mcp/filesystem.d.ts +35 -29
- package/dist/mcp/filesystem.js +272 -122
- package/dist/mcp/ideDiagnostics.d.ts +36 -0
- package/dist/mcp/ideDiagnostics.js +92 -0
- package/dist/ui/components/ChatInput.js +6 -3
- package/dist/ui/pages/ConfigProfileScreen.d.ts +7 -0
- package/dist/ui/pages/ConfigProfileScreen.js +300 -0
- package/dist/ui/pages/ConfigScreen.js +228 -29
- package/dist/ui/pages/WelcomeScreen.js +1 -1
- package/dist/utils/apiConfig.js +12 -0
- package/dist/utils/configManager.d.ts +45 -0
- package/dist/utils/configManager.js +274 -0
- package/dist/utils/contextCompressor.js +0 -8
- package/dist/utils/escapeHandler.d.ts +79 -0
- package/dist/utils/escapeHandler.js +153 -0
- package/dist/utils/incrementalSnapshot.js +2 -1
- package/dist/utils/mcpToolsManager.js +44 -0
- package/dist/utils/textBuffer.js +13 -15
- package/dist/utils/vscodeConnection.js +26 -11
- package/dist/utils/workspaceSnapshot.js +2 -1
- package/package.json +2 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, } from 'fs';
|
|
4
|
+
import { loadConfig, saveConfig } from './apiConfig.js';
|
|
5
|
+
const CONFIG_DIR = join(homedir(), '.snow');
|
|
6
|
+
const PROFILES_DIR = join(CONFIG_DIR, 'profiles');
|
|
7
|
+
const ACTIVE_PROFILE_FILE = join(CONFIG_DIR, 'active-profile.txt');
|
|
8
|
+
const LEGACY_CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
9
|
+
/**
|
|
10
|
+
* Ensure the profiles directory exists
|
|
11
|
+
*/
|
|
12
|
+
function ensureProfilesDirectory() {
|
|
13
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
14
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
if (!existsSync(PROFILES_DIR)) {
|
|
17
|
+
mkdirSync(PROFILES_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the current active profile name
|
|
22
|
+
*/
|
|
23
|
+
export function getActiveProfileName() {
|
|
24
|
+
ensureProfilesDirectory();
|
|
25
|
+
if (!existsSync(ACTIVE_PROFILE_FILE)) {
|
|
26
|
+
return 'default';
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const profileName = readFileSync(ACTIVE_PROFILE_FILE, 'utf8').trim();
|
|
30
|
+
return profileName || 'default';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 'default';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Set the active profile
|
|
38
|
+
*/
|
|
39
|
+
function setActiveProfileName(profileName) {
|
|
40
|
+
ensureProfilesDirectory();
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(ACTIVE_PROFILE_FILE, profileName, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new Error(`Failed to set active profile: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the path to a profile file
|
|
50
|
+
*/
|
|
51
|
+
function getProfilePath(profileName) {
|
|
52
|
+
return join(PROFILES_DIR, `${profileName}.json`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Migrate legacy config.json to profiles/default.json
|
|
56
|
+
* This ensures backward compatibility with existing installations
|
|
57
|
+
*/
|
|
58
|
+
function migrateLegacyConfig() {
|
|
59
|
+
ensureProfilesDirectory();
|
|
60
|
+
const defaultProfilePath = getProfilePath('default');
|
|
61
|
+
// If default profile already exists, no migration needed
|
|
62
|
+
if (existsSync(defaultProfilePath)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// If legacy config exists, migrate it
|
|
66
|
+
if (existsSync(LEGACY_CONFIG_FILE)) {
|
|
67
|
+
try {
|
|
68
|
+
const legacyConfig = readFileSync(LEGACY_CONFIG_FILE, 'utf8');
|
|
69
|
+
writeFileSync(defaultProfilePath, legacyConfig, 'utf8');
|
|
70
|
+
// Set default as active profile
|
|
71
|
+
setActiveProfileName('default');
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// If migration fails, we'll create a default profile later
|
|
75
|
+
console.error('Failed to migrate legacy config:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Load a specific profile
|
|
81
|
+
*/
|
|
82
|
+
export function loadProfile(profileName) {
|
|
83
|
+
ensureProfilesDirectory();
|
|
84
|
+
migrateLegacyConfig();
|
|
85
|
+
const profilePath = getProfilePath(profileName);
|
|
86
|
+
if (!existsSync(profilePath)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const configData = readFileSync(profilePath, 'utf8');
|
|
91
|
+
return JSON.parse(configData);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Save a profile
|
|
99
|
+
*/
|
|
100
|
+
export function saveProfile(profileName, config) {
|
|
101
|
+
ensureProfilesDirectory();
|
|
102
|
+
const profilePath = getProfilePath(profileName);
|
|
103
|
+
try {
|
|
104
|
+
// Remove openai field for backward compatibility
|
|
105
|
+
const { openai, ...configWithoutOpenai } = config;
|
|
106
|
+
const configData = JSON.stringify(configWithoutOpenai, null, 2);
|
|
107
|
+
writeFileSync(profilePath, configData, 'utf8');
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new Error(`Failed to save profile: ${error}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get all available profiles
|
|
115
|
+
*/
|
|
116
|
+
export function getAllProfiles() {
|
|
117
|
+
ensureProfilesDirectory();
|
|
118
|
+
migrateLegacyConfig();
|
|
119
|
+
const activeProfile = getActiveProfileName();
|
|
120
|
+
const profiles = [];
|
|
121
|
+
try {
|
|
122
|
+
const files = readdirSync(PROFILES_DIR);
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
if (file.endsWith('.json')) {
|
|
125
|
+
const profileName = file.replace('.json', '');
|
|
126
|
+
const config = loadProfile(profileName);
|
|
127
|
+
if (config) {
|
|
128
|
+
profiles.push({
|
|
129
|
+
name: profileName,
|
|
130
|
+
displayName: getProfileDisplayName(profileName),
|
|
131
|
+
isActive: profileName === activeProfile,
|
|
132
|
+
config,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// If reading fails, return empty array
|
|
140
|
+
}
|
|
141
|
+
// Ensure at least a default profile exists
|
|
142
|
+
if (profiles.length === 0) {
|
|
143
|
+
const defaultConfig = loadConfig();
|
|
144
|
+
saveProfile('default', defaultConfig);
|
|
145
|
+
profiles.push({
|
|
146
|
+
name: 'default',
|
|
147
|
+
displayName: 'Default',
|
|
148
|
+
isActive: true,
|
|
149
|
+
config: defaultConfig,
|
|
150
|
+
});
|
|
151
|
+
setActiveProfileName('default');
|
|
152
|
+
}
|
|
153
|
+
return profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get a user-friendly display name for a profile
|
|
157
|
+
*/
|
|
158
|
+
function getProfileDisplayName(profileName) {
|
|
159
|
+
// Capitalize first letter
|
|
160
|
+
return profileName.charAt(0).toUpperCase() + profileName.slice(1);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Switch to a different profile
|
|
164
|
+
* This copies the profile config to config.json and updates the active profile
|
|
165
|
+
*/
|
|
166
|
+
export function switchProfile(profileName) {
|
|
167
|
+
ensureProfilesDirectory();
|
|
168
|
+
const profileConfig = loadProfile(profileName);
|
|
169
|
+
if (!profileConfig) {
|
|
170
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
171
|
+
}
|
|
172
|
+
// Save the profile config to the main config.json (for backward compatibility)
|
|
173
|
+
saveConfig(profileConfig);
|
|
174
|
+
// Update the active profile marker
|
|
175
|
+
setActiveProfileName(profileName);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create a new profile
|
|
179
|
+
*/
|
|
180
|
+
export function createProfile(profileName, config) {
|
|
181
|
+
ensureProfilesDirectory();
|
|
182
|
+
// Validate profile name
|
|
183
|
+
if (!profileName.trim() || profileName.includes('/') || profileName.includes('\\')) {
|
|
184
|
+
throw new Error('Invalid profile name');
|
|
185
|
+
}
|
|
186
|
+
const profilePath = getProfilePath(profileName);
|
|
187
|
+
if (existsSync(profilePath)) {
|
|
188
|
+
throw new Error(`Profile "${profileName}" already exists`);
|
|
189
|
+
}
|
|
190
|
+
// If no config provided, use the current config
|
|
191
|
+
const profileConfig = config || loadConfig();
|
|
192
|
+
saveProfile(profileName, profileConfig);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Delete a profile
|
|
196
|
+
*/
|
|
197
|
+
export function deleteProfile(profileName) {
|
|
198
|
+
ensureProfilesDirectory();
|
|
199
|
+
// Don't allow deleting the default profile
|
|
200
|
+
if (profileName === 'default') {
|
|
201
|
+
throw new Error('Cannot delete the default profile');
|
|
202
|
+
}
|
|
203
|
+
const profilePath = getProfilePath(profileName);
|
|
204
|
+
if (!existsSync(profilePath)) {
|
|
205
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
206
|
+
}
|
|
207
|
+
// If this is the active profile, switch to default first
|
|
208
|
+
if (getActiveProfileName() === profileName) {
|
|
209
|
+
switchProfile('default');
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
unlinkSync(profilePath);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
throw new Error(`Failed to delete profile: ${error}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Rename a profile
|
|
220
|
+
*/
|
|
221
|
+
export function renameProfile(oldName, newName) {
|
|
222
|
+
ensureProfilesDirectory();
|
|
223
|
+
// Validate new name
|
|
224
|
+
if (!newName.trim() || newName.includes('/') || newName.includes('\\')) {
|
|
225
|
+
throw new Error('Invalid profile name');
|
|
226
|
+
}
|
|
227
|
+
if (oldName === newName) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const oldPath = getProfilePath(oldName);
|
|
231
|
+
const newPath = getProfilePath(newName);
|
|
232
|
+
if (!existsSync(oldPath)) {
|
|
233
|
+
throw new Error(`Profile "${oldName}" not found`);
|
|
234
|
+
}
|
|
235
|
+
if (existsSync(newPath)) {
|
|
236
|
+
throw new Error(`Profile "${newName}" already exists`);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const config = loadProfile(oldName);
|
|
240
|
+
if (!config) {
|
|
241
|
+
throw new Error(`Failed to load profile "${oldName}"`);
|
|
242
|
+
}
|
|
243
|
+
// Save with new name
|
|
244
|
+
saveProfile(newName, config);
|
|
245
|
+
// Update active profile if necessary
|
|
246
|
+
if (getActiveProfileName() === oldName) {
|
|
247
|
+
setActiveProfileName(newName);
|
|
248
|
+
}
|
|
249
|
+
// Delete old profile
|
|
250
|
+
unlinkSync(oldPath);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
throw new Error(`Failed to rename profile: ${error}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Initialize profiles system
|
|
258
|
+
* This should be called on app startup to ensure profiles are set up
|
|
259
|
+
*/
|
|
260
|
+
export function initializeProfiles() {
|
|
261
|
+
ensureProfilesDirectory();
|
|
262
|
+
migrateLegacyConfig();
|
|
263
|
+
// Ensure the active profile exists and is loaded to config.json
|
|
264
|
+
const activeProfile = getActiveProfileName();
|
|
265
|
+
const profileConfig = loadProfile(activeProfile);
|
|
266
|
+
if (profileConfig) {
|
|
267
|
+
// Sync the active profile to config.json
|
|
268
|
+
saveConfig(profileConfig);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// If active profile doesn't exist, switch to default
|
|
272
|
+
switchProfile('default');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -48,8 +48,6 @@ async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversat
|
|
|
48
48
|
stream: true,
|
|
49
49
|
stream_options: { include_usage: true },
|
|
50
50
|
};
|
|
51
|
-
// Log request payload
|
|
52
|
-
console.log('[ContextCompressor] Chat Completions Request:', JSON.stringify(requestPayload, null, 2));
|
|
53
51
|
// Use streaming to avoid timeout
|
|
54
52
|
const stream = (await client.chat.completions.create(requestPayload, {
|
|
55
53
|
headers: customHeaders,
|
|
@@ -132,8 +130,6 @@ async function compressWithResponses(baseUrl, apiKey, modelName, conversationMes
|
|
|
132
130
|
input,
|
|
133
131
|
stream: true,
|
|
134
132
|
};
|
|
135
|
-
// Log request payload
|
|
136
|
-
console.log('[ContextCompressor] Responses API Request:', JSON.stringify(requestPayload, null, 2));
|
|
137
133
|
// Use streaming to avoid timeout
|
|
138
134
|
const stream = await client.responses.create(requestPayload, {
|
|
139
135
|
headers: customHeaders,
|
|
@@ -229,8 +225,6 @@ async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessag
|
|
|
229
225
|
systemInstruction,
|
|
230
226
|
contents,
|
|
231
227
|
};
|
|
232
|
-
// Log request payload
|
|
233
|
-
console.log('[ContextCompressor] Gemini Request:', JSON.stringify(requestConfig, null, 2));
|
|
234
228
|
// Use streaming to avoid timeout
|
|
235
229
|
const stream = await client.models.generateContentStream(requestConfig);
|
|
236
230
|
let summary = '';
|
|
@@ -305,8 +299,6 @@ async function compressWithAnthropic(baseUrl, apiKey, modelName, conversationMes
|
|
|
305
299
|
system: systemParam,
|
|
306
300
|
messages,
|
|
307
301
|
};
|
|
308
|
-
// Log request payload
|
|
309
|
-
console.log('[ContextCompressor] Anthropic Request:', JSON.stringify(requestPayload, null, 2));
|
|
310
302
|
// Use streaming to avoid timeout
|
|
311
303
|
const stream = await client.messages.stream(requestPayload);
|
|
312
304
|
let summary = '';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape Handler Utility
|
|
3
|
+
* Handles escape sequence issues in AI-generated content
|
|
4
|
+
* Based on Gemini CLI's approach to handle common LLM escaping bugs
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Unescapes a string that might have been overly escaped by an LLM.
|
|
8
|
+
* Common issues:
|
|
9
|
+
* - "\\n" should be "\n" (newline)
|
|
10
|
+
* - "\\t" should be "\t" (tab)
|
|
11
|
+
* - "\\`" should be "`" (backtick)
|
|
12
|
+
* - "\\\\" should be "\\" (single backslash)
|
|
13
|
+
* - "\\"Hello\\"" should be "\"Hello\"" (quotes)
|
|
14
|
+
*
|
|
15
|
+
* @param inputString - The potentially over-escaped string from AI
|
|
16
|
+
* @returns The unescaped string
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* unescapeString("console.log(\\"Hello\\\\n\\")")
|
|
20
|
+
* // Returns: console.log("Hello\n")
|
|
21
|
+
*
|
|
22
|
+
* unescapeString("const msg = \`Hello \\`\${name}\\`\`")
|
|
23
|
+
* // Returns: const msg = `Hello `${name}``
|
|
24
|
+
*/
|
|
25
|
+
export declare function unescapeString(inputString: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a string appears to be over-escaped by comparing it with its unescaped version
|
|
28
|
+
*
|
|
29
|
+
* @param inputString - The string to check
|
|
30
|
+
* @returns True if the string contains escape sequences that would be modified by unescapeString
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* isOverEscaped("console.log(\\"Hello\\")") // Returns: true
|
|
34
|
+
* isOverEscaped("console.log(\"Hello\")") // Returns: false
|
|
35
|
+
*/
|
|
36
|
+
export declare function isOverEscaped(inputString: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Counts occurrences of a substring in a string
|
|
39
|
+
* Used to verify if unescaping helps find the correct match
|
|
40
|
+
*
|
|
41
|
+
* @param str - The string to search in
|
|
42
|
+
* @param substr - The substring to search for
|
|
43
|
+
* @returns Number of occurrences found
|
|
44
|
+
*/
|
|
45
|
+
export declare function countOccurrences(str: string, substr: string): number;
|
|
46
|
+
/**
|
|
47
|
+
* Attempts to fix a search string that doesn't match by trying unescaping
|
|
48
|
+
* This is a lightweight, non-LLM approach to handle common escaping issues
|
|
49
|
+
*
|
|
50
|
+
* @param fileContent - The content of the file to search in
|
|
51
|
+
* @param searchString - The search string that failed to match
|
|
52
|
+
* @param expectedOccurrences - Expected number of matches (default: 1)
|
|
53
|
+
* @returns Object with corrected string and match count, or null if correction didn't help
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const fixed = tryUnescapeFix(fileContent, "console.log(\\"Hello\\")", 1);
|
|
57
|
+
* if (fixed) {
|
|
58
|
+
* // Use fixed.correctedString for the search
|
|
59
|
+
* }
|
|
60
|
+
*/
|
|
61
|
+
export declare function tryUnescapeFix(fileContent: string, searchString: string, expectedOccurrences?: number): {
|
|
62
|
+
correctedString: string;
|
|
63
|
+
occurrences: number;
|
|
64
|
+
} | null;
|
|
65
|
+
/**
|
|
66
|
+
* Smart trimming that preserves the relationship between paired strings
|
|
67
|
+
* If trimming the target string results in the expected number of matches,
|
|
68
|
+
* also trim the paired string to maintain consistency
|
|
69
|
+
*
|
|
70
|
+
* @param targetString - The string to potentially trim
|
|
71
|
+
* @param pairedString - The paired string (e.g., replacement content)
|
|
72
|
+
* @param fileContent - The file content to search in
|
|
73
|
+
* @param expectedOccurrences - Expected number of matches
|
|
74
|
+
* @returns Object with potentially trimmed strings
|
|
75
|
+
*/
|
|
76
|
+
export declare function trimPairIfPossible(targetString: string, pairedString: string, fileContent: string, expectedOccurrences?: number): {
|
|
77
|
+
target: string;
|
|
78
|
+
paired: string;
|
|
79
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape Handler Utility
|
|
3
|
+
* Handles escape sequence issues in AI-generated content
|
|
4
|
+
* Based on Gemini CLI's approach to handle common LLM escaping bugs
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Unescapes a string that might have been overly escaped by an LLM.
|
|
8
|
+
* Common issues:
|
|
9
|
+
* - "\\n" should be "\n" (newline)
|
|
10
|
+
* - "\\t" should be "\t" (tab)
|
|
11
|
+
* - "\\`" should be "`" (backtick)
|
|
12
|
+
* - "\\\\" should be "\\" (single backslash)
|
|
13
|
+
* - "\\"Hello\\"" should be "\"Hello\"" (quotes)
|
|
14
|
+
*
|
|
15
|
+
* @param inputString - The potentially over-escaped string from AI
|
|
16
|
+
* @returns The unescaped string
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* unescapeString("console.log(\\"Hello\\\\n\\")")
|
|
20
|
+
* // Returns: console.log("Hello\n")
|
|
21
|
+
*
|
|
22
|
+
* unescapeString("const msg = \`Hello \\`\${name}\\`\`")
|
|
23
|
+
* // Returns: const msg = `Hello `${name}``
|
|
24
|
+
*/
|
|
25
|
+
export function unescapeString(inputString) {
|
|
26
|
+
// Regex explanation:
|
|
27
|
+
// \\+ : Matches one or more literal backslash characters
|
|
28
|
+
// (n|t|r|'|"|`|\\|\n) : Capturing group that matches:
|
|
29
|
+
// n, t, r : Literal characters for escape sequences
|
|
30
|
+
// ', ", ` : Quote characters
|
|
31
|
+
// \\ : Literal backslash
|
|
32
|
+
// \n : Actual newline character
|
|
33
|
+
// g : Global flag to replace all occurrences
|
|
34
|
+
return inputString.replace(/\\+(n|t|r|'|"|`|\\|\n)/g, (match, capturedChar) => {
|
|
35
|
+
// 'match' is the entire erroneous sequence, e.g., "\\n" or "\\\\`"
|
|
36
|
+
// 'capturedChar' is the character that determines the true meaning
|
|
37
|
+
switch (capturedChar) {
|
|
38
|
+
case 'n':
|
|
39
|
+
return '\n'; // Newline character
|
|
40
|
+
case 't':
|
|
41
|
+
return '\t'; // Tab character
|
|
42
|
+
case 'r':
|
|
43
|
+
return '\r'; // Carriage return
|
|
44
|
+
case "'":
|
|
45
|
+
return "'"; // Single quote
|
|
46
|
+
case '"':
|
|
47
|
+
return '"'; // Double quote
|
|
48
|
+
case '`':
|
|
49
|
+
return '`'; // Backtick
|
|
50
|
+
case '\\':
|
|
51
|
+
return '\\'; // Single backslash
|
|
52
|
+
case '\n':
|
|
53
|
+
return '\n'; // Clean newline (handles "\\\n" cases)
|
|
54
|
+
default:
|
|
55
|
+
// Fallback: return original match if unexpected character
|
|
56
|
+
return match;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Checks if a string appears to be over-escaped by comparing it with its unescaped version
|
|
62
|
+
*
|
|
63
|
+
* @param inputString - The string to check
|
|
64
|
+
* @returns True if the string contains escape sequences that would be modified by unescapeString
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* isOverEscaped("console.log(\\"Hello\\")") // Returns: true
|
|
68
|
+
* isOverEscaped("console.log(\"Hello\")") // Returns: false
|
|
69
|
+
*/
|
|
70
|
+
export function isOverEscaped(inputString) {
|
|
71
|
+
return unescapeString(inputString) !== inputString;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Counts occurrences of a substring in a string
|
|
75
|
+
* Used to verify if unescaping helps find the correct match
|
|
76
|
+
*
|
|
77
|
+
* @param str - The string to search in
|
|
78
|
+
* @param substr - The substring to search for
|
|
79
|
+
* @returns Number of occurrences found
|
|
80
|
+
*/
|
|
81
|
+
export function countOccurrences(str, substr) {
|
|
82
|
+
if (substr === '') {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
let count = 0;
|
|
86
|
+
let pos = str.indexOf(substr);
|
|
87
|
+
while (pos !== -1) {
|
|
88
|
+
count++;
|
|
89
|
+
pos = str.indexOf(substr, pos + substr.length);
|
|
90
|
+
}
|
|
91
|
+
return count;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Attempts to fix a search string that doesn't match by trying unescaping
|
|
95
|
+
* This is a lightweight, non-LLM approach to handle common escaping issues
|
|
96
|
+
*
|
|
97
|
+
* @param fileContent - The content of the file to search in
|
|
98
|
+
* @param searchString - The search string that failed to match
|
|
99
|
+
* @param expectedOccurrences - Expected number of matches (default: 1)
|
|
100
|
+
* @returns Object with corrected string and match count, or null if correction didn't help
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* const fixed = tryUnescapeFix(fileContent, "console.log(\\"Hello\\")", 1);
|
|
104
|
+
* if (fixed) {
|
|
105
|
+
* // Use fixed.correctedString for the search
|
|
106
|
+
* }
|
|
107
|
+
*/
|
|
108
|
+
export function tryUnescapeFix(fileContent, searchString, expectedOccurrences = 1) {
|
|
109
|
+
// Check if the string appears to be over-escaped
|
|
110
|
+
if (!isOverEscaped(searchString)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// Try unescaping
|
|
114
|
+
const unescaped = unescapeString(searchString);
|
|
115
|
+
// Count occurrences with unescaped version
|
|
116
|
+
const occurrences = countOccurrences(fileContent, unescaped);
|
|
117
|
+
// Return result if it matches expected occurrences
|
|
118
|
+
if (occurrences === expectedOccurrences) {
|
|
119
|
+
return {
|
|
120
|
+
correctedString: unescaped,
|
|
121
|
+
occurrences,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Smart trimming that preserves the relationship between paired strings
|
|
128
|
+
* If trimming the target string results in the expected number of matches,
|
|
129
|
+
* also trim the paired string to maintain consistency
|
|
130
|
+
*
|
|
131
|
+
* @param targetString - The string to potentially trim
|
|
132
|
+
* @param pairedString - The paired string (e.g., replacement content)
|
|
133
|
+
* @param fileContent - The file content to search in
|
|
134
|
+
* @param expectedOccurrences - Expected number of matches
|
|
135
|
+
* @returns Object with potentially trimmed strings
|
|
136
|
+
*/
|
|
137
|
+
export function trimPairIfPossible(targetString, pairedString, fileContent, expectedOccurrences = 1) {
|
|
138
|
+
const trimmedTarget = targetString.trim();
|
|
139
|
+
// If trimming doesn't change the string, return as-is
|
|
140
|
+
if (targetString.length === trimmedTarget.length) {
|
|
141
|
+
return { target: targetString, paired: pairedString };
|
|
142
|
+
}
|
|
143
|
+
// Check if trimmed version matches expected occurrences
|
|
144
|
+
const trimmedOccurrences = countOccurrences(fileContent, trimmedTarget);
|
|
145
|
+
if (trimmedOccurrences === expectedOccurrences) {
|
|
146
|
+
return {
|
|
147
|
+
target: trimmedTarget,
|
|
148
|
+
paired: pairedString.trim(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Trimming didn't help, return original
|
|
152
|
+
return { target: targetString, paired: pairedString };
|
|
153
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
4
5
|
/**
|
|
5
6
|
* Incremental Snapshot Manager
|
|
6
7
|
* Only backs up files that are actually modified by tools
|
|
@@ -117,7 +118,7 @@ class IncrementalSnapshotManager {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
catch (error) {
|
|
120
|
-
|
|
121
|
+
logger.error('Failed to list snapshots:', error);
|
|
121
122
|
}
|
|
122
123
|
return snapshots.sort((a, b) => b.messageIndex - a.messageIndex);
|
|
123
124
|
}
|
|
@@ -7,6 +7,7 @@ import { mcpTools as filesystemTools } from '../mcp/filesystem.js';
|
|
|
7
7
|
import { mcpTools as terminalTools } from '../mcp/bash.js';
|
|
8
8
|
import { mcpTools as aceCodeSearchTools } from '../mcp/aceCodeSearch.js';
|
|
9
9
|
import { mcpTools as websearchTools } from '../mcp/websearch.js';
|
|
10
|
+
import { mcpTools as ideDiagnosticsTools } from '../mcp/ideDiagnostics.js';
|
|
10
11
|
import { TodoService } from '../mcp/todo.js';
|
|
11
12
|
import { sessionManager } from './sessionManager.js';
|
|
12
13
|
import { logger } from './logger.js';
|
|
@@ -177,6 +178,28 @@ async function refreshToolsCache() {
|
|
|
177
178
|
},
|
|
178
179
|
});
|
|
179
180
|
}
|
|
181
|
+
// Add built-in IDE Diagnostics tools (always available)
|
|
182
|
+
const ideDiagnosticsServiceTools = ideDiagnosticsTools.map(tool => ({
|
|
183
|
+
name: tool.name.replace('ide_', ''),
|
|
184
|
+
description: tool.description,
|
|
185
|
+
inputSchema: tool.inputSchema,
|
|
186
|
+
}));
|
|
187
|
+
servicesInfo.push({
|
|
188
|
+
serviceName: 'ide',
|
|
189
|
+
tools: ideDiagnosticsServiceTools,
|
|
190
|
+
isBuiltIn: true,
|
|
191
|
+
connected: true,
|
|
192
|
+
});
|
|
193
|
+
for (const tool of ideDiagnosticsTools) {
|
|
194
|
+
allTools.push({
|
|
195
|
+
type: 'function',
|
|
196
|
+
function: {
|
|
197
|
+
name: `ide-${tool.name.replace('ide_', '')}`,
|
|
198
|
+
description: tool.description,
|
|
199
|
+
parameters: tool.inputSchema,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
180
203
|
// Add user-configured MCP server tools (probe for availability but don't maintain connections)
|
|
181
204
|
try {
|
|
182
205
|
const mcpConfig = getMCPConfig();
|
|
@@ -519,6 +542,10 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
|
|
|
519
542
|
serviceName = 'websearch';
|
|
520
543
|
actualToolName = toolName.substring('websearch-'.length);
|
|
521
544
|
}
|
|
545
|
+
else if (toolName.startsWith('ide-')) {
|
|
546
|
+
serviceName = 'ide';
|
|
547
|
+
actualToolName = toolName.substring('ide-'.length);
|
|
548
|
+
}
|
|
522
549
|
else {
|
|
523
550
|
// Check configured MCP services
|
|
524
551
|
try {
|
|
@@ -625,6 +652,23 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
|
|
|
625
652
|
throw new Error(`Unknown websearch tool: ${actualToolName}`);
|
|
626
653
|
}
|
|
627
654
|
}
|
|
655
|
+
else if (serviceName === 'ide') {
|
|
656
|
+
// Handle built-in IDE Diagnostics tools (no connection needed)
|
|
657
|
+
const { ideDiagnosticsService } = await import('../mcp/ideDiagnostics.js');
|
|
658
|
+
switch (actualToolName) {
|
|
659
|
+
case 'get_diagnostics':
|
|
660
|
+
const diagnostics = await ideDiagnosticsService.getDiagnostics(args.filePath);
|
|
661
|
+
// Format diagnostics for better readability
|
|
662
|
+
const formatted = ideDiagnosticsService.formatDiagnostics(diagnostics, args.filePath);
|
|
663
|
+
return {
|
|
664
|
+
diagnostics,
|
|
665
|
+
formatted,
|
|
666
|
+
summary: `Found ${diagnostics.length} diagnostic(s) in ${args.filePath}`,
|
|
667
|
+
};
|
|
668
|
+
default:
|
|
669
|
+
throw new Error(`Unknown IDE tool: ${actualToolName}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
628
672
|
else {
|
|
629
673
|
// Handle user-configured MCP service tools - connect only when needed
|
|
630
674
|
const mcpConfig = getMCPConfig();
|