snow-ai 0.4.15 → 0.4.17

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.
Files changed (351) hide show
  1. package/bundle/cli.mjs +477445 -0
  2. package/bundle/sql-wasm.wasm +0 -0
  3. package/package.json +31 -26
  4. package/dist/agents/codebaseIndexAgent.d.ts +0 -102
  5. package/dist/agents/codebaseIndexAgent.js +0 -641
  6. package/dist/agents/codebaseReviewAgent.d.ts +0 -61
  7. package/dist/agents/codebaseReviewAgent.js +0 -301
  8. package/dist/agents/compactAgent.d.ts +0 -55
  9. package/dist/agents/compactAgent.js +0 -306
  10. package/dist/agents/promptOptimizeAgent.d.ts +0 -54
  11. package/dist/agents/promptOptimizeAgent.js +0 -268
  12. package/dist/agents/reviewAgent.d.ts +0 -50
  13. package/dist/agents/reviewAgent.js +0 -265
  14. package/dist/agents/summaryAgent.d.ts +0 -57
  15. package/dist/agents/summaryAgent.js +0 -260
  16. package/dist/api/anthropic.d.ts +0 -44
  17. package/dist/api/anthropic.js +0 -598
  18. package/dist/api/chat.d.ts +0 -73
  19. package/dist/api/chat.js +0 -386
  20. package/dist/api/embedding.d.ts +0 -34
  21. package/dist/api/embedding.js +0 -80
  22. package/dist/api/gemini.d.ts +0 -31
  23. package/dist/api/gemini.js +0 -445
  24. package/dist/api/models.d.ts +0 -15
  25. package/dist/api/models.js +0 -139
  26. package/dist/api/responses.d.ts +0 -38
  27. package/dist/api/responses.js +0 -515
  28. package/dist/api/systemPrompt.d.ts +0 -4
  29. package/dist/api/systemPrompt.js +0 -408
  30. package/dist/api/types.d.ts +0 -53
  31. package/dist/api/types.js +0 -4
  32. package/dist/app.d.ts +0 -8
  33. package/dist/app.js +0 -112
  34. package/dist/cli.d.ts +0 -2
  35. package/dist/cli.js +0 -199
  36. package/dist/hooks/useAgentPicker.d.ts +0 -14
  37. package/dist/hooks/useAgentPicker.js +0 -119
  38. package/dist/hooks/useClipboard.d.ts +0 -4
  39. package/dist/hooks/useClipboard.js +0 -175
  40. package/dist/hooks/useCommandHandler.d.ts +0 -35
  41. package/dist/hooks/useCommandHandler.js +0 -346
  42. package/dist/hooks/useCommandPanel.d.ts +0 -17
  43. package/dist/hooks/useCommandPanel.js +0 -114
  44. package/dist/hooks/useConversation.d.ts +0 -49
  45. package/dist/hooks/useConversation.js +0 -1052
  46. package/dist/hooks/useFilePicker.d.ts +0 -18
  47. package/dist/hooks/useFilePicker.js +0 -224
  48. package/dist/hooks/useGlobalExit.d.ts +0 -5
  49. package/dist/hooks/useGlobalExit.js +0 -34
  50. package/dist/hooks/useGlobalNavigation.d.ts +0 -6
  51. package/dist/hooks/useGlobalNavigation.js +0 -17
  52. package/dist/hooks/useHistoryNavigation.d.ts +0 -35
  53. package/dist/hooks/useHistoryNavigation.js +0 -133
  54. package/dist/hooks/useInputBuffer.d.ts +0 -6
  55. package/dist/hooks/useInputBuffer.js +0 -45
  56. package/dist/hooks/useKeyboardInput.d.ts +0 -80
  57. package/dist/hooks/useKeyboardInput.js +0 -608
  58. package/dist/hooks/useSessionManagement.d.ts +0 -10
  59. package/dist/hooks/useSessionManagement.js +0 -43
  60. package/dist/hooks/useSessionSave.d.ts +0 -8
  61. package/dist/hooks/useSessionSave.js +0 -63
  62. package/dist/hooks/useSnapshotState.d.ts +0 -26
  63. package/dist/hooks/useSnapshotState.js +0 -28
  64. package/dist/hooks/useStreamingState.d.ts +0 -33
  65. package/dist/hooks/useStreamingState.js +0 -105
  66. package/dist/hooks/useTerminalFocus.d.ts +0 -28
  67. package/dist/hooks/useTerminalFocus.js +0 -87
  68. package/dist/hooks/useTerminalSize.d.ts +0 -4
  69. package/dist/hooks/useTerminalSize.js +0 -20
  70. package/dist/hooks/useTodoPicker.d.ts +0 -16
  71. package/dist/hooks/useTodoPicker.js +0 -94
  72. package/dist/hooks/useToolConfirmation.d.ts +0 -19
  73. package/dist/hooks/useToolConfirmation.js +0 -61
  74. package/dist/hooks/useVSCodeState.d.ts +0 -8
  75. package/dist/hooks/useVSCodeState.js +0 -81
  76. package/dist/i18n/I18nContext.d.ts +0 -14
  77. package/dist/i18n/I18nContext.js +0 -24
  78. package/dist/i18n/index.d.ts +0 -3
  79. package/dist/i18n/index.js +0 -2
  80. package/dist/i18n/lang/en.d.ts +0 -2
  81. package/dist/i18n/lang/en.js +0 -502
  82. package/dist/i18n/lang/es.d.ts +0 -2
  83. package/dist/i18n/lang/es.js +0 -502
  84. package/dist/i18n/lang/ja.d.ts +0 -2
  85. package/dist/i18n/lang/ja.js +0 -502
  86. package/dist/i18n/lang/ko.d.ts +0 -2
  87. package/dist/i18n/lang/ko.js +0 -502
  88. package/dist/i18n/lang/zh-TW.d.ts +0 -2
  89. package/dist/i18n/lang/zh-TW.js +0 -502
  90. package/dist/i18n/lang/zh.d.ts +0 -2
  91. package/dist/i18n/lang/zh.js +0 -502
  92. package/dist/i18n/translations.d.ts +0 -2
  93. package/dist/i18n/translations.js +0 -14
  94. package/dist/i18n/types.d.ts +0 -478
  95. package/dist/i18n/types.js +0 -1
  96. package/dist/mcp/aceCodeSearch.d.ts +0 -247
  97. package/dist/mcp/aceCodeSearch.js +0 -1058
  98. package/dist/mcp/bash.d.ts +0 -50
  99. package/dist/mcp/bash.js +0 -153
  100. package/dist/mcp/codebaseSearch.d.ts +0 -44
  101. package/dist/mcp/codebaseSearch.js +0 -275
  102. package/dist/mcp/filesystem.d.ts +0 -392
  103. package/dist/mcp/filesystem.js +0 -1445
  104. package/dist/mcp/ideDiagnostics.d.ts +0 -36
  105. package/dist/mcp/ideDiagnostics.js +0 -90
  106. package/dist/mcp/notebook.d.ts +0 -10
  107. package/dist/mcp/notebook.js +0 -367
  108. package/dist/mcp/subagent.d.ts +0 -37
  109. package/dist/mcp/subagent.js +0 -113
  110. package/dist/mcp/todo.d.ts +0 -46
  111. package/dist/mcp/todo.js +0 -511
  112. package/dist/mcp/types/aceCodeSearch.types.d.ts +0 -92
  113. package/dist/mcp/types/aceCodeSearch.types.js +0 -4
  114. package/dist/mcp/types/bash.types.d.ts +0 -13
  115. package/dist/mcp/types/bash.types.js +0 -4
  116. package/dist/mcp/types/filesystem.types.d.ts +0 -210
  117. package/dist/mcp/types/filesystem.types.js +0 -27
  118. package/dist/mcp/types/todo.types.d.ts +0 -27
  119. package/dist/mcp/types/todo.types.js +0 -4
  120. package/dist/mcp/types/websearch.types.d.ts +0 -30
  121. package/dist/mcp/types/websearch.types.js +0 -4
  122. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +0 -34
  123. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +0 -146
  124. package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +0 -14
  125. package/dist/mcp/utils/aceCodeSearch/language.utils.js +0 -418
  126. package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +0 -31
  127. package/dist/mcp/utils/aceCodeSearch/search.utils.js +0 -136
  128. package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +0 -20
  129. package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +0 -141
  130. package/dist/mcp/utils/bash/security.utils.d.ts +0 -20
  131. package/dist/mcp/utils/bash/security.utils.js +0 -34
  132. package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +0 -39
  133. package/dist/mcp/utils/filesystem/batch-operations.utils.js +0 -182
  134. package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +0 -18
  135. package/dist/mcp/utils/filesystem/code-analysis.utils.js +0 -165
  136. package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +0 -16
  137. package/dist/mcp/utils/filesystem/match-finder.utils.js +0 -85
  138. package/dist/mcp/utils/filesystem/office-parser.utils.d.ts +0 -43
  139. package/dist/mcp/utils/filesystem/office-parser.utils.js +0 -163
  140. package/dist/mcp/utils/filesystem/path-fixer.utils.d.ts +0 -7
  141. package/dist/mcp/utils/filesystem/path-fixer.utils.js +0 -60
  142. package/dist/mcp/utils/filesystem/similarity.utils.d.ts +0 -22
  143. package/dist/mcp/utils/filesystem/similarity.utils.js +0 -75
  144. package/dist/mcp/utils/todo/date.utils.d.ts +0 -9
  145. package/dist/mcp/utils/todo/date.utils.js +0 -14
  146. package/dist/mcp/utils/websearch/browser.utils.d.ts +0 -8
  147. package/dist/mcp/utils/websearch/browser.utils.js +0 -58
  148. package/dist/mcp/utils/websearch/text.utils.d.ts +0 -16
  149. package/dist/mcp/utils/websearch/text.utils.js +0 -39
  150. package/dist/mcp/websearch.d.ts +0 -88
  151. package/dist/mcp/websearch.js +0 -375
  152. package/dist/test/logger-test.d.ts +0 -1
  153. package/dist/test/logger-test.js +0 -7
  154. package/dist/types/index.d.ts +0 -15
  155. package/dist/types/index.js +0 -1
  156. package/dist/ui/components/AgentPickerPanel.d.ts +0 -10
  157. package/dist/ui/components/AgentPickerPanel.js +0 -74
  158. package/dist/ui/components/ChatInput.d.ts +0 -46
  159. package/dist/ui/components/ChatInput.js +0 -379
  160. package/dist/ui/components/CommandPanel.d.ts +0 -15
  161. package/dist/ui/components/CommandPanel.js +0 -80
  162. package/dist/ui/components/DiffViewer.d.ts +0 -11
  163. package/dist/ui/components/DiffViewer.js +0 -178
  164. package/dist/ui/components/FileList.d.ts +0 -15
  165. package/dist/ui/components/FileList.js +0 -360
  166. package/dist/ui/components/FileRollbackConfirmation.d.ts +0 -8
  167. package/dist/ui/components/FileRollbackConfirmation.js +0 -108
  168. package/dist/ui/components/HelpPanel.d.ts +0 -2
  169. package/dist/ui/components/HelpPanel.js +0 -67
  170. package/dist/ui/components/MCPInfoPanel.d.ts +0 -2
  171. package/dist/ui/components/MCPInfoPanel.js +0 -108
  172. package/dist/ui/components/MCPInfoScreen.d.ts +0 -7
  173. package/dist/ui/components/MCPInfoScreen.js +0 -115
  174. package/dist/ui/components/MarkdownRenderer.d.ts +0 -6
  175. package/dist/ui/components/MarkdownRenderer.js +0 -70
  176. package/dist/ui/components/Menu.d.ts +0 -17
  177. package/dist/ui/components/Menu.js +0 -88
  178. package/dist/ui/components/MessageList.d.ts +0 -56
  179. package/dist/ui/components/MessageList.js +0 -97
  180. package/dist/ui/components/PendingMessages.d.ts +0 -13
  181. package/dist/ui/components/PendingMessages.js +0 -29
  182. package/dist/ui/components/PendingToolCalls.d.ts +0 -11
  183. package/dist/ui/components/PendingToolCalls.js +0 -35
  184. package/dist/ui/components/ScrollableSelectInput.d.ts +0 -29
  185. package/dist/ui/components/ScrollableSelectInput.js +0 -157
  186. package/dist/ui/components/SessionListPanel.d.ts +0 -7
  187. package/dist/ui/components/SessionListPanel.js +0 -175
  188. package/dist/ui/components/SessionListScreen.d.ts +0 -7
  189. package/dist/ui/components/SessionListScreen.js +0 -217
  190. package/dist/ui/components/SessionListScreenWrapper.d.ts +0 -7
  191. package/dist/ui/components/SessionListScreenWrapper.js +0 -14
  192. package/dist/ui/components/ShimmerText.d.ts +0 -9
  193. package/dist/ui/components/ShimmerText.js +0 -30
  194. package/dist/ui/components/TodoPickerPanel.d.ts +0 -14
  195. package/dist/ui/components/TodoPickerPanel.js +0 -119
  196. package/dist/ui/components/TodoTree.d.ts +0 -15
  197. package/dist/ui/components/TodoTree.js +0 -60
  198. package/dist/ui/components/ToolConfirmation.d.ts +0 -21
  199. package/dist/ui/components/ToolConfirmation.js +0 -204
  200. package/dist/ui/components/ToolResultPreview.d.ts +0 -13
  201. package/dist/ui/components/ToolResultPreview.js +0 -337
  202. package/dist/ui/components/UsagePanel.d.ts +0 -2
  203. package/dist/ui/components/UsagePanel.js +0 -394
  204. package/dist/ui/contexts/ThemeContext.d.ts +0 -13
  205. package/dist/ui/contexts/ThemeContext.js +0 -28
  206. package/dist/ui/pages/ChatScreen.d.ts +0 -6
  207. package/dist/ui/pages/ChatScreen.js +0 -1495
  208. package/dist/ui/pages/CodeBaseConfigScreen.d.ts +0 -8
  209. package/dist/ui/pages/CodeBaseConfigScreen.js +0 -350
  210. package/dist/ui/pages/ConfigScreen.d.ts +0 -8
  211. package/dist/ui/pages/ConfigScreen.js +0 -1101
  212. package/dist/ui/pages/CustomHeadersScreen.d.ts +0 -6
  213. package/dist/ui/pages/CustomHeadersScreen.js +0 -502
  214. package/dist/ui/pages/HeadlessModeScreen.d.ts +0 -7
  215. package/dist/ui/pages/HeadlessModeScreen.js +0 -381
  216. package/dist/ui/pages/LanguageSettingsScreen.d.ts +0 -7
  217. package/dist/ui/pages/LanguageSettingsScreen.js +0 -91
  218. package/dist/ui/pages/MCPConfigScreen.d.ts +0 -6
  219. package/dist/ui/pages/MCPConfigScreen.js +0 -55
  220. package/dist/ui/pages/ProxyConfigScreen.d.ts +0 -8
  221. package/dist/ui/pages/ProxyConfigScreen.js +0 -149
  222. package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +0 -7
  223. package/dist/ui/pages/SensitiveCommandConfigScreen.js +0 -271
  224. package/dist/ui/pages/SubAgentConfigScreen.d.ts +0 -9
  225. package/dist/ui/pages/SubAgentConfigScreen.js +0 -435
  226. package/dist/ui/pages/SubAgentListScreen.d.ts +0 -9
  227. package/dist/ui/pages/SubAgentListScreen.js +0 -131
  228. package/dist/ui/pages/SystemPromptConfigScreen.d.ts +0 -6
  229. package/dist/ui/pages/SystemPromptConfigScreen.js +0 -326
  230. package/dist/ui/pages/ThemeSettingsScreen.d.ts +0 -7
  231. package/dist/ui/pages/ThemeSettingsScreen.js +0 -106
  232. package/dist/ui/pages/WelcomeScreen.d.ts +0 -7
  233. package/dist/ui/pages/WelcomeScreen.js +0 -217
  234. package/dist/ui/themes/index.d.ts +0 -23
  235. package/dist/ui/themes/index.js +0 -140
  236. package/dist/utils/apiConfig.d.ts +0 -126
  237. package/dist/utils/apiConfig.js +0 -423
  238. package/dist/utils/autoCompress.d.ts +0 -15
  239. package/dist/utils/autoCompress.js +0 -24
  240. package/dist/utils/chatExporter.d.ts +0 -9
  241. package/dist/utils/chatExporter.js +0 -118
  242. package/dist/utils/checkpointManager.d.ts +0 -74
  243. package/dist/utils/checkpointManager.js +0 -181
  244. package/dist/utils/codebaseConfig.d.ts +0 -16
  245. package/dist/utils/codebaseConfig.js +0 -67
  246. package/dist/utils/codebaseDatabase.d.ts +0 -102
  247. package/dist/utils/codebaseDatabase.js +0 -333
  248. package/dist/utils/codebaseSearchEvents.d.ts +0 -16
  249. package/dist/utils/codebaseSearchEvents.js +0 -13
  250. package/dist/utils/commandExecutor.d.ts +0 -13
  251. package/dist/utils/commandExecutor.js +0 -26
  252. package/dist/utils/commands/agent.d.ts +0 -2
  253. package/dist/utils/commands/agent.js +0 -12
  254. package/dist/utils/commands/clear.d.ts +0 -2
  255. package/dist/utils/commands/clear.js +0 -12
  256. package/dist/utils/commands/compact.d.ts +0 -2
  257. package/dist/utils/commands/compact.js +0 -12
  258. package/dist/utils/commands/export.d.ts +0 -2
  259. package/dist/utils/commands/export.js +0 -12
  260. package/dist/utils/commands/help.d.ts +0 -2
  261. package/dist/utils/commands/help.js +0 -11
  262. package/dist/utils/commands/home.d.ts +0 -2
  263. package/dist/utils/commands/home.js +0 -34
  264. package/dist/utils/commands/ide.d.ts +0 -2
  265. package/dist/utils/commands/ide.js +0 -32
  266. package/dist/utils/commands/init.d.ts +0 -2
  267. package/dist/utils/commands/init.js +0 -93
  268. package/dist/utils/commands/mcp.d.ts +0 -2
  269. package/dist/utils/commands/mcp.js +0 -12
  270. package/dist/utils/commands/resume.d.ts +0 -2
  271. package/dist/utils/commands/resume.js +0 -12
  272. package/dist/utils/commands/review.d.ts +0 -2
  273. package/dist/utils/commands/review.js +0 -81
  274. package/dist/utils/commands/role.d.ts +0 -2
  275. package/dist/utils/commands/role.js +0 -37
  276. package/dist/utils/commands/todoPicker.d.ts +0 -2
  277. package/dist/utils/commands/todoPicker.js +0 -12
  278. package/dist/utils/commands/usage.d.ts +0 -2
  279. package/dist/utils/commands/usage.js +0 -12
  280. package/dist/utils/commands/yolo.d.ts +0 -2
  281. package/dist/utils/commands/yolo.js +0 -12
  282. package/dist/utils/configManager.d.ts +0 -45
  283. package/dist/utils/configManager.js +0 -303
  284. package/dist/utils/contextCompressor.d.ts +0 -16
  285. package/dist/utils/contextCompressor.js +0 -334
  286. package/dist/utils/devMode.d.ts +0 -13
  287. package/dist/utils/devMode.js +0 -54
  288. package/dist/utils/escapeHandler.d.ts +0 -79
  289. package/dist/utils/escapeHandler.js +0 -153
  290. package/dist/utils/fileDialog.d.ts +0 -9
  291. package/dist/utils/fileDialog.js +0 -74
  292. package/dist/utils/fileUtils.d.ts +0 -40
  293. package/dist/utils/fileUtils.js +0 -185
  294. package/dist/utils/historyManager.d.ts +0 -45
  295. package/dist/utils/historyManager.js +0 -159
  296. package/dist/utils/incrementalSnapshot.d.ts +0 -109
  297. package/dist/utils/incrementalSnapshot.js +0 -383
  298. package/dist/utils/index.d.ts +0 -11
  299. package/dist/utils/index.js +0 -18
  300. package/dist/utils/languageConfig.d.ts +0 -21
  301. package/dist/utils/languageConfig.js +0 -61
  302. package/dist/utils/logger.d.ts +0 -37
  303. package/dist/utils/logger.js +0 -122
  304. package/dist/utils/mcpToolsManager.d.ts +0 -52
  305. package/dist/utils/mcpToolsManager.js +0 -878
  306. package/dist/utils/messageFormatter.d.ts +0 -12
  307. package/dist/utils/messageFormatter.js +0 -115
  308. package/dist/utils/notebookManager.d.ts +0 -59
  309. package/dist/utils/notebookManager.js +0 -213
  310. package/dist/utils/patch-highlight.d.ts +0 -5
  311. package/dist/utils/patch-highlight.js +0 -23
  312. package/dist/utils/processManager.d.ts +0 -27
  313. package/dist/utils/processManager.js +0 -75
  314. package/dist/utils/proxyUtils.d.ts +0 -15
  315. package/dist/utils/proxyUtils.js +0 -50
  316. package/dist/utils/resourceMonitor.d.ts +0 -65
  317. package/dist/utils/resourceMonitor.js +0 -175
  318. package/dist/utils/retryUtils.d.ts +0 -49
  319. package/dist/utils/retryUtils.js +0 -303
  320. package/dist/utils/sensitiveCommandManager.d.ts +0 -53
  321. package/dist/utils/sensitiveCommandManager.js +0 -308
  322. package/dist/utils/sessionConverter.d.ts +0 -7
  323. package/dist/utils/sessionConverter.js +0 -306
  324. package/dist/utils/sessionManager.d.ts +0 -53
  325. package/dist/utils/sessionManager.js +0 -371
  326. package/dist/utils/subAgentConfig.d.ts +0 -50
  327. package/dist/utils/subAgentConfig.js +0 -221
  328. package/dist/utils/subAgentExecutor.d.ts +0 -40
  329. package/dist/utils/subAgentExecutor.js +0 -434
  330. package/dist/utils/terminal.d.ts +0 -5
  331. package/dist/utils/terminal.js +0 -13
  332. package/dist/utils/textBuffer.d.ts +0 -99
  333. package/dist/utils/textBuffer.js +0 -547
  334. package/dist/utils/textUtils.d.ts +0 -37
  335. package/dist/utils/textUtils.js +0 -102
  336. package/dist/utils/themeConfig.d.ts +0 -21
  337. package/dist/utils/themeConfig.js +0 -61
  338. package/dist/utils/todoPreprocessor.d.ts +0 -5
  339. package/dist/utils/todoPreprocessor.js +0 -18
  340. package/dist/utils/todoScanner.d.ts +0 -8
  341. package/dist/utils/todoScanner.js +0 -148
  342. package/dist/utils/toolDisplayConfig.d.ts +0 -16
  343. package/dist/utils/toolDisplayConfig.js +0 -47
  344. package/dist/utils/toolExecutor.d.ts +0 -37
  345. package/dist/utils/toolExecutor.js +0 -224
  346. package/dist/utils/usageLogger.d.ts +0 -11
  347. package/dist/utils/usageLogger.js +0 -114
  348. package/dist/utils/vscodeConnection.d.ts +0 -76
  349. package/dist/utils/vscodeConnection.js +0 -430
  350. package/dist/utils/workspaceSnapshot.d.ts +0 -63
  351. package/dist/utils/workspaceSnapshot.js +0 -300
