snow-ai 0.4.5 → 0.4.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.
@@ -283,6 +283,7 @@ export const en = {
283
283
  quickAccessTitle: '🔍 Quick Access:',
284
284
  insertFiles: '@ - Insert files from project',
285
285
  searchContent: '@@ - Search file content',
286
+ selectAgent: '# - Select sub-agent for task execution',
286
287
  showCommands: '/ - Show available commands',
287
288
  navigationTitle: '📋 Navigation:',
288
289
  navigateHistory: '↑/↓ - Navigate command/message history',
@@ -329,7 +330,7 @@ export const en = {
329
330
  headerExplanations: 'Ask for code explanations and debugging help',
330
331
  headerInterrupt: 'Press ESC during response to interrupt',
331
332
  headerYolo: 'Press Shift+Tab: toggle YOLO',
332
- headerShortcuts: "Shortcuts: Ctrl+L (delete to start) • Ctrl+R (delete to end) • {pasteKey} (paste images) • '@' (files) • '@@' (search content) • '/' (commands)",
333
+ headerShortcuts: "Shortcuts: Ctrl+L (delete to start) • Ctrl+R (delete to end) • {pasteKey} (paste images) • '@' (files) • '@@' (search content) • '#' (sub-agents) • '/' (commands)",
333
334
  headerWorkingDirectory: 'Working directory: {directory}',
334
335
  // Status messages
335
336
  statusThinking: 'Thinking...',
@@ -283,6 +283,7 @@ export const es = {
283
283
  quickAccessTitle: '🔍 Acceso Rápido:',
284
284
  insertFiles: '@ - Insertar archivos del proyecto',
285
285
  searchContent: '@@ - Buscar contenido de archivos',
286
+ selectAgent: '# - Seleccionar sub-agente para ejecutar tarea',
286
287
  showCommands: '/ - Mostrar comandos disponibles',
287
288
  navigationTitle: '📋 Navegación:',
288
289
  navigateHistory: '↑/↓ - Navegar por el historial de comandos/mensajes',
@@ -329,7 +330,7 @@ export const es = {
329
330
  headerExplanations: 'Solicita explicaciones de código y ayuda de depuración',
330
331
  headerInterrupt: 'Presiona ESC durante la respuesta para interrumpir',
331
332
  headerYolo: 'Presiona Shift+Tab: Alternar YOLO',
332
- headerShortcuts: "Atajos: Ctrl+L (eliminar hasta inicio) • Ctrl+R (eliminar hasta final) • {pasteKey} (pegar imagen) • '@' (archivo) • '@@' (buscar contenido) • '/' (comando)",
333
+ headerShortcuts: "Atajos: Ctrl+L (eliminar hasta inicio) • Ctrl+R (eliminar hasta final) • {pasteKey} (pegar imagen) • '@' (archivo) • '@@' (buscar contenido) • '#' (sub-agentes) • '/' (comando)",
333
334
  headerWorkingDirectory: 'Directorio de Trabajo: {directory}',
334
335
  // Status messages
335
336
  statusThinking: 'Pensando...',
@@ -283,6 +283,7 @@ export const ja = {
283
283
  quickAccessTitle: '🔍 クイックアクセス:',
284
284
  insertFiles: '@ - プロジェクトからファイルを挿入',
285
285
  searchContent: '@@ - ファイル内容を検索',
286
+ selectAgent: '# - サブエージェントを選択してタスクを実行',
286
287
  showCommands: '/ - 利用可能なコマンドを表示',
287
288
  navigationTitle: '📋 ナビゲーション:',
288
289
  navigateHistory: '↑/↓ - コマンド/メッセージ履歴を移動',
@@ -329,7 +330,7 @@ export const ja = {
329
330
  headerExplanations: 'コードの説明とデバッグヘルプを問い合わせ',
330
331
  headerInterrupt: '応答中にESCで中断',
331
332
  headerYolo: 'Shift+Tabを押す: YOLOを切替',
332
- headerShortcuts: "ショートカット: Ctrl+L (先頭まで削除) • Ctrl+R (末尾まで削除) • {pasteKey} (画像貼付) • '@' (ファイル) • '@@' (内容検索) • '/' (コマンド)",
333
+ headerShortcuts: "ショートカット: Ctrl+L (先頭まで削除) • Ctrl+R (末尾まで削除) • {pasteKey} (画像貼付) • '@' (ファイル) • '@@' (内容検索) • '#' (サブエージェント) • '/' (コマンド)",
333
334
  headerWorkingDirectory: '作業ディレクトリ: {directory}',
334
335
  // Status messages
335
336
  statusThinking: '思考中...',
@@ -283,6 +283,7 @@ export const ko = {
283
283
  quickAccessTitle: '🔍 빠른 접근:',
284
284
  insertFiles: '@ - 프로젝트에서 파일 삽입',
285
285
  searchContent: '@@ - 파일 내용 검색',
286
+ selectAgent: '# - 작업 실행을 위한 하위 에이전트 선택',
286
287
  showCommands: '/ - 사용 가능한 명령 표시',
287
288
  navigationTitle: '📋 탐색:',
288
289
  navigateHistory: '↑/↓ - 명령/메시지 기록 탐색',
@@ -329,7 +330,7 @@ export const ko = {
329
330
  headerExplanations: '코드 설명 및 디버그 도움 요청',
330
331
  headerInterrupt: '응답 중 ESC로 중단',
331
332
  headerYolo: 'Shift+Tab 누르기: YOLO 토글',
332
- headerShortcuts: "단축키: Ctrl+L (시작까지 삭제) • Ctrl+R (끝까지 삭제) • {pasteKey} (이미지 붙여넣기) • '@' (파일) • '@@' (내용 검색) • '/' (명령)",
333
+ headerShortcuts: "단축키: Ctrl+L (시작까지 삭제) • Ctrl+R (끝까지 삭제) • {pasteKey} (이미지 붙여넣기) • '@' (파일) • '@@' (내용 검색) • '#' (하위 에이전트) • '/' (명령)",
333
334
  headerWorkingDirectory: '작업 디렉토리: {directory}',
334
335
  // Status messages
335
336
  statusThinking: '생각 중...',
@@ -283,6 +283,7 @@ export const zhTW = {
283
283
  quickAccessTitle: '🔍 快速存取:',
284
284
  insertFiles: '@ - 從專案插入檔案',
285
285
  searchContent: '@@ - 搜尋檔案內容',
286
+ selectAgent: '# - 選擇子代理執行任務',
286
287
  showCommands: '/ - 顯示可用命令',
287
288
  navigationTitle: '📋 導航:',
288
289
  navigateHistory: '↑/↓ - 導航命令/訊息歷史',
@@ -329,7 +330,7 @@ export const zhTW = {
329
330
  headerExplanations: '詢問程式碼說明和偵錯協助',
330
331
  headerInterrupt: '在回應期間按 ESC 中斷',
331
332
  headerYolo: '按 Shift+Tab: 切換 YOLO',
332
- headerShortcuts: "快捷鍵: Ctrl+L (刪除至開頭) • Ctrl+R (刪除至末尾) • {pasteKey} (貼上圖片) • '@' (檔案) • '@@' (搜尋內容) • '/' (命令)",
333
+ headerShortcuts: "快捷鍵: Ctrl+L (刪除至開頭) • Ctrl+R (刪除至末尾) • {pasteKey} (貼上圖片) • '@' (檔案) • '@@' (搜尋內容) • '#' (子代理) • '/' (命令)",
333
334
  headerWorkingDirectory: '工作目錄: {directory}',
334
335
  // Status messages
335
336
  statusThinking: '思考中...',
@@ -283,6 +283,7 @@ export const zh = {
283
283
  quickAccessTitle: '🔍 快速访问:',
284
284
  insertFiles: '@ - 从项目插入文件',
285
285
  searchContent: '@@ - 搜索文件内容',
286
+ selectAgent: '# - 选择子代理执行任务',
286
287
  showCommands: '/ - 显示可用命令',
287
288
  navigationTitle: '📋 导航:',
288
289
  navigateHistory: '↑/↓ - 导航命令/消息历史',
@@ -329,7 +330,7 @@ export const zh = {
329
330
  headerExplanations: '询问代码说明和调试帮助',
330
331
  headerInterrupt: '在响应期间按 ESC 中断',
331
332
  headerYolo: '按 Shift+Tab: 切换 YOLO',
332
- headerShortcuts: "快捷键: Ctrl+L (删除至开头) • Ctrl+R (删除至末尾) • {pasteKey} (粘贴图片) • '@' (文件) • '@@' (搜索内容) • '/' (命令)",
333
+ headerShortcuts: "快捷键: Ctrl+L (删除至开头) • Ctrl+R (删除至末尾) • {pasteKey} (粘贴图片) • '@' (文件) • '@@' (搜索内容) • '#' (子代理) • '/' (命令)",
333
334
  headerWorkingDirectory: '工作目录: {directory}',
334
335
  // Status messages
335
336
  statusThinking: '思考中...',
@@ -280,6 +280,7 @@ export type TranslationKeys = {
280
280
  quickAccessTitle: string;
281
281
  insertFiles: string;
282
282
  searchContent: string;
283
+ selectAgent: string;
283
284
  showCommands: string;
284
285
  navigationTitle: string;
285
286
  navigateHistory: string;
@@ -1,4 +1,4 @@
1
- import type { EditBySearchConfig, EditByLineConfig, EditBySearchResult, EditByLineResult } from './types/filesystem.types.js';
1
+ import type { EditBySearchConfig, EditByLineConfig, EditBySearchResult, EditByLineResult, SingleFileReadResult, MultipleFilesReadResult } from './types/filesystem.types.js';
2
2
  /**
3
3
  * Filesystem MCP Service
4
4
  * Provides basic file operations: read, create, and delete files
@@ -10,6 +10,30 @@ export declare class FilesystemMCPService {
10
10
  */
11
11
  private readonly prettierSupportedExtensions;
12
12
  constructor(basePath?: string);
13
+ /**
14
+ * Check if a file is an image based on extension
15
+ * @param filePath - Path to the file
16
+ * @returns True if the file is an image
17
+ */
18
+ private isImageFile;
19
+ /**
20
+ * Check if a file is an Office document based on extension
21
+ * @param filePath - Path to the file
22
+ * @returns True if the file is an Office document
23
+ */
24
+ private isOfficeFile;
25
+ /**
26
+ * Get MIME type for an image file
27
+ * @param filePath - Path to the file
28
+ * @returns MIME type or undefined if not an image
29
+ */
30
+ private getImageMimeType;
31
+ /**
32
+ * Read image file and convert to base64
33
+ * @param fullPath - Full path to the image file
34
+ * @returns ImageContent object with base64 data
35
+ */
36
+ private readImageAsBase64;
13
37
  /**
14
38
  * Extract relevant symbol information for a specific line range
15
39
  * This provides context that helps AI make more accurate modifications
@@ -29,31 +53,18 @@ export declare class FilesystemMCPService {
29
53
  /**
30
54
  * Get the content of a file with optional line range
31
55
  * Enhanced with symbol information for better AI context
56
+ * Supports multimodal content (text + images)
32
57
  * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
33
58
  * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
34
59
  * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to file end). Used for single file or as default for array of strings
35
- * @returns Object containing the requested content with line numbers and metadata
60
+ * @returns Object containing the requested content with line numbers and metadata (supports multimodal content)
36
61
  * @throws Error if file doesn't exist or cannot be read
37
62
  */
38
63
  getFileContent(filePath: string | string[] | Array<{
39
64
  path: string;
40
65
  startLine?: number;
41
66
  endLine?: number;
42
- }>, startLine?: number, endLine?: number): Promise<{
43
- content: string;
44
- startLine: number;
45
- endLine: number;
46
- totalLines: number;
47
- } | {
48
- content: string;
49
- files: Array<{
50
- path: string;
51
- startLine: number;
52
- endLine: number;
53
- totalLines: number;
54
- }>;
55
- totalFiles: number;
56
- }>;
67
+ }>, startLine?: number, endLine?: number): Promise<SingleFileReadResult | MultipleFilesReadResult>;
57
68
  /**
58
69
  * Create a new file with specified content
59
70
  * @param filePath - Path where the file should be created
@@ -5,16 +5,19 @@ import * as prettier from 'prettier';
5
5
  import { vscodeConnection } from '../utils/vscodeConnection.js';
6
6
  import { incrementalSnapshotManager } from '../utils/incrementalSnapshot.js';
7
7
  import { tryUnescapeFix, trimPairIfPossible, isOverEscaped, } from '../utils/escapeHandler.js';
8
+ import { IMAGE_MIME_TYPES, OFFICE_FILE_TYPES } from './types/filesystem.types.js';
8
9
  // Utility functions
9
10
  import { calculateSimilarity, normalizeForDisplay, } from './utils/filesystem/similarity.utils.js';
10
11
  import { analyzeCodeStructure, findSmartContextBoundaries, } from './utils/filesystem/code-analysis.utils.js';
11
12
  import { findClosestMatches, generateDiffMessage, } from './utils/filesystem/match-finder.utils.js';
12
13
  import { parseEditBySearchParams, parseEditByLineParams, executeBatchOperation, } from './utils/filesystem/batch-operations.utils.js';
14
+ import { tryFixPath } from './utils/filesystem/path-fixer.utils.js';
15
+ import { readOfficeDocument } from './utils/filesystem/office-parser.utils.js';
13
16
  // ACE Code Search utilities for symbol parsing
14
17
  import { parseFileSymbols } from './utils/aceCodeSearch/symbol.utils.js';
15
18
  // Notebook utilities for automatic note retrieval
16
19
  import { queryNotebook } from '../utils/notebookManager.js';
17
- const { resolve, dirname, isAbsolute } = path;
20
+ const { resolve, dirname, isAbsolute, extname } = path;
18
21
  /**
19
22
  * Filesystem MCP Service
20
23
  * Provides basic file operations: read, create, and delete files
@@ -54,6 +57,57 @@ export class FilesystemMCPService {
54
57
  });
55
58
  this.basePath = resolve(basePath);
56
59
  }
60
+ /**
61
+ * Check if a file is an image based on extension
62
+ * @param filePath - Path to the file
63
+ * @returns True if the file is an image
64
+ */
65
+ isImageFile(filePath) {
66
+ const ext = extname(filePath).toLowerCase();
67
+ return ext in IMAGE_MIME_TYPES;
68
+ }
69
+ /**
70
+ * Check if a file is an Office document based on extension
71
+ * @param filePath - Path to the file
72
+ * @returns True if the file is an Office document
73
+ */
74
+ isOfficeFile(filePath) {
75
+ const ext = extname(filePath).toLowerCase();
76
+ return ext in OFFICE_FILE_TYPES;
77
+ }
78
+ /**
79
+ * Get MIME type for an image file
80
+ * @param filePath - Path to the file
81
+ * @returns MIME type or undefined if not an image
82
+ */
83
+ getImageMimeType(filePath) {
84
+ const ext = extname(filePath).toLowerCase();
85
+ return IMAGE_MIME_TYPES[ext];
86
+ }
87
+ /**
88
+ * Read image file and convert to base64
89
+ * @param fullPath - Full path to the image file
90
+ * @returns ImageContent object with base64 data
91
+ */
92
+ async readImageAsBase64(fullPath) {
93
+ try {
94
+ const mimeType = this.getImageMimeType(fullPath);
95
+ if (!mimeType) {
96
+ return null;
97
+ }
98
+ const buffer = await fs.readFile(fullPath);
99
+ const base64Data = buffer.toString('base64');
100
+ return {
101
+ type: 'image',
102
+ data: base64Data,
103
+ mimeType,
104
+ };
105
+ }
106
+ catch (error) {
107
+ console.error(`Failed to read image ${fullPath}:`, error);
108
+ return null;
109
+ }
110
+ }
57
111
  /**
58
112
  * Extract relevant symbol information for a specific line range
59
113
  * This provides context that helps AI make more accurate modifications
@@ -155,10 +209,11 @@ export class FilesystemMCPService {
155
209
  /**
156
210
  * Get the content of a file with optional line range
157
211
  * Enhanced with symbol information for better AI context
212
+ * Supports multimodal content (text + images)
158
213
  * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
159
214
  * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
160
215
  * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to file end). Used for single file or as default for array of strings
161
- * @returns Object containing the requested content with line numbers and metadata
216
+ * @returns Object containing the requested content with line numbers and metadata (supports multimodal content)
162
217
  * @throws Error if file doesn't exist or cannot be read
163
218
  */
164
219
  async getFileContent(filePath, startLine, endLine) {
@@ -166,7 +221,7 @@ export class FilesystemMCPService {
166
221
  // Handle array of files
167
222
  if (Array.isArray(filePath)) {
168
223
  const filesData = [];
169
- const allContents = [];
224
+ const multimodalContent = [];
170
225
  for (const fileItem of filePath) {
171
226
  try {
172
227
  // Support both string format and object format
@@ -195,7 +250,10 @@ export class FilesystemMCPService {
195
250
  if (stats.isDirectory()) {
196
251
  const dirFiles = await this.listFiles(file);
197
252
  const fileList = dirFiles.join('\n');
198
- allContents.push(`📁 Directory: ${file}\n${fileList}`);
253
+ multimodalContent.push({
254
+ type: 'text',
255
+ text: `📁 Directory: ${file}\n${fileList}`,
256
+ });
199
257
  filesData.push({
200
258
  path: file,
201
259
  startLine: 1,
@@ -204,6 +262,44 @@ export class FilesystemMCPService {
204
262
  });
205
263
  continue;
206
264
  }
265
+ // Check if this is an image file
266
+ if (this.isImageFile(fullPath)) {
267
+ const imageContent = await this.readImageAsBase64(fullPath);
268
+ if (imageContent) {
269
+ // Add text description first
270
+ multimodalContent.push({
271
+ type: 'text',
272
+ text: `🖼️ Image: ${file} (${imageContent.mimeType})`,
273
+ });
274
+ // Add image content
275
+ multimodalContent.push(imageContent);
276
+ filesData.push({
277
+ path: file,
278
+ isImage: true,
279
+ mimeType: imageContent.mimeType,
280
+ });
281
+ continue;
282
+ }
283
+ }
284
+ // Check if this is an Office document file
285
+ if (this.isOfficeFile(fullPath)) {
286
+ const docContent = await readOfficeDocument(fullPath);
287
+ if (docContent) {
288
+ // Add text description first
289
+ multimodalContent.push({
290
+ type: 'text',
291
+ text: `📄 ${docContent.fileType.toUpperCase()} Document: ${file}`,
292
+ });
293
+ // Add document content
294
+ multimodalContent.push(docContent);
295
+ filesData.push({
296
+ path: file,
297
+ isDocument: true,
298
+ fileType: docContent.fileType,
299
+ });
300
+ continue;
301
+ }
302
+ }
207
303
  const content = await fs.readFile(fullPath, 'utf-8');
208
304
  const lines = content.split('\n');
209
305
  const totalLines = lines.length;
@@ -217,10 +313,8 @@ export class FilesystemMCPService {
217
313
  if (actualEndLine < actualStartLine) {
218
314
  throw new Error(`End line must be greater than or equal to start line for ${file}`);
219
315
  }
220
- if (actualStartLine > totalLines) {
221
- throw new Error(`Start line ${actualStartLine} exceeds file length ${totalLines} for ${file}`);
222
- }
223
- const start = actualStartLine;
316
+ // Auto-adjust if startLine exceeds file length
317
+ const start = Math.min(actualStartLine, totalLines);
224
318
  const end = Math.min(totalLines, actualEndLine);
225
319
  // Extract specified lines
226
320
  const selectedLines = lines.slice(start - 1, end);
@@ -245,7 +339,10 @@ export class FilesystemMCPService {
245
339
  if (notebookInfo) {
246
340
  fileContent += notebookInfo;
247
341
  }
248
- allContents.push(fileContent);
342
+ multimodalContent.push({
343
+ type: 'text',
344
+ text: fileContent,
345
+ });
249
346
  filesData.push({
250
347
  path: file,
251
348
  startLine: start,
@@ -257,11 +354,14 @@ export class FilesystemMCPService {
257
354
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
258
355
  // Extract file path for error message
259
356
  const filePath = typeof fileItem === 'string' ? fileItem : fileItem.path;
260
- allContents.push(`❌ ${filePath}: ${errorMsg}`);
357
+ multimodalContent.push({
358
+ type: 'text',
359
+ text: `❌ ${filePath}: ${errorMsg}`,
360
+ });
261
361
  }
262
362
  }
263
363
  return {
264
- content: allContents.join('\n\n'),
364
+ content: multimodalContent,
265
365
  files: filesData,
266
366
  totalFiles: filePath.length,
267
367
  };
@@ -285,6 +385,41 @@ export class FilesystemMCPService {
285
385
  totalLines: lines.length,
286
386
  };
287
387
  }
388
+ // Check if this is an image file
389
+ if (this.isImageFile(fullPath)) {
390
+ const imageContent = await this.readImageAsBase64(fullPath);
391
+ if (imageContent) {
392
+ return {
393
+ content: [
394
+ {
395
+ type: 'text',
396
+ text: `🖼️ Image: ${filePath} (${imageContent.mimeType})`,
397
+ },
398
+ imageContent,
399
+ ],
400
+ isImage: true,
401
+ mimeType: imageContent.mimeType,
402
+ };
403
+ }
404
+ }
405
+ // Check if this is an Office document file
406
+ if (this.isOfficeFile(fullPath)) {
407
+ const docContent = await readOfficeDocument(fullPath);
408
+ if (docContent) {
409
+ return {
410
+ content: [
411
+ {
412
+ type: 'text',
413
+ text: `📄 ${docContent.fileType.toUpperCase()} Document: ${filePath}`,
414
+ },
415
+ docContent,
416
+ ],
417
+ isDocument: true,
418
+ fileType: docContent.fileType,
419
+ };
420
+ }
421
+ }
422
+ // Text file processing
288
423
  const content = await fs.readFile(fullPath, 'utf-8');
289
424
  // Parse lines
290
425
  const lines = content.split('\n');
@@ -302,10 +437,8 @@ export class FilesystemMCPService {
302
437
  if (actualEndLine < actualStartLine) {
303
438
  throw new Error('End line must be greater than or equal to start line');
304
439
  }
305
- if (actualStartLine > totalLines) {
306
- throw new Error(`Start line ${actualStartLine} exceeds file length ${totalLines}`);
307
- }
308
- const start = actualStartLine;
440
+ // Auto-adjust if startLine exceeds file length
441
+ const start = Math.min(actualStartLine, totalLines);
309
442
  const end = Math.min(totalLines, actualEndLine);
310
443
  // Extract specified lines (convert to 0-indexed) and add line numbers
311
444
  const selectedLines = lines.slice(start - 1, end);
@@ -340,6 +473,24 @@ export class FilesystemMCPService {
340
473
  };
341
474
  }
342
475
  catch (error) {
476
+ // Try to fix common path issues if it's a file not found error
477
+ if (error instanceof Error &&
478
+ error.message.includes('ENOENT') &&
479
+ typeof filePath === 'string') {
480
+ const fixedPath = await tryFixPath(filePath, this.basePath);
481
+ if (fixedPath && fixedPath !== filePath) {
482
+ // Verify the fixed path actually exists before suggesting
483
+ const fixedFullPath = this.resolvePath(fixedPath);
484
+ try {
485
+ await fs.access(fixedFullPath);
486
+ // File exists, provide helpful suggestion to AI
487
+ throw new Error(`Failed to read file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}\n💡 Tip: File not found. Did you mean "${fixedPath}"? Please use the correct path.`);
488
+ }
489
+ catch {
490
+ // Fixed path also doesn't work, just throw original error
491
+ }
492
+ }
493
+ }
343
494
  throw new Error(`Failed to read file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
344
495
  }
345
496
  }
@@ -855,25 +1006,23 @@ export class FilesystemMCPService {
855
1006
  if (startLine > endLine) {
856
1007
  throw new Error('Start line must be less than or equal to end line');
857
1008
  }
858
- if (startLine > totalLines) {
859
- throw new Error(`Start line ${startLine} exceeds file length ${totalLines}`);
860
- }
861
- // Adjust endLine if it exceeds file length
1009
+ // Adjust startLine and endLine if they exceed file length
1010
+ const adjustedStartLine = Math.min(startLine, totalLines);
862
1011
  const adjustedEndLine = Math.min(endLine, totalLines);
863
- const linesToModify = adjustedEndLine - startLine + 1;
1012
+ const linesToModify = adjustedEndLine - adjustedStartLine + 1;
864
1013
  // Backup file before editing
865
1014
  await incrementalSnapshotManager.backupFile(fullPath);
866
1015
  // Extract the lines that will be replaced (for comparison)
867
1016
  // Compress whitespace for display readability
868
- const replacedLines = lines.slice(startLine - 1, adjustedEndLine);
1017
+ const replacedLines = lines.slice(adjustedStartLine - 1, adjustedEndLine);
869
1018
  const replacedContent = replacedLines
870
1019
  .map((line, idx) => {
871
- const lineNum = startLine + idx;
1020
+ const lineNum = adjustedStartLine + idx;
872
1021
  return `${lineNum}→${normalizeForDisplay(line)}`;
873
1022
  })
874
1023
  .join('\n');
875
1024
  // Calculate context range using smart boundary detection
876
- const smartBoundaries = findSmartContextBoundaries(lines, startLine, adjustedEndLine, contextLines);
1025
+ const smartBoundaries = findSmartContextBoundaries(lines, adjustedStartLine, adjustedEndLine, contextLines);
877
1026
  const contextStart = smartBoundaries.start;
878
1027
  const contextEnd = smartBoundaries.end;
879
1028
  // Extract old content for context (compress whitespace for readability)
@@ -886,12 +1035,12 @@ export class FilesystemMCPService {
886
1035
  .join('\n');
887
1036
  // Replace the specified lines
888
1037
  const newContentLines = newContent.split('\n');
889
- const beforeLines = lines.slice(0, startLine - 1);
1038
+ const beforeLines = lines.slice(0, adjustedStartLine - 1);
890
1039
  const afterLines = lines.slice(adjustedEndLine);
891
1040
  const modifiedLines = [...beforeLines, ...newContentLines, ...afterLines];
892
1041
  // Calculate new context range
893
1042
  const newTotalLines = modifiedLines.length;
894
- const lineDifference = newContentLines.length - (adjustedEndLine - startLine + 1);
1043
+ const lineDifference = newContentLines.length - (adjustedEndLine - adjustedStartLine + 1);
895
1044
  const newContextEnd = Math.min(newTotalLines, contextEnd + lineDifference);
896
1045
  // Extract new content for context with line numbers (compress whitespace)
897
1046
  const newContextLines = modifiedLines.slice(contextStart - 1, newContextEnd);
@@ -941,7 +1090,7 @@ export class FilesystemMCPService {
941
1090
  }
942
1091
  }
943
1092
  // Analyze code structure of the edited content (using formatted content if available)
944
- const editedContentLines = finalLines.slice(startLine - 1, startLine - 1 + newContentLines.length);
1093
+ const editedContentLines = finalLines.slice(adjustedStartLine - 1, adjustedStartLine - 1 + newContentLines.length);
945
1094
  const structureAnalysis = analyzeCodeStructure(finalLines.join('\n'), filePath, editedContentLines);
946
1095
  // Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
947
1096
  let diagnostics = [];
@@ -958,7 +1107,7 @@ export class FilesystemMCPService {
958
1107
  }
959
1108
  const result = {
960
1109
  message: `✅ File edited successfully,Please check the edit results and pay attention to code boundary issues to avoid syntax errors caused by missing closed parts: ${filePath}\n` +
961
- ` Replaced: lines ${startLine}-${adjustedEndLine} (${linesToModify} lines)\n` +
1110
+ ` Replaced: lines ${adjustedStartLine}-${adjustedEndLine} (${linesToModify} lines)\n` +
962
1111
  ` Result: ${newContentLines.length} new lines` +
963
1112
  (smartBoundaries.extended
964
1113
  ? `\n 📍 Context auto-extended to show complete code block (lines ${contextStart}-${finalContextEnd})`
@@ -1077,7 +1226,7 @@ export const filesystemService = new FilesystemMCPService();
1077
1226
  export const mcpTools = [
1078
1227
  {
1079
1228
  name: 'filesystem-read',
1080
- description: 'Read file content with line numbers. **Read only when the actual file or folder path is found or provided by the user, do not make random guesses,Search for specific documents or line numbers before reading more accurately** **SUPPORTS MULTIPLE FILES WITH FLEXIBLE LINE RANGES**: Pass either (1) a single file path (string), (2) array of file paths (strings) with unified startLine/endLine, or (3) array of file config objects with per-file line ranges. **INTEGRATED DIRECTORY LISTING**: When filePath is a directory, automatically lists its contents instead of throwing error. ⚠️ **IMPORTANT WORKFLOW**: (1) ALWAYS use ACE search tools FIRST (ace-text_search/ace-search_symbols/ace-file_outline) to locate the relevant code, (2) ONLY use filesystem-read when you know the approximate location and need precise line numbers for editing. **ANTI-PATTERN**: Reading files line-by-line from the top wastes tokens - use search instead! **USAGE**: Call without parameters to read entire file(s), or specify startLine/endLine for partial reads. Returns content with line numbers (format: "123→code") for precise editing. **EXAMPLES**: (A) Unified: filePath=["a.ts", "b.ts"], startLine=1, endLine=500 reads lines 1-500 from both. (B) Per-file: filePath=[{path:"a.ts", startLine:1, endLine:300}, {path:"b.ts", startLine:100, endLine:550}] reads different ranges from each file. (C) Directory: filePath="./src" returns list of files in src/.',
1229
+ description: 'Read file content with line numbers and multimodal support (text + images + Office documents). **MULTIMODAL SUPPORT**: Automatically detects and processes: (1) Image files (.png, .jpg, .jpeg, .gif, .webp, .bmp, .svg) - returns base64-encoded image data, (2) Office documents (.pdf, .docx, .doc, .xlsx, .xls, .pptx, .ppt) - extracts and returns readable text content. All returned in MCP content format for AI analysis. **Read only when the actual file or folder path is found or provided by the user, do not make random guesses,Search for specific documents or line numbers before reading more accurately** **SUPPORTS MULTIPLE FILES WITH FLEXIBLE LINE RANGES**: Pass either (1) a single file path (string), (2) array of file paths (strings) with unified startLine/endLine, or (3) array of file config objects with per-file line ranges. **INTEGRATED DIRECTORY LISTING**: When filePath is a directory, automatically lists its contents instead of throwing error. ⚠️ **IMPORTANT WORKFLOW**: (1) ALWAYS use ACE search tools FIRST (ace-text_search/ace-search_symbols/ace-file_outline) to locate the relevant code, (2) ONLY use filesystem-read when you know the approximate location and need precise line numbers for editing. **ANTI-PATTERN**: Reading files line-by-line from the top wastes tokens - use search instead! **USAGE**: Call without parameters to read entire file(s), or specify startLine/endLine for partial reads. Returns content with line numbers (format: "123→code") for text files or multimodal content array for images/documents. **EXAMPLES**: (A) Unified: filePath=["a.ts", "b.ts"], startLine=1, endLine=500 reads lines 1-500 from both. (B) Per-file: filePath=[{path:"a.ts", startLine:1, endLine:300}, {path:"b.ts", startLine:100, endLine:550}] reads different ranges from each file. (C) Directory: filePath="./src" returns list of files in src/. (D) Image: filePath="screenshot.png" returns multimodal content with base64 image data. (E) Office: filePath="report.pdf" or "data.xlsx" extracts and returns document text.',
1081
1230
  inputSchema: {
1082
1231
  type: 'object',
1083
1232
  properties: {
@@ -2,6 +2,51 @@
2
2
  * Type definitions for Filesystem MCP Service
3
3
  */
4
4
  import type { Diagnostic } from '../../utils/vscodeConnection.js';
5
+ /**
6
+ * MCP Content Types - supports multimodal content
7
+ */
8
+ export type MCPContentType = 'text' | 'image' | 'document';
9
+ /**
10
+ * Text content block
11
+ */
12
+ export interface TextContent {
13
+ type: 'text';
14
+ text: string;
15
+ }
16
+ /**
17
+ * Image content block (base64 encoded)
18
+ */
19
+ export interface ImageContent {
20
+ type: 'image';
21
+ data: string;
22
+ mimeType: string;
23
+ }
24
+ /**
25
+ * Document content block (for Office files like PDF, Word, Excel, PPT)
26
+ */
27
+ export interface DocumentContent {
28
+ type: 'document';
29
+ text: string;
30
+ fileType: 'pdf' | 'word' | 'excel' | 'powerpoint';
31
+ metadata?: {
32
+ pages?: number;
33
+ sheets?: string[];
34
+ slides?: number;
35
+ [key: string]: unknown;
36
+ };
37
+ }
38
+ /**
39
+ * Multimodal content - array of text, image, and document blocks
40
+ */
41
+ export type MultimodalContent = Array<TextContent | ImageContent | DocumentContent>;
42
+ /**
43
+ * Supported image MIME types
44
+ */
45
+ export declare const IMAGE_MIME_TYPES: Record<string, string>;
46
+ /**
47
+ * Supported Office document types
48
+ */
49
+ export declare const OFFICE_FILE_TYPES: Record<string, 'pdf' | 'word' | 'excel' | 'powerpoint'>;
5
50
  /**
6
51
  * Structure analysis result for code validation
7
52
  */
@@ -51,6 +96,36 @@ export interface FileReadConfig {
51
96
  startLine?: number;
52
97
  endLine?: number;
53
98
  }
99
+ /**
100
+ * Single file read result
101
+ */
102
+ export interface SingleFileReadResult {
103
+ content: string | MultimodalContent;
104
+ startLine?: number;
105
+ endLine?: number;
106
+ totalLines?: number;
107
+ isImage?: boolean;
108
+ isDocument?: boolean;
109
+ fileType?: 'pdf' | 'word' | 'excel' | 'powerpoint';
110
+ mimeType?: string;
111
+ }
112
+ /**
113
+ * Multiple files read result
114
+ */
115
+ export interface MultipleFilesReadResult {
116
+ content: string | MultimodalContent;
117
+ files: Array<{
118
+ path: string;
119
+ startLine?: number;
120
+ endLine?: number;
121
+ totalLines?: number;
122
+ isImage?: boolean;
123
+ isDocument?: boolean;
124
+ fileType?: 'pdf' | 'word' | 'excel' | 'powerpoint';
125
+ mimeType?: string;
126
+ }>;
127
+ totalFiles: number;
128
+ }
54
129
  /**
55
130
  * Edit by search configuration
56
131
  */