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.
@@ -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
- console.error('Failed to list snapshots:', error);
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();