@@ -1,1445 +0,0 @@
1
- import { promises as fs } from 'fs';
2
- import * as path from 'path';
3
- import * as prettier from 'prettier';
4
- // IDE connection supports both VSCode and JetBrains IDEs
5
- import { vscodeConnection } from '../utils/vscodeConnection.js';
6
- import { incrementalSnapshotManager } from '../utils/incrementalSnapshot.js';
7
- import { tryUnescapeFix, trimPairIfPossible, isOverEscaped, } from '../utils/escapeHandler.js';
8
- import { IMAGE_MIME_TYPES, OFFICE_FILE_TYPES } from './types/filesystem.types.js';
9
- // Utility functions
10
- import { calculateSimilarity, normalizeForDisplay, } from './utils/filesystem/similarity.utils.js';
11
- import { analyzeCodeStructure, findSmartContextBoundaries, } from './utils/filesystem/code-analysis.utils.js';
12
- import { findClosestMatches, generateDiffMessage, } from './utils/filesystem/match-finder.utils.js';
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';
16
- // ACE Code Search utilities for symbol parsing
17
- import { parseFileSymbols } from './utils/aceCodeSearch/symbol.utils.js';
18
- // Notebook utilities for automatic note retrieval
19
- import { queryNotebook } from '../utils/notebookManager.js';
20
- const { resolve, dirname, isAbsolute, extname } = path;
21
- /**
22
- * Filesystem MCP Service
23
- * Provides basic file operations: read, create, and delete files
24
- */
25
- export class FilesystemMCPService {
26
- constructor(basePath = process.cwd()) {
27
- Object.defineProperty(this, "basePath", {
28
- enumerable: true,
29
- configurable: true,
30
- writable: true,
31
- value: void 0
32
- });
33
- /**
34
- * File extensions supported by Prettier for automatic formatting
35
- */
36
- Object.defineProperty(this, "prettierSupportedExtensions", {
37
- enumerable: true,
38
- configurable: true,
39
- writable: true,
40
- value: [
41
- '.js',
42
- '.jsx',
43
- '.ts',
44
- '.tsx',
45
- '.json',
46
- '.css',
47
- '.scss',
48
- '.less',
49
- '.html',
50
- '.vue',
51
- '.yaml',
52
- '.yml',
53
- '.md',
54
- '.graphql',
55
- '.gql',
56
- ]
57
- });
58
- this.basePath = resolve(basePath);
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
- }
111
- /**
112
- * Extract relevant symbol information for a specific line range
113
- * This provides context that helps AI make more accurate modifications
114
- * @param symbols - All symbols in the file
115
- * @param startLine - Start line of the range
116
- * @param endLine - End line of the range
117
- * @param _totalLines - Total lines in the file (reserved for future use)
118
- * @returns Formatted string with relevant symbol information
119
- */
120
- extractRelevantSymbols(symbols, startLine, endLine, _totalLines) {
121
- if (symbols.length === 0) {
122
- return '';
123
- }
124
- // Categorize symbols
125
- const imports = symbols.filter(s => s.type === 'import');
126
- const exports = symbols.filter(s => s.type === 'export');
127
- // Symbols within the requested range
128
- const symbolsInRange = symbols.filter(s => s.line >= startLine && s.line <= endLine);
129
- // Symbols defined before the range that might be referenced
130
- const symbolsBeforeRange = symbols.filter(s => s.line < startLine);
131
- // Build context information
132
- const parts = [];
133
- // Always include imports (crucial for understanding dependencies)
134
- if (imports.length > 0) {
135
- const importList = imports
136
- .slice(0, 10) // Limit to avoid excessive tokens
137
- .map(s => ` • ${s.name} (line ${s.line})`)
138
- .join('\n');
139
- parts.push(`📦 Imports:\n${importList}`);
140
- }
141
- // Symbols defined in the current range
142
- if (symbolsInRange.length > 0) {
143
- const rangeSymbols = symbolsInRange
144
- .slice(0, 15)
145
- .map(s => ` • ${s.type}: ${s.name} (line ${s.line})${s.signature ? ` - ${s.signature.slice(0, 60)}` : ''}`)
146
- .join('\n');
147
- parts.push(`🎯 Symbols in this range:\n${rangeSymbols}`);
148
- }
149
- // Key definitions before this range (that might be referenced)
150
- if (symbolsBeforeRange.length > 0 && startLine > 1) {
151
- const relevantBefore = symbolsBeforeRange
152
- .filter(s => s.type === 'function' || s.type === 'class')
153
- .slice(-5) // Last 5 before the range
154
- .map(s => ` • ${s.type}: ${s.name} (line ${s.line})`)
155
- .join('\n');
156
- if (relevantBefore) {
157
- parts.push(`⬆️ Key definitions above:\n${relevantBefore}`);
158
- }
159
- }
160
- // Exports (important for understanding module interface)
161
- if (exports.length > 0) {
162
- const exportList = exports
163
- .slice(0, 10)
164
- .map(s => ` • ${s.name} (line ${s.line})`)
165
- .join('\n');
166
- parts.push(`📤 Exports:\n${exportList}`);
167
- }
168
- if (parts.length === 0) {
169
- return '';
170
- }
171
- return ('\n\n' +
172
- '='.repeat(60) +
173
- '\n📚 SYMBOL INDEX & DEFINITIONS:\n' +
174
- '='.repeat(60) +
175
- '\n' +
176
- parts.join('\n\n'));
177
- }
178
- /**
179
- * Get notebook entries for a file
180
- * @param filePath - Path to the file
181
- * @returns Formatted notebook entries string, or empty if none found
182
- */
183
- getNotebookEntries(filePath) {
184
- try {
185
- const entries = queryNotebook(filePath, 10);
186
- if (entries.length === 0) {
187
- return '';
188
- }
189
- const notesText = entries
190
- .map((entry, index) => {
191
- // createdAt 已经是本地时间格式: "YYYY-MM-DDTHH:mm:ss.SSS"
192
- // 提取日期和时间部分: "YYYY-MM-DD HH:mm"
193
- const dateStr = entry.createdAt.substring(0, 16).replace('T', ' ');
194
- return ` ${index + 1}. [${dateStr}] ${entry.note}`;
195
- })
196
- .join('\n');
197
- return ('\n\n' +
198
- '='.repeat(60) +
199
- '\n📝 CODE NOTEBOOKS (Latest 10):\n' +
200
- '='.repeat(60) +
201
- '\n' +
202
- notesText);
203
- }
204
- catch {
205
- // Silently fail notebook retrieval - don't block file reading
206
- return '';
207
- }
208
- }
209
- /**
210
- * Get the content of a file with optional line range
211
- * Enhanced with symbol information for better AI context
212
- * Supports multimodal content (text + images)
213
- * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
214
- * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
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
216
- * @returns Object containing the requested content with line numbers and metadata (supports multimodal content)
217
- * @throws Error if file doesn't exist or cannot be read
218
- */
219
- async getFileContent(filePath, startLine, endLine) {
220
- try {
221
- // Handle array of files
222
- if (Array.isArray(filePath)) {
223
- const filesData = [];
224
- const multimodalContent = [];
225
- for (const fileItem of filePath) {
226
- try {
227
- // Support both string format and object format
228
- let file;
229
- let fileStartLine;
230
- let fileEndLine;
231
- if (typeof fileItem === 'string') {
232
- // String format: use global startLine/endLine
233
- file = fileItem;
234
- fileStartLine = startLine;
235
- fileEndLine = endLine;
236
- }
237
- else {
238
- // Object format: use per-file startLine/endLine
239
- file = fileItem.path;
240
- fileStartLine = fileItem.startLine ?? startLine;
241
- fileEndLine = fileItem.endLine ?? endLine;
242
- }
243
- const fullPath = this.resolvePath(file);
244
- // For absolute paths, skip validation to allow access outside base path
245
- if (!isAbsolute(file)) {
246
- await this.validatePath(fullPath);
247
- }
248
- // Check if the path is a directory, if so, list its contents instead
249
- const stats = await fs.stat(fullPath);
250
- if (stats.isDirectory()) {
251
- const dirFiles = await this.listFiles(file);
252
- const fileList = dirFiles.join('\n');
253
- multimodalContent.push({
254
- type: 'text',
255
- text: `📁 Directory: ${file}\n${fileList}`,
256
- });
257
- filesData.push({
258
- path: file,
259
- startLine: 1,
260
- endLine: dirFiles.length,
261
- totalLines: dirFiles.length,
262
- });
263
- continue;
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
- }
303
- const content = await fs.readFile(fullPath, 'utf-8');
304
- const lines = content.split('\n');
305
- const totalLines = lines.length;
306
- // Default values and logic (use file-specific values)
307
- const actualStartLine = fileStartLine ?? 1;
308
- const actualEndLine = fileEndLine ?? totalLines;
309
- // Validate and adjust line numbers
310
- if (actualStartLine < 1) {
311
- throw new Error(`Start line must be greater than 0 for ${file}`);
312
- }
313
- if (actualEndLine < actualStartLine) {
314
- throw new Error(`End line must be greater than or equal to start line for ${file}`);
315
- }
316
- // Auto-adjust if startLine exceeds file length
317
- const start = Math.min(actualStartLine, totalLines);
318
- const end = Math.min(totalLines, actualEndLine);
319
- // Extract specified lines
320
- const selectedLines = lines.slice(start - 1, end);
321
- const numberedLines = selectedLines.map((line, index) => {
322
- const lineNum = start + index;
323
- return `${lineNum}→${line}`;
324
- });
325
- let fileContent = `📄 ${file} (lines ${start}-${end}/${totalLines})\n${numberedLines.join('\n')}`;
326
- // Parse and append symbol information
327
- try {
328
- const symbols = await parseFileSymbols(fullPath, content, this.basePath);
329
- const symbolInfo = this.extractRelevantSymbols(symbols, start, end, totalLines);
330
- if (symbolInfo) {
331
- fileContent += symbolInfo;
332
- }
333
- }
334
- catch {
335
- // Silently fail symbol parsing
336
- }
337
- // Append notebook entries
338
- const notebookInfo = this.getNotebookEntries(file);
339
- if (notebookInfo) {
340
- fileContent += notebookInfo;
341
- }
342
- multimodalContent.push({
343
- type: 'text',
344
- text: fileContent,
345
- });
346
- filesData.push({
347
- path: file,
348
- startLine: start,
349
- endLine: end,
350
- totalLines,
351
- });
352
- }
353
- catch (error) {
354
- const errorMsg = error instanceof Error ? error.message : 'Unknown error';
355
- // Extract file path for error message
356
- const filePath = typeof fileItem === 'string' ? fileItem : fileItem.path;
357
- multimodalContent.push({
358
- type: 'text',
359
- text: `❌ ${filePath}: ${errorMsg}`,
360
- });
361
- }
362
- }
363
- return {
364
- content: multimodalContent,
365
- files: filesData,
366
- totalFiles: filePath.length,
367
- };
368
- }
369
- // Original single file logic
370
- const fullPath = this.resolvePath(filePath);
371
- // For absolute paths, skip validation to allow access outside base path
372
- if (!isAbsolute(filePath)) {
373
- await this.validatePath(fullPath);
374
- }
375
- // Check if the path is a directory, if so, list its contents instead
376
- const stats = await fs.stat(fullPath);
377
- if (stats.isDirectory()) {
378
- const files = await this.listFiles(filePath);
379
- const fileList = files.join('\n');
380
- const lines = fileList.split('\n');
381
- return {
382
- content: `Directory: ${filePath}\n\n${fileList}`,
383
- startLine: 1,
384
- endLine: lines.length,
385
- totalLines: lines.length,
386
- };
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
423
- const content = await fs.readFile(fullPath, 'utf-8');
424
- // Parse lines
425
- const lines = content.split('\n');
426
- const totalLines = lines.length;
427
- // Default values and logic:
428
- // - No params: read entire file (1 to totalLines)
429
- // - Only startLine: read from startLine to end of file
430
- // - Both params: read from startLine to endLine
431
- const actualStartLine = startLine ?? 1;
432
- const actualEndLine = endLine ?? totalLines;
433
- // Validate and adjust line numbers
434
- if (actualStartLine < 1) {
435
- throw new Error('Start line must be greater than 0');
436
- }
437
- if (actualEndLine < actualStartLine) {
438
- throw new Error('End line must be greater than or equal to start line');
439
- }
440
- // Auto-adjust if startLine exceeds file length
441
- const start = Math.min(actualStartLine, totalLines);
442
- const end = Math.min(totalLines, actualEndLine);
443
- // Extract specified lines (convert to 0-indexed) and add line numbers
444
- const selectedLines = lines.slice(start - 1, end);
445
- // Format with line numbers (no padding to save tokens)
446
- const numberedLines = selectedLines.map((line, index) => {
447
- const lineNum = start + index;
448
- return `${lineNum}→${line}`;
449
- });
450
- let partialContent = numberedLines.join('\n');
451
- // Parse and append symbol information to provide better context for AI
452
- try {
453
- const symbols = await parseFileSymbols(fullPath, content, this.basePath);
454
- const symbolInfo = this.extractRelevantSymbols(symbols, start, end, totalLines);
455
- if (symbolInfo) {
456
- partialContent += symbolInfo;
457
- }
458
- }
459
- catch (error) {
460
- // Silently fail symbol parsing - don't block file reading
461
- // This is optional context enhancement, not critical
462
- }
463
- // Append notebook entries
464
- const notebookInfo = this.getNotebookEntries(filePath);
465
- if (notebookInfo) {
466
- partialContent += notebookInfo;
467
- }
468
- return {
469
- content: partialContent,
470
- startLine: start,
471
- endLine: end,
472
- totalLines,
473
- };
474
- }
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
- }
494
- throw new Error(`Failed to read file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
495
- }
496
- }
497
- /**
498
- * Create a new file with specified content
499
- * @param filePath - Path where the file should be created
500
- * @param content - Content to write to the file
501
- * @param createDirectories - Whether to create parent directories if they don't exist
502
- * @returns Success message
503
- * @throws Error if file creation fails
504
- */
505
- async createFile(filePath, content, createDirectories = true) {
506
- try {
507
- const fullPath = this.resolvePath(filePath);
508
- // Check if file already exists
509
- try {
510
- await fs.access(fullPath);
511
- throw new Error(`File already exists: ${filePath}`);
512
- }
513
- catch (error) {
514
- // File doesn't exist, which is what we want
515
- if (error.code !== 'ENOENT') {
516
- throw error;
517
- }
518
- }
519
- // Backup file before creation
520
- await incrementalSnapshotManager.backupFile(fullPath);
521
- // Create parent directories if needed
522
- if (createDirectories) {
523
- const dir = dirname(fullPath);
524
- await fs.mkdir(dir, { recursive: true });
525
- }
526
- await fs.writeFile(fullPath, content, 'utf-8');
527
- return `File created successfully: ${filePath}`;
528
- }
529
- catch (error) {
530
- throw new Error(`Failed to create file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
531
- }
532
- }
533
- /**
534
- * List files in a directory (internal use for read tool)
535
- * @param dirPath - Directory path relative to base path or absolute path
536
- * @returns Array of file names
537
- * @throws Error if directory cannot be read
538
- * @private
539
- */
540
- async listFiles(dirPath = '.') {
541
- try {
542
- const fullPath = this.resolvePath(dirPath);
543
- // For absolute paths, skip validation to allow access outside base path
544
- if (!isAbsolute(dirPath)) {
545
- await this.validatePath(fullPath);
546
- }
547
- const stats = await fs.stat(fullPath);
548
- if (!stats.isDirectory()) {
549
- throw new Error(`Path is not a directory: ${dirPath}`);
550
- }
551
- const files = await fs.readdir(fullPath);
552
- return files;
553
- }
554
- catch (error) {
555
- throw new Error(`Failed to list files in ${dirPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
556
- }
557
- }
558
- /**
559
- * Check if a file or directory exists
560
- * @param filePath - Path to check
561
- * @returns Boolean indicating existence
562
- */
563
- async exists(filePath) {
564
- try {
565
- const fullPath = this.resolvePath(filePath);
566
- await fs.access(fullPath);
567
- return true;
568
- }
569
- catch {
570
- return false;
571
- }
572
- }
573
- /**
574
- * Get file information (stats)
575
- * @param filePath - Path to the file
576
- * @returns File stats object
577
- * @throws Error if file doesn't exist
578
- */
579
- async getFileInfo(filePath) {
580
- try {
581
- const fullPath = this.resolvePath(filePath);
582
- await this.validatePath(fullPath);
583
- const stats = await fs.stat(fullPath);
584
- return {
585
- size: stats.size,
586
- isFile: stats.isFile(),
587
- isDirectory: stats.isDirectory(),
588
- modified: stats.mtime,
589
- created: stats.birthtime,
590
- };
591
- }
592
- catch (error) {
593
- throw new Error(`Failed to get file info for ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
594
- }
595
- }
596
- /**
597
- * Edit file(s) by searching for exact content and replacing it
598
- * This method uses SMART MATCHING to handle whitespace differences automatically.
599
- *
600
- * @param filePath - Path to the file to edit, or array of file paths, or array of edit config objects
601
- * @param searchContent - Content to search for (for single file or unified mode)
602
- * @param replaceContent - New content to replace (for single file or unified mode)
603
- * @param occurrence - Which occurrence to replace (1-indexed, default: 1, use -1 for all)
604
- * @param contextLines - Number of context lines to return before and after the edit (default: 8)
605
- * @returns Object containing success message, before/after comparison, and diagnostics from IDE (VSCode or JetBrains)
606
- * @throws Error if search content is not found or multiple matches exist
607
- */
608
- async editFileBySearch(filePath, searchContent, replaceContent, occurrence = 1, contextLines = 8) {
609
- // Handle array of files
610
- if (Array.isArray(filePath)) {
611
- return await executeBatchOperation(filePath, fileItem => parseEditBySearchParams(fileItem, searchContent, replaceContent, occurrence), (path, search, replace, occ) => this.editFileBySearchSingle(path, search, replace, occ, contextLines), (path, result) => {
612
- return { path, ...result };
613
- });
614
- }
615
- // Single file mode
616
- if (searchContent === undefined ||
617
- searchContent === null ||
618
- replaceContent === undefined ||
619
- replaceContent === null) {
620
- throw new Error('searchContent and replaceContent are required for single file mode');
621
- }
622
- return await this.editFileBySearchSingle(filePath, searchContent, replaceContent, occurrence, contextLines);
623
- }
624
- /**
625
- * Internal method: Edit a single file by search-replace
626
- * @private
627
- */
628
- async editFileBySearchSingle(filePath, searchContent, replaceContent, occurrence, contextLines) {
629
- try {
630
- const fullPath = this.resolvePath(filePath);
631
- // For absolute paths, skip validation to allow access outside base path
632
- if (!isAbsolute(filePath)) {
633
- await this.validatePath(fullPath);
634
- }
635
- // Read the entire file
636
- const content = await fs.readFile(fullPath, 'utf-8');
637
- const lines = content.split('\n');
638
- // Normalize line endings
639
- let normalizedSearch = searchContent
640
- .replace(/\r\n/g, '\n')
641
- .replace(/\r/g, '\n');
642
- const normalizedContent = content
643
- .replace(/\r\n/g, '\n')
644
- .replace(/\r/g, '\n');
645
- // Split into lines for matching
646
- let searchLines = normalizedSearch.split('\n');
647
- const contentLines = normalizedContent.split('\n');
648
- // Find all matches using smart fuzzy matching (auto-handles whitespace)
649
- const matches = [];
650
- const threshold = 0.6; // Lowered to 60% to allow smaller partial edits (was 0.75)
651
- // Fast pre-filter: use first line as anchor to skip unlikely positions
652
- // Only apply pre-filter for multi-line searches to avoid missing valid matches
653
- const searchFirstLine = searchLines[0]?.replace(/\s+/g, ' ').trim() || '';
654
- const usePreFilter = searchLines.length >= 5; // Only pre-filter for 5+ line searches
655
- const preFilterThreshold = 0.2; // Very low threshold - only skip completely unrelated lines
656
- const maxMatches = 10; // Limit matches to avoid excessive computation
657
- const YIELD_INTERVAL = 100; // Yield control every 100 iterations to prevent UI freeze
658
- for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
659
- // Yield control periodically to prevent UI freeze
660
- if (i % YIELD_INTERVAL === 0) {
661
- await new Promise(resolve => setTimeout(resolve, 0));
662
- }
663
- // Quick pre-filter: check first line similarity (only for multi-line searches)
664
- if (usePreFilter) {
665
- const firstLineCandidate = contentLines[i]?.replace(/\s+/g, ' ').trim() || '';
666
- const firstLineSimilarity = calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
667
- // Skip only if first line is very different (< 30% match)
668
- // This is safe because if first line differs this much, full match unlikely
669
- if (firstLineSimilarity < preFilterThreshold) {
670
- continue;
671
- }
672
- }
673
- // Full candidate check
674
- const candidateLines = contentLines.slice(i, i + searchLines.length);
675
- const candidateContent = candidateLines.join('\n');
676
- const similarity = calculateSimilarity(normalizedSearch, candidateContent, threshold);
677
- // Accept matches above threshold
678
- if (similarity >= threshold) {
679
- matches.push({
680
- startLine: i + 1,
681
- endLine: i + searchLines.length,
682
- similarity,
683
- });
684
- // Early exit if we found a nearly perfect match
685
- if (similarity >= 0.95) {
686
- break;
687
- }
688
- // Limit matches to avoid excessive computation
689
- if (matches.length >= maxMatches) {
690
- break;
691
- }
692
- }
693
- }
694
- // Sort by similarity descending (best match first)
695
- matches.sort((a, b) => b.similarity - a.similarity);
696
- // Handle no matches: Try escape correction before giving up
697
- if (matches.length === 0) {
698
- // Step 1: Try unescape correction (lightweight, no LLM)
699
- const unescapeFix = tryUnescapeFix(normalizedContent, normalizedSearch, 1);
700
- if (unescapeFix) {
701
- // Unescape succeeded! Re-run the matching with corrected content
702
- const correctedSearchLines = unescapeFix.correctedString.split('\n');
703
- for (let i = 0; i <= contentLines.length - correctedSearchLines.length; i++) {
704
- // Yield control periodically to prevent UI freeze
705
- if (i % YIELD_INTERVAL === 0) {
706
- await new Promise(resolve => setTimeout(resolve, 0));
707
- }
708
- const candidateLines = contentLines.slice(i, i + correctedSearchLines.length);
709
- const candidateContent = candidateLines.join('\n');
710
- const similarity = calculateSimilarity(unescapeFix.correctedString, candidateContent);
711
- if (similarity >= threshold) {
712
- matches.push({
713
- startLine: i + 1,
714
- endLine: i + correctedSearchLines.length,
715
- similarity,
716
- });
717
- }
718
- }
719
- matches.sort((a, b) => b.similarity - a.similarity);
720
- // If unescape fix worked, also fix replaceContent if needed
721
- if (matches.length > 0) {
722
- const trimResult = trimPairIfPossible(unescapeFix.correctedString, replaceContent, normalizedContent, 1);
723
- // Update searchContent and replaceContent for the edit
724
- normalizedSearch = trimResult.target;
725
- replaceContent = trimResult.paired;
726
- // Also update searchLines for later use
727
- searchLines.splice(0, searchLines.length, ...normalizedSearch.split('\n'));
728
- }
729
- }
730
- // If still no matches after unescape, provide detailed error
731
- if (matches.length === 0) {
732
- // Find closest matches for suggestions
733
- const closestMatches = await findClosestMatches(normalizedSearch, normalizedContent.split('\n'), 3);
734
- let errorMessage = `❌ Search content not found in file: ${filePath}\n\n`;
735
- errorMessage += `🔍 Using smart fuzzy matching (threshold: 60%)\n`;
736
- if (isOverEscaped(searchContent)) {
737
- errorMessage += `⚠️ Detected over-escaped content, automatic fix attempted but failed\n`;
738
- }
739
- errorMessage += `\n`;
740
- if (closestMatches.length > 0) {
741
- errorMessage += `💡 Found ${closestMatches.length} similar location(s):\n\n`;
742
- closestMatches.forEach((candidate, idx) => {
743
- errorMessage += `${idx + 1}. Lines ${candidate.startLine}-${candidate.endLine} (${(candidate.similarity * 100).toFixed(0)}% match):\n`;
744
- errorMessage += `${candidate.preview}\n\n`;
745
- });
746
- // Show diff with the closest match
747
- const bestMatch = closestMatches[0];
748
- if (bestMatch) {
749
- const bestMatchLines = lines.slice(bestMatch.startLine - 1, bestMatch.endLine);
750
- const bestMatchContent = bestMatchLines.join('\n');
751
- const diffMsg = generateDiffMessage(normalizedSearch, bestMatchContent, 5);
752
- if (diffMsg) {
753
- errorMessage += `📊 Difference with closest match:\n${diffMsg}\n\n`;
754
- }
755
- }
756
- errorMessage += `💡 Suggestions:\n`;
757
- errorMessage += ` • Make sure you copied content from filesystem-read (without "123→")\n`;
758
- errorMessage += ` • Whitespace differences are automatically handled\n`;
759
- errorMessage += ` • Try copying a larger or smaller code block\n`;
760
- errorMessage += ` • If multiple filesystem-edit_search attempts fail, use terminal-execute to edit via command line (e.g. sed, printf)\n`;
761
- errorMessage += `⚠️ No similar content found in the file.\n\n`;
762
- errorMessage += `📝 What you searched for (first 5 lines, formatted):\n`;
763
- searchLines.slice(0, 5).forEach((line, idx) => {
764
- errorMessage += `${idx + 1}. ${JSON.stringify(normalizeForDisplay(line))}\n`;
765
- });
766
- errorMessage += `\n💡 Copy exact content from filesystem-read (without line numbers)\n`;
767
- }
768
- throw new Error(errorMessage);
769
- }
770
- }
771
- // Handle occurrence selection
772
- let selectedMatch;
773
- if (occurrence === -1) {
774
- // Replace all occurrences
775
- if (matches.length === 1) {
776
- selectedMatch = matches[0];
777
- }
778
- else {
779
- throw new Error(`Found ${matches.length} matches. Please specify which occurrence to replace (1-${matches.length}), or use occurrence=-1 to replace all (not yet implemented for safety).`);
780
- }
781
- }
782
- else if (occurrence < 1 || occurrence > matches.length) {
783
- throw new Error(`Invalid occurrence ${occurrence}. Found ${matches.length} match(es) at lines: ${matches.map(m => m.startLine).join(', ')}`);
784
- }
785
- else {
786
- selectedMatch = matches[occurrence - 1];
787
- }
788
- const { startLine, endLine } = selectedMatch;
789
- // Backup file before editing
790
- await incrementalSnapshotManager.backupFile(fullPath);
791
- // Perform the replacement by replacing the matched lines
792
- const normalizedReplace = replaceContent
793
- .replace(/\r\n/g, '\n')
794
- .replace(/\r/g, '\n');
795
- const beforeLines = lines.slice(0, startLine - 1);
796
- const afterLines = lines.slice(endLine);
797
- const replaceLines = normalizedReplace.split('\n');
798
- const modifiedLines = [...beforeLines, ...replaceLines, ...afterLines];
799
- const modifiedContent = modifiedLines.join('\n');
800
- // Calculate replaced content for display (compress whitespace for readability)
801
- const replacedLines = lines.slice(startLine - 1, endLine);
802
- const replacedContent = replacedLines
803
- .map((line, idx) => {
804
- const lineNum = startLine + idx;
805
- return `${lineNum}→${normalizeForDisplay(line)}`;
806
- })
807
- .join('\n');
808
- // Calculate context boundaries
809
- const lineDifference = replaceLines.length - (endLine - startLine + 1);
810
- const smartBoundaries = findSmartContextBoundaries(lines, startLine, endLine, contextLines);
811
- const contextStart = smartBoundaries.start;
812
- const contextEnd = smartBoundaries.end;
813
- // Extract old content for context (compress whitespace for readability)
814
- const oldContextLines = lines.slice(contextStart - 1, contextEnd);
815
- const oldContent = oldContextLines
816
- .map((line, idx) => {
817
- const lineNum = contextStart + idx;
818
- return `${lineNum}→${normalizeForDisplay(line)}`;
819
- })
820
- .join('\n');
821
- // Write the modified content
822
- await fs.writeFile(fullPath, modifiedContent, 'utf-8');
823
- // Format with Prettier asynchronously (non-blocking)
824
- let finalContent = modifiedContent;
825
- let finalLines = modifiedLines;
826
- let finalTotalLines = modifiedLines.length;
827
- let finalContextEnd = Math.min(finalTotalLines, contextEnd + lineDifference);
828
- // Check if Prettier supports this file type
829
- const fileExtension = path.extname(fullPath).toLowerCase();
830
- const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
831
- if (shouldFormat) {
832
- try {
833
- // Use Prettier API for better performance (avoids npx overhead)
834
- const prettierConfig = await prettier.resolveConfig(fullPath);
835
- finalContent = await prettier.format(modifiedContent, {
836
- filepath: fullPath,
837
- ...prettierConfig,
838
- });
839
- // Write formatted content back to file
840
- await fs.writeFile(fullPath, finalContent, 'utf-8');
841
- finalLines = finalContent.split('\n');
842
- finalTotalLines = finalLines.length;
843
- finalContextEnd = Math.min(finalTotalLines, contextStart + (contextEnd - contextStart) + lineDifference);
844
- }
845
- catch (formatError) {
846
- // Continue with unformatted content
847
- }
848
- }
849
- // Extract new content for context (compress whitespace for readability)
850
- const newContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
851
- const newContextContent = newContextLines
852
- .map((line, idx) => {
853
- const lineNum = contextStart + idx;
854
- return `${lineNum}→${normalizeForDisplay(line)}`;
855
- })
856
- .join('\n');
857
- // Analyze code structure
858
- const editedContentLines = replaceLines;
859
- const structureAnalysis = analyzeCodeStructure(finalContent, filePath, editedContentLines);
860
- // Get diagnostics from IDE (VSCode or JetBrains) - non-blocking, fire-and-forget
861
- let diagnostics = [];
862
- try {
863
- // Request diagnostics without blocking (with timeout protection)
864
- const diagnosticsPromise = Promise.race([
865
- vscodeConnection.requestDiagnostics(fullPath),
866
- new Promise(resolve => setTimeout(() => resolve([]), 1000)), // 1s max wait
867
- ]);
868
- diagnostics = await diagnosticsPromise;
869
- }
870
- catch (error) {
871
- // Ignore diagnostics errors - this is optional functionality
872
- }
873
- // Build result
874
- const result = {
875
- message: `✅ File edited successfully using search-replace (safer boundary detection): ${filePath}\n` +
876
- ` Matched: lines ${startLine}-${endLine} (occurrence ${occurrence}/${matches.length})\n` +
877
- ` Result: ${replaceLines.length} new lines` +
878
- (smartBoundaries.extended
879
- ? `\n 📍 Context auto-extended to show complete code block (lines ${contextStart}-${finalContextEnd})`
880
- : ''),
881
- oldContent,
882
- newContent: newContextContent,
883
- replacedContent,
884
- matchLocation: { startLine, endLine },
885
- contextStartLine: contextStart,
886
- contextEndLine: finalContextEnd,
887
- totalLines: finalTotalLines,
888
- structureAnalysis,
889
- diagnostics: undefined,
890
- };
891
- // Add diagnostics if found
892
- if (diagnostics.length > 0) {
893
- // Limit diagnostics to top 10 to avoid excessive token usage
894
- const limitedDiagnostics = diagnostics.slice(0, 10);
895
- result.diagnostics = limitedDiagnostics;
896
- const errorCount = diagnostics.filter(d => d.severity === 'error').length;
897
- const warningCount = diagnostics.filter(d => d.severity === 'warning').length;
898
- if (errorCount > 0 || warningCount > 0) {
899
- result.message += `\n\n⚠️ Diagnostics detected: ${errorCount} error(s), ${warningCount} warning(s)`;
900
- // Format diagnostics for better readability (limit to first 5 for message display)
901
- const formattedDiagnostics = diagnostics
902
- .filter(d => d.severity === 'error' || d.severity === 'warning')
903
- .slice(0, 5)
904
- .map(d => {
905
- const icon = d.severity === 'error' ? '❌' : '⚠️';
906
- const location = `${filePath}:${d.line}:${d.character}`;
907
- return ` ${icon} [${d.source || 'unknown'}] ${location}\n ${d.message}`;
908
- })
909
- .join('\n\n');
910
- result.message += `\n\n📋 Diagnostic Details:\n${formattedDiagnostics}`;
911
- if (errorCount + warningCount > 5) {
912
- result.message += `\n ... and ${errorCount + warningCount - 5} more issue(s)`;
913
- }
914
- result.message += `\n\n ⚡ TIP: Review the errors above and make another edit to fix them`;
915
- }
916
- }
917
- // Add structure analysis warnings
918
- const structureWarnings = [];
919
- if (!structureAnalysis.bracketBalance.curly.balanced) {
920
- const diff = structureAnalysis.bracketBalance.curly.open -
921
- structureAnalysis.bracketBalance.curly.close;
922
- structureWarnings.push(`Curly brackets: ${diff > 0 ? `${diff} unclosed {` : `${Math.abs(diff)} extra }`}`);
923
- }
924
- if (!structureAnalysis.bracketBalance.round.balanced) {
925
- const diff = structureAnalysis.bracketBalance.round.open -
926
- structureAnalysis.bracketBalance.round.close;
927
- structureWarnings.push(`Round brackets: ${diff > 0 ? `${diff} unclosed (` : `${Math.abs(diff)} extra )`}`);
928
- }
929
- if (!structureAnalysis.bracketBalance.square.balanced) {
930
- const diff = structureAnalysis.bracketBalance.square.open -
931
- structureAnalysis.bracketBalance.square.close;
932
- structureWarnings.push(`Square brackets: ${diff > 0 ? `${diff} unclosed [` : `${Math.abs(diff)} extra ]`}`);
933
- }
934
- if (structureAnalysis.htmlTags && !structureAnalysis.htmlTags.balanced) {
935
- if (structureAnalysis.htmlTags.unclosedTags.length > 0) {
936
- structureWarnings.push(`Unclosed HTML tags: ${structureAnalysis.htmlTags.unclosedTags.join(', ')}`);
937
- }
938
- if (structureAnalysis.htmlTags.unopenedTags.length > 0) {
939
- structureWarnings.push(`Unopened closing tags: ${structureAnalysis.htmlTags.unopenedTags.join(', ')}`);
940
- }
941
- }
942
- if (structureAnalysis.indentationWarnings.length > 0) {
943
- structureWarnings.push(...structureAnalysis.indentationWarnings.map((w) => `Indentation: ${w}`));
944
- }
945
- // Note: Boundary warnings removed - partial edits are common and expected
946
- if (structureWarnings.length > 0) {
947
- result.message += `\n\n🔍 Structure Analysis:\n`;
948
- structureWarnings.forEach(warning => {
949
- result.message += ` ⚠️ ${warning}\n`;
950
- });
951
- result.message += `\n 💡 TIP: These warnings help identify potential issues. If intentional (e.g., opening a block), you can ignore them.`;
952
- }
953
- return result;
954
- }
955
- catch (error) {
956
- throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
957
- }
958
- }
959
- /**
960
- * Edit file(s) by replacing lines within a specified range
961
- * BEST PRACTICE: Keep edits small and focused (≤15 lines recommended) for better accuracy.
962
- * For larger changes, make multiple parallel edits to non-overlapping sections instead of one large edit.
963
- *
964
- * @param filePath - Path to the file to edit, or array of file paths, or array of edit config objects
965
- * @param startLine - Starting line number (for single file or unified mode)
966
- * @param endLine - Ending line number (for single file or unified mode)
967
- * @param newContent - New content to replace (for single file or unified mode)
968
- * @param contextLines - Number of context lines to return before and after the edit (default: 8)
969
- * @returns Object containing success message, precise before/after comparison, and diagnostics from IDE (VSCode or JetBrains)
970
- * @throws Error if file editing fails
971
- */
972
- async editFile(filePath, startLine, endLine, newContent, contextLines = 8) {
973
- // Handle array of files
974
- if (Array.isArray(filePath)) {
975
- return await executeBatchOperation(filePath, fileItem => parseEditByLineParams(fileItem, startLine, endLine, newContent), (path, start, end, content) => this.editFileSingle(path, start, end, content, contextLines), (path, result) => {
976
- return { path, ...result };
977
- });
978
- }
979
- // Single file mode
980
- if (startLine === undefined ||
981
- endLine === undefined ||
982
- newContent === undefined) {
983
- throw new Error('startLine, endLine, and newContent are required for single file mode');
984
- }
985
- return await this.editFileSingle(filePath, startLine, endLine, newContent, contextLines);
986
- }
987
- /**
988
- * Internal method: Edit a single file by line range
989
- * @private
990
- */
991
- async editFileSingle(filePath, startLine, endLine, newContent, contextLines) {
992
- try {
993
- const fullPath = this.resolvePath(filePath);
994
- // For absolute paths, skip validation to allow access outside base path
995
- if (!isAbsolute(filePath)) {
996
- await this.validatePath(fullPath);
997
- }
998
- // Read the entire file
999
- const content = await fs.readFile(fullPath, 'utf-8');
1000
- const lines = content.split('\n');
1001
- const totalLines = lines.length;
1002
- // Validate line numbers
1003
- if (startLine < 1 || endLine < 1) {
1004
- throw new Error('Line numbers must be greater than 0');
1005
- }
1006
- if (startLine > endLine) {
1007
- throw new Error('Start line must be less than or equal to end line');
1008
- }
1009
- // Adjust startLine and endLine if they exceed file length
1010
- const adjustedStartLine = Math.min(startLine, totalLines);
1011
- const adjustedEndLine = Math.min(endLine, totalLines);
1012
- const linesToModify = adjustedEndLine - adjustedStartLine + 1;
1013
- // Backup file before editing
1014
- await incrementalSnapshotManager.backupFile(fullPath);
1015
- // Extract the lines that will be replaced (for comparison)
1016
- // Compress whitespace for display readability
1017
- const replacedLines = lines.slice(adjustedStartLine - 1, adjustedEndLine);
1018
- const replacedContent = replacedLines
1019
- .map((line, idx) => {
1020
- const lineNum = adjustedStartLine + idx;
1021
- return `${lineNum}→${normalizeForDisplay(line)}`;
1022
- })
1023
- .join('\n');
1024
- // Calculate context range using smart boundary detection
1025
- const smartBoundaries = findSmartContextBoundaries(lines, adjustedStartLine, adjustedEndLine, contextLines);
1026
- const contextStart = smartBoundaries.start;
1027
- const contextEnd = smartBoundaries.end;
1028
- // Extract old content for context (compress whitespace for readability)
1029
- const oldContextLines = lines.slice(contextStart - 1, contextEnd);
1030
- const oldContent = oldContextLines
1031
- .map((line, idx) => {
1032
- const lineNum = contextStart + idx;
1033
- return `${lineNum}→${normalizeForDisplay(line)}`;
1034
- })
1035
- .join('\n');
1036
- // Replace the specified lines
1037
- const newContentLines = newContent.split('\n');
1038
- const beforeLines = lines.slice(0, adjustedStartLine - 1);
1039
- const afterLines = lines.slice(adjustedEndLine);
1040
- const modifiedLines = [...beforeLines, ...newContentLines, ...afterLines];
1041
- // Calculate new context range
1042
- const newTotalLines = modifiedLines.length;
1043
- const lineDifference = newContentLines.length - (adjustedEndLine - adjustedStartLine + 1);
1044
- const newContextEnd = Math.min(newTotalLines, contextEnd + lineDifference);
1045
- // Extract new content for context with line numbers (compress whitespace)
1046
- const newContextLines = modifiedLines.slice(contextStart - 1, newContextEnd);
1047
- const newContextContent = newContextLines
1048
- .map((line, idx) => {
1049
- const lineNum = contextStart + idx;
1050
- return `${lineNum}→${normalizeForDisplay(line)}`;
1051
- })
1052
- .join('\n');
1053
- // Write the modified content back to file
1054
- await fs.writeFile(fullPath, modifiedLines.join('\n'), 'utf-8');
1055
- // Format the file with Prettier after editing to ensure consistent code style
1056
- let finalLines = modifiedLines;
1057
- let finalTotalLines = newTotalLines;
1058
- let finalContextEnd = newContextEnd;
1059
- let finalContextContent = newContextContent;
1060
- // Check if Prettier supports this file type
1061
- const fileExtension = path.extname(fullPath).toLowerCase();
1062
- const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
1063
- if (shouldFormat) {
1064
- try {
1065
- // Use Prettier API for better performance (avoids npx overhead)
1066
- const prettierConfig = await prettier.resolveConfig(fullPath);
1067
- const newContent = modifiedLines.join('\n');
1068
- const formattedContent = await prettier.format(newContent, {
1069
- filepath: fullPath,
1070
- ...prettierConfig,
1071
- });
1072
- // Write formatted content back to file
1073
- await fs.writeFile(fullPath, formattedContent, 'utf-8');
1074
- finalLines = formattedContent.split('\n');
1075
- finalTotalLines = finalLines.length;
1076
- // Recalculate the context end line based on formatted content
1077
- finalContextEnd = Math.min(finalTotalLines, contextStart + (newContextEnd - contextStart));
1078
- // Extract formatted content for context (compress whitespace)
1079
- const formattedContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
1080
- finalContextContent = formattedContextLines
1081
- .map((line, idx) => {
1082
- const lineNum = contextStart + idx;
1083
- return `${lineNum}→${normalizeForDisplay(line)}`;
1084
- })
1085
- .join('\n');
1086
- }
1087
- catch (formatError) {
1088
- // If formatting fails, continue with the original content
1089
- // This ensures editing is not blocked by formatting issues
1090
- }
1091
- }
1092
- // Analyze code structure of the edited content (using formatted content if available)
1093
- const editedContentLines = finalLines.slice(adjustedStartLine - 1, adjustedStartLine - 1 + newContentLines.length);
1094
- const structureAnalysis = analyzeCodeStructure(finalLines.join('\n'), filePath, editedContentLines);
1095
- // Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
1096
- let diagnostics = [];
1097
- try {
1098
- // Request diagnostics without blocking (with timeout protection)
1099
- const diagnosticsPromise = Promise.race([
1100
- vscodeConnection.requestDiagnostics(fullPath),
1101
- new Promise(resolve => setTimeout(() => resolve([]), 1000)), // 1s max wait
1102
- ]);
1103
- diagnostics = await diagnosticsPromise;
1104
- }
1105
- catch (error) {
1106
- // Ignore diagnostics errors - they are optional
1107
- }
1108
- const result = {
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` +
1110
- ` Replaced: lines ${adjustedStartLine}-${adjustedEndLine} (${linesToModify} lines)\n` +
1111
- ` Result: ${newContentLines.length} new lines` +
1112
- (smartBoundaries.extended
1113
- ? `\n 📍 Context auto-extended to show complete code block (lines ${contextStart}-${finalContextEnd})`
1114
- : ''),
1115
- oldContent,
1116
- newContent: finalContextContent,
1117
- replacedLines: replacedContent,
1118
- contextStartLine: contextStart,
1119
- contextEndLine: finalContextEnd,
1120
- totalLines: finalTotalLines,
1121
- linesModified: linesToModify,
1122
- structureAnalysis,
1123
- };
1124
- // Add diagnostics if any were found
1125
- if (diagnostics.length > 0) {
1126
- // Limit diagnostics to top 10 to avoid excessive token usage
1127
- const limitedDiagnostics = diagnostics.slice(0, 10);
1128
- result.diagnostics = limitedDiagnostics;
1129
- const errorCount = diagnostics.filter(d => d.severity === 'error').length;
1130
- const warningCount = diagnostics.filter(d => d.severity === 'warning').length;
1131
- if (errorCount > 0 || warningCount > 0) {
1132
- result.message += `\n\n⚠️ Diagnostics detected: ${errorCount} error(s), ${warningCount} warning(s)`;
1133
- // Format diagnostics for better readability (limit to first 5 for message display)
1134
- const formattedDiagnostics = diagnostics
1135
- .filter(d => d.severity === 'error' || d.severity === 'warning')
1136
- .slice(0, 5)
1137
- .map(d => {
1138
- const icon = d.severity === 'error' ? '❌' : '⚠️';
1139
- const location = `${filePath}:${d.line}:${d.character}`;
1140
- return ` ${icon} [${d.source || 'unknown'}] ${location}\n ${d.message}`;
1141
- })
1142
- .join('\n\n');
1143
- result.message += `\n\n📋 Diagnostic Details:\n${formattedDiagnostics}`;
1144
- if (errorCount + warningCount > 5) {
1145
- result.message += `\n ... and ${errorCount + warningCount - 5} more issue(s)`;
1146
- }
1147
- result.message += `\n\n ⚡ TIP: Review the errors above and make another small edit to fix them`;
1148
- }
1149
- }
1150
- // Add structure analysis warnings to the message
1151
- const structureWarnings = [];
1152
- // Check bracket balance
1153
- if (!structureAnalysis.bracketBalance.curly.balanced) {
1154
- const diff = structureAnalysis.bracketBalance.curly.open -
1155
- structureAnalysis.bracketBalance.curly.close;
1156
- structureWarnings.push(`Curly brackets: ${diff > 0 ? `${diff} unclosed {` : `${Math.abs(diff)} extra }`}`);
1157
- }
1158
- if (!structureAnalysis.bracketBalance.round.balanced) {
1159
- const diff = structureAnalysis.bracketBalance.round.open -
1160
- structureAnalysis.bracketBalance.round.close;
1161
- structureWarnings.push(`Round brackets: ${diff > 0 ? `${diff} unclosed (` : `${Math.abs(diff)} extra )`}`);
1162
- }
1163
- if (!structureAnalysis.bracketBalance.square.balanced) {
1164
- const diff = structureAnalysis.bracketBalance.square.open -
1165
- structureAnalysis.bracketBalance.square.close;
1166
- structureWarnings.push(`Square brackets: ${diff > 0 ? `${diff} unclosed [` : `${Math.abs(diff)} extra ]`}`);
1167
- }
1168
- // Check HTML tags
1169
- if (structureAnalysis.htmlTags && !structureAnalysis.htmlTags.balanced) {
1170
- if (structureAnalysis.htmlTags.unclosedTags.length > 0) {
1171
- structureWarnings.push(`Unclosed HTML tags: ${structureAnalysis.htmlTags.unclosedTags.join(', ')}`);
1172
- }
1173
- if (structureAnalysis.htmlTags.unopenedTags.length > 0) {
1174
- structureWarnings.push(`Unopened closing tags: ${structureAnalysis.htmlTags.unopenedTags.join(', ')}`);
1175
- }
1176
- }
1177
- // Check indentation
1178
- if (structureAnalysis.indentationWarnings.length > 0) {
1179
- structureWarnings.push(...structureAnalysis.indentationWarnings.map((w) => `Indentation: ${w}`));
1180
- }
1181
- // Note: Boundary warnings removed - partial edits are common and expected
1182
- // Format structure warnings
1183
- if (structureWarnings.length > 0) {
1184
- result.message += `\n\n🔍 Structure Analysis:\n`;
1185
- structureWarnings.forEach(warning => {
1186
- result.message += ` ⚠️ ${warning}\n`;
1187
- });
1188
- result.message += `\n 💡 TIP: These warnings help identify potential issues. If intentional (e.g., opening a block), you can ignore them.`;
1189
- }
1190
- return result;
1191
- }
1192
- catch (error) {
1193
- throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
1194
- }
1195
- }
1196
- /**
1197
- * Resolve path relative to base path and normalize it
1198
- * @private
1199
- */
1200
- resolvePath(filePath) {
1201
- // Check if the path is already absolute
1202
- const isAbsolute = path.isAbsolute(filePath);
1203
- if (isAbsolute) {
1204
- // Return absolute path as-is (will be validated later)
1205
- return resolve(filePath);
1206
- }
1207
- // For relative paths, resolve against base path
1208
- // Remove any leading slashes to treat as relative path
1209
- const relativePath = filePath.replace(/^\/+/, '');
1210
- return resolve(this.basePath, relativePath);
1211
- }
1212
- /**
1213
- * Validate that the path is within the allowed base directory
1214
- * @private
1215
- */
1216
- async validatePath(fullPath) {
1217
- const normalizedPath = resolve(fullPath);
1218
- const normalizedBase = resolve(this.basePath);
1219
- if (!normalizedPath.startsWith(normalizedBase)) {
1220
- throw new Error('Access denied: Path is outside of allowed directory');
1221
- }
1222
- }
1223
- }
1224
- // Export a default instance
1225
- export const filesystemService = new FilesystemMCPService();
1226
- export const mcpTools = [
1227
- {
1228
- name: 'filesystem-read',
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.',
1230
- inputSchema: {
1231
- type: 'object',
1232
- properties: {
1233
- filePath: {
1234
- oneOf: [
1235
- {
1236
- type: 'string',
1237
- description: 'Path to a single file to read or directory to list',
1238
- },
1239
- {
1240
- type: 'array',
1241
- items: {
1242
- type: 'string',
1243
- },
1244
- description: 'Array of file paths to read in one call (uses unified startLine/endLine from top-level parameters)',
1245
- },
1246
- {
1247
- type: 'array',
1248
- items: {
1249
- type: 'object',
1250
- properties: {
1251
- path: {
1252
- type: 'string',
1253
- description: 'File path',
1254
- },
1255
- startLine: {
1256
- type: 'number',
1257
- description: 'Optional: Starting line for this file (overrides top-level startLine)',
1258
- },
1259
- endLine: {
1260
- type: 'number',
1261
- description: 'Optional: Ending line for this file (overrides top-level endLine)',
1262
- },
1263
- },
1264
- required: ['path'],
1265
- },
1266
- description: 'Array of file config objects with per-file line ranges. Each file can have its own startLine/endLine.',
1267
- },
1268
- ],
1269
- description: 'Path to the file(s) to read or directory to list: string, array of strings, or array of {path, startLine?, endLine?} objects',
1270
- },
1271
- startLine: {
1272
- type: 'number',
1273
- description: 'Optional: Default starting line number (1-indexed) for all files. Omit to read from line 1. Can be overridden by per-file startLine in object format.',
1274
- },
1275
- endLine: {
1276
- type: 'number',
1277
- description: 'Optional: Default ending line number (1-indexed) for all files. Omit to read to end of file. Can be overridden by per-file endLine in object format.',
1278
- },
1279
- },
1280
- required: ['filePath'],
1281
- },
1282
- },
1283
- {
1284
- name: 'filesystem-create',
1285
- description: 'Preferred tool for creating files: Use specified content to create a new file. Before creating the file, you need to determine if the file already exists; if it does, your creation will fail. You should use editing instead of creation, as this tool is more reliable than terminal commands like echo/cat with redirection. If necessary, automatically create the parent directory. If necessary, terminal commands can be used as a fallback.',
1286
- inputSchema: {
1287
- type: 'object',
1288
- properties: {
1289
- filePath: {
1290
- type: 'string',
1291
- description: 'Path where the file should be created',
1292
- },
1293
- content: {
1294
- type: 'string',
1295
- description: 'Content to write to the file',
1296
- },
1297
- createDirectories: {
1298
- type: 'boolean',
1299
- description: "Whether to create parent directories if they don't exist",
1300
- default: true,
1301
- },
1302
- },
1303
- required: ['filePath', 'content'],
1304
- },
1305
- },
1306
- {
1307
- name: 'filesystem-edit_search',
1308
- description: 'RECOMMENDED for most edits: Search-and-replace with SMART FUZZY MATCHING. SUPPORTS BATCH EDITING: Pass (1) single file with search/replace, (2) array of file paths with unified search/replace, or (3) array of {path, searchContent, replaceContent, occurrence?} for per-file edits. CRITICAL WORKFLOW FOR CODE SAFETY: (1) Use ace-text_search/ace-search_symbols to locate code, (2) MUST use filesystem-read to identify COMPLETE code boundaries (entire function body with all braces, complete markup tags with opening/closing pairs, full code blocks), (3) Copy the COMPLETE code block (without line numbers), (4) Verify boundaries are intact (matching braces/brackets/tags), (5) Use THIS tool. WHY: No line tracking, auto-handles spacing/tabs, finds best match. COMMON ERRORS TO AVOID: Modifying only part of a function (missing closing brace), incomplete markup tags (HTML/Vue/JSX), partial code blocks. Always include complete syntactic units. BATCH EXAMPLE: filePath=[{path:"a.ts", searchContent:"old1", replaceContent:"new1"}, {path:"b.ts", searchContent:"old2", replaceContent:"new2"}]',
1309
- inputSchema: {
1310
- type: 'object',
1311
- properties: {
1312
- filePath: {
1313
- oneOf: [
1314
- {
1315
- type: 'string',
1316
- description: 'Path to a single file to edit',
1317
- },
1318
- {
1319
- type: 'array',
1320
- items: {
1321
- type: 'string',
1322
- },
1323
- description: 'Array of file paths (uses unified searchContent/replaceContent from top-level)',
1324
- },
1325
- {
1326
- type: 'array',
1327
- items: {
1328
- type: 'object',
1329
- properties: {
1330
- path: {
1331
- type: 'string',
1332
- description: 'File path',
1333
- },
1334
- searchContent: {
1335
- type: 'string',
1336
- description: 'Content to search for in this file',
1337
- },
1338
- replaceContent: {
1339
- type: 'string',
1340
- description: 'New content to replace with',
1341
- },
1342
- occurrence: {
1343
- type: 'number',
1344
- description: 'Which match to replace (1-indexed, default: 1)',
1345
- },
1346
- },
1347
- required: ['path', 'searchContent', 'replaceContent'],
1348
- },
1349
- description: 'Array of edit config objects for per-file search-replace operations',
1350
- },
1351
- ],
1352
- description: 'File path(s) to edit',
1353
- },
1354
- searchContent: {
1355
- type: 'string',
1356
- description: 'Content to find and replace (for single file or unified mode). Copy from filesystem-read WITHOUT line numbers.',
1357
- },
1358
- replaceContent: {
1359
- type: 'string',
1360
- description: 'New content to replace with (for single file or unified mode)',
1361
- },
1362
- occurrence: {
1363
- type: 'number',
1364
- description: 'Which match to replace if multiple found (1-indexed). Default: 1 (best match first). Use -1 for all (not yet supported).',
1365
- default: 1,
1366
- },
1367
- contextLines: {
1368
- type: 'number',
1369
- description: 'Context lines to show before/after (default: 8)',
1370
- default: 8,
1371
- },
1372
- },
1373
- required: ['filePath'],
1374
- },
1375
- },
1376
- {
1377
- name: 'filesystem-edit',
1378
- description: 'Line-based editing for precise control. SUPPORTS BATCH EDITING: Pass (1) single file with line range, (2) array of file paths with unified line range, or (3) array of {path, startLine, endLine, newContent} for per-file edits. WHEN TO USE: (1) Adding new code sections, (2) Deleting specific line ranges, (3) When search-replace not suitable. CRITICAL WORKFLOW FOR CODE SAFETY: (1) Use ace-text_search/ace-file_outline to locate area, (2) MUST use filesystem-read to identify COMPLETE code boundaries - for functions: include opening line to closing brace; for markup tags (HTML/Vue/JSX): include opening tag to closing tag; for code blocks: include all braces/brackets, (3) Verify line range covers the ENTIRE syntactic unit (check indentation levels, matching pairs), (4) Use THIS tool with exact startLine/endLine. RECOMMENDATION: For modifying existing code, use filesystem-edit_search - safer and no line tracking needed. COMMON ERRORS TO AVOID: Line range stops mid-function (missing closing brace), partial markup tags, incomplete code blocks. Always verify boundaries with filesystem-read first. BATCH EXAMPLE: filePath=[{path:"a.ts", startLine:10, endLine:20, newContent:"..."}, {path:"b.ts", startLine:50, endLine:60, newContent:"..."}]',
1379
- inputSchema: {
1380
- type: 'object',
1381
- properties: {
1382
- filePath: {
1383
- oneOf: [
1384
- {
1385
- type: 'string',
1386
- description: 'Path to a single file to edit',
1387
- },
1388
- {
1389
- type: 'array',
1390
- items: {
1391
- type: 'string',
1392
- },
1393
- description: 'Array of file paths (uses unified startLine/endLine/newContent from top-level)',
1394
- },
1395
- {
1396
- type: 'array',
1397
- items: {
1398
- type: 'object',
1399
- properties: {
1400
- path: {
1401
- type: 'string',
1402
- description: 'File path',
1403
- },
1404
- startLine: {
1405
- type: 'number',
1406
- description: 'Starting line number (1-indexed, inclusive)',
1407
- },
1408
- endLine: {
1409
- type: 'number',
1410
- description: 'Ending line number (1-indexed, inclusive)',
1411
- },
1412
- newContent: {
1413
- type: 'string',
1414
- description: 'New content to replace lines (without line numbers)',
1415
- },
1416
- },
1417
- required: ['path', 'startLine', 'endLine', 'newContent'],
1418
- },
1419
- description: 'Array of edit config objects for per-file line-based edits',
1420
- },
1421
- ],
1422
- description: 'File path(s) to edit',
1423
- },
1424
- startLine: {
1425
- type: 'number',
1426
- description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive) for single file or unified mode. MUST match filesystem-read output.',
1427
- },
1428
- endLine: {
1429
- type: 'number',
1430
- description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive) for single file or unified mode. Keep edits small (≤15 lines).',
1431
- },
1432
- newContent: {
1433
- type: 'string',
1434
- description: 'New content to replace specified lines (for single file or unified mode). ⚠️ Do NOT include line numbers. Ensure proper indentation.',
1435
- },
1436
- contextLines: {
1437
- type: 'number',
1438
- description: 'Number of context lines to show before/after edit for verification (default: 8)',
1439
- default: 8,
1440
- },
1441
- },
1442
- required: ['filePath'],
1443
- },
1444
- },
1445
- ];