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.
- package/bundle/cli.mjs +477445 -0
- package/bundle/sql-wasm.wasm +0 -0
- package/package.json +31 -26
- package/dist/agents/codebaseIndexAgent.d.ts +0 -102
- package/dist/agents/codebaseIndexAgent.js +0 -641
- package/dist/agents/codebaseReviewAgent.d.ts +0 -61
- package/dist/agents/codebaseReviewAgent.js +0 -301
- package/dist/agents/compactAgent.d.ts +0 -55
- package/dist/agents/compactAgent.js +0 -306
- package/dist/agents/promptOptimizeAgent.d.ts +0 -54
- package/dist/agents/promptOptimizeAgent.js +0 -268
- package/dist/agents/reviewAgent.d.ts +0 -50
- package/dist/agents/reviewAgent.js +0 -265
- package/dist/agents/summaryAgent.d.ts +0 -57
- package/dist/agents/summaryAgent.js +0 -260
- package/dist/api/anthropic.d.ts +0 -44
- package/dist/api/anthropic.js +0 -598
- package/dist/api/chat.d.ts +0 -73
- package/dist/api/chat.js +0 -386
- package/dist/api/embedding.d.ts +0 -34
- package/dist/api/embedding.js +0 -80
- package/dist/api/gemini.d.ts +0 -31
- package/dist/api/gemini.js +0 -445
- package/dist/api/models.d.ts +0 -15
- package/dist/api/models.js +0 -139
- package/dist/api/responses.d.ts +0 -38
- package/dist/api/responses.js +0 -515
- package/dist/api/systemPrompt.d.ts +0 -4
- package/dist/api/systemPrompt.js +0 -408
- package/dist/api/types.d.ts +0 -53
- package/dist/api/types.js +0 -4
- package/dist/app.d.ts +0 -8
- package/dist/app.js +0 -112
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -199
- package/dist/hooks/useAgentPicker.d.ts +0 -14
- package/dist/hooks/useAgentPicker.js +0 -119
- package/dist/hooks/useClipboard.d.ts +0 -4
- package/dist/hooks/useClipboard.js +0 -175
- package/dist/hooks/useCommandHandler.d.ts +0 -35
- package/dist/hooks/useCommandHandler.js +0 -346
- package/dist/hooks/useCommandPanel.d.ts +0 -17
- package/dist/hooks/useCommandPanel.js +0 -114
- package/dist/hooks/useConversation.d.ts +0 -49
- package/dist/hooks/useConversation.js +0 -1052
- package/dist/hooks/useFilePicker.d.ts +0 -18
- package/dist/hooks/useFilePicker.js +0 -224
- package/dist/hooks/useGlobalExit.d.ts +0 -5
- package/dist/hooks/useGlobalExit.js +0 -34
- package/dist/hooks/useGlobalNavigation.d.ts +0 -6
- package/dist/hooks/useGlobalNavigation.js +0 -17
- package/dist/hooks/useHistoryNavigation.d.ts +0 -35
- package/dist/hooks/useHistoryNavigation.js +0 -133
- package/dist/hooks/useInputBuffer.d.ts +0 -6
- package/dist/hooks/useInputBuffer.js +0 -45
- package/dist/hooks/useKeyboardInput.d.ts +0 -80
- package/dist/hooks/useKeyboardInput.js +0 -608
- package/dist/hooks/useSessionManagement.d.ts +0 -10
- package/dist/hooks/useSessionManagement.js +0 -43
- package/dist/hooks/useSessionSave.d.ts +0 -8
- package/dist/hooks/useSessionSave.js +0 -63
- package/dist/hooks/useSnapshotState.d.ts +0 -26
- package/dist/hooks/useSnapshotState.js +0 -28
- package/dist/hooks/useStreamingState.d.ts +0 -33
- package/dist/hooks/useStreamingState.js +0 -105
- package/dist/hooks/useTerminalFocus.d.ts +0 -28
- package/dist/hooks/useTerminalFocus.js +0 -87
- package/dist/hooks/useTerminalSize.d.ts +0 -4
- package/dist/hooks/useTerminalSize.js +0 -20
- package/dist/hooks/useTodoPicker.d.ts +0 -16
- package/dist/hooks/useTodoPicker.js +0 -94
- package/dist/hooks/useToolConfirmation.d.ts +0 -19
- package/dist/hooks/useToolConfirmation.js +0 -61
- package/dist/hooks/useVSCodeState.d.ts +0 -8
- package/dist/hooks/useVSCodeState.js +0 -81
- package/dist/i18n/I18nContext.d.ts +0 -14
- package/dist/i18n/I18nContext.js +0 -24
- package/dist/i18n/index.d.ts +0 -3
- package/dist/i18n/index.js +0 -2
- package/dist/i18n/lang/en.d.ts +0 -2
- package/dist/i18n/lang/en.js +0 -502
- package/dist/i18n/lang/es.d.ts +0 -2
- package/dist/i18n/lang/es.js +0 -502
- package/dist/i18n/lang/ja.d.ts +0 -2
- package/dist/i18n/lang/ja.js +0 -502
- package/dist/i18n/lang/ko.d.ts +0 -2
- package/dist/i18n/lang/ko.js +0 -502
- package/dist/i18n/lang/zh-TW.d.ts +0 -2
- package/dist/i18n/lang/zh-TW.js +0 -502
- package/dist/i18n/lang/zh.d.ts +0 -2
- package/dist/i18n/lang/zh.js +0 -502
- package/dist/i18n/translations.d.ts +0 -2
- package/dist/i18n/translations.js +0 -14
- package/dist/i18n/types.d.ts +0 -478
- package/dist/i18n/types.js +0 -1
- package/dist/mcp/aceCodeSearch.d.ts +0 -247
- package/dist/mcp/aceCodeSearch.js +0 -1058
- package/dist/mcp/bash.d.ts +0 -50
- package/dist/mcp/bash.js +0 -153
- package/dist/mcp/codebaseSearch.d.ts +0 -44
- package/dist/mcp/codebaseSearch.js +0 -275
- package/dist/mcp/filesystem.d.ts +0 -392
- package/dist/mcp/filesystem.js +0 -1445
- package/dist/mcp/ideDiagnostics.d.ts +0 -36
- package/dist/mcp/ideDiagnostics.js +0 -90
- package/dist/mcp/notebook.d.ts +0 -10
- package/dist/mcp/notebook.js +0 -367
- package/dist/mcp/subagent.d.ts +0 -37
- package/dist/mcp/subagent.js +0 -113
- package/dist/mcp/todo.d.ts +0 -46
- package/dist/mcp/todo.js +0 -511
- package/dist/mcp/types/aceCodeSearch.types.d.ts +0 -92
- package/dist/mcp/types/aceCodeSearch.types.js +0 -4
- package/dist/mcp/types/bash.types.d.ts +0 -13
- package/dist/mcp/types/bash.types.js +0 -4
- package/dist/mcp/types/filesystem.types.d.ts +0 -210
- package/dist/mcp/types/filesystem.types.js +0 -27
- package/dist/mcp/types/todo.types.d.ts +0 -27
- package/dist/mcp/types/todo.types.js +0 -4
- package/dist/mcp/types/websearch.types.d.ts +0 -30
- package/dist/mcp/types/websearch.types.js +0 -4
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +0 -34
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +0 -146
- package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +0 -14
- package/dist/mcp/utils/aceCodeSearch/language.utils.js +0 -418
- package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +0 -31
- package/dist/mcp/utils/aceCodeSearch/search.utils.js +0 -136
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +0 -20
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +0 -141
- package/dist/mcp/utils/bash/security.utils.d.ts +0 -20
- package/dist/mcp/utils/bash/security.utils.js +0 -34
- package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +0 -39
- package/dist/mcp/utils/filesystem/batch-operations.utils.js +0 -182
- package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +0 -18
- package/dist/mcp/utils/filesystem/code-analysis.utils.js +0 -165
- package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +0 -16
- package/dist/mcp/utils/filesystem/match-finder.utils.js +0 -85
- package/dist/mcp/utils/filesystem/office-parser.utils.d.ts +0 -43
- package/dist/mcp/utils/filesystem/office-parser.utils.js +0 -163
- package/dist/mcp/utils/filesystem/path-fixer.utils.d.ts +0 -7
- package/dist/mcp/utils/filesystem/path-fixer.utils.js +0 -60
- package/dist/mcp/utils/filesystem/similarity.utils.d.ts +0 -22
- package/dist/mcp/utils/filesystem/similarity.utils.js +0 -75
- package/dist/mcp/utils/todo/date.utils.d.ts +0 -9
- package/dist/mcp/utils/todo/date.utils.js +0 -14
- package/dist/mcp/utils/websearch/browser.utils.d.ts +0 -8
- package/dist/mcp/utils/websearch/browser.utils.js +0 -58
- package/dist/mcp/utils/websearch/text.utils.d.ts +0 -16
- package/dist/mcp/utils/websearch/text.utils.js +0 -39
- package/dist/mcp/websearch.d.ts +0 -88
- package/dist/mcp/websearch.js +0 -375
- package/dist/test/logger-test.d.ts +0 -1
- package/dist/test/logger-test.js +0 -7
- package/dist/types/index.d.ts +0 -15
- package/dist/types/index.js +0 -1
- package/dist/ui/components/AgentPickerPanel.d.ts +0 -10
- package/dist/ui/components/AgentPickerPanel.js +0 -74
- package/dist/ui/components/ChatInput.d.ts +0 -46
- package/dist/ui/components/ChatInput.js +0 -379
- package/dist/ui/components/CommandPanel.d.ts +0 -15
- package/dist/ui/components/CommandPanel.js +0 -80
- package/dist/ui/components/DiffViewer.d.ts +0 -11
- package/dist/ui/components/DiffViewer.js +0 -178
- package/dist/ui/components/FileList.d.ts +0 -15
- package/dist/ui/components/FileList.js +0 -360
- package/dist/ui/components/FileRollbackConfirmation.d.ts +0 -8
- package/dist/ui/components/FileRollbackConfirmation.js +0 -108
- package/dist/ui/components/HelpPanel.d.ts +0 -2
- package/dist/ui/components/HelpPanel.js +0 -67
- package/dist/ui/components/MCPInfoPanel.d.ts +0 -2
- package/dist/ui/components/MCPInfoPanel.js +0 -108
- package/dist/ui/components/MCPInfoScreen.d.ts +0 -7
- package/dist/ui/components/MCPInfoScreen.js +0 -115
- package/dist/ui/components/MarkdownRenderer.d.ts +0 -6
- package/dist/ui/components/MarkdownRenderer.js +0 -70
- package/dist/ui/components/Menu.d.ts +0 -17
- package/dist/ui/components/Menu.js +0 -88
- package/dist/ui/components/MessageList.d.ts +0 -56
- package/dist/ui/components/MessageList.js +0 -97
- package/dist/ui/components/PendingMessages.d.ts +0 -13
- package/dist/ui/components/PendingMessages.js +0 -29
- package/dist/ui/components/PendingToolCalls.d.ts +0 -11
- package/dist/ui/components/PendingToolCalls.js +0 -35
- package/dist/ui/components/ScrollableSelectInput.d.ts +0 -29
- package/dist/ui/components/ScrollableSelectInput.js +0 -157
- package/dist/ui/components/SessionListPanel.d.ts +0 -7
- package/dist/ui/components/SessionListPanel.js +0 -175
- package/dist/ui/components/SessionListScreen.d.ts +0 -7
- package/dist/ui/components/SessionListScreen.js +0 -217
- package/dist/ui/components/SessionListScreenWrapper.d.ts +0 -7
- package/dist/ui/components/SessionListScreenWrapper.js +0 -14
- package/dist/ui/components/ShimmerText.d.ts +0 -9
- package/dist/ui/components/ShimmerText.js +0 -30
- package/dist/ui/components/TodoPickerPanel.d.ts +0 -14
- package/dist/ui/components/TodoPickerPanel.js +0 -119
- package/dist/ui/components/TodoTree.d.ts +0 -15
- package/dist/ui/components/TodoTree.js +0 -60
- package/dist/ui/components/ToolConfirmation.d.ts +0 -21
- package/dist/ui/components/ToolConfirmation.js +0 -204
- package/dist/ui/components/ToolResultPreview.d.ts +0 -13
- package/dist/ui/components/ToolResultPreview.js +0 -337
- package/dist/ui/components/UsagePanel.d.ts +0 -2
- package/dist/ui/components/UsagePanel.js +0 -394
- package/dist/ui/contexts/ThemeContext.d.ts +0 -13
- package/dist/ui/contexts/ThemeContext.js +0 -28
- package/dist/ui/pages/ChatScreen.d.ts +0 -6
- package/dist/ui/pages/ChatScreen.js +0 -1495
- package/dist/ui/pages/CodeBaseConfigScreen.d.ts +0 -8
- package/dist/ui/pages/CodeBaseConfigScreen.js +0 -350
- package/dist/ui/pages/ConfigScreen.d.ts +0 -8
- package/dist/ui/pages/ConfigScreen.js +0 -1101
- package/dist/ui/pages/CustomHeadersScreen.d.ts +0 -6
- package/dist/ui/pages/CustomHeadersScreen.js +0 -502
- package/dist/ui/pages/HeadlessModeScreen.d.ts +0 -7
- package/dist/ui/pages/HeadlessModeScreen.js +0 -381
- package/dist/ui/pages/LanguageSettingsScreen.d.ts +0 -7
- package/dist/ui/pages/LanguageSettingsScreen.js +0 -91
- package/dist/ui/pages/MCPConfigScreen.d.ts +0 -6
- package/dist/ui/pages/MCPConfigScreen.js +0 -55
- package/dist/ui/pages/ProxyConfigScreen.d.ts +0 -8
- package/dist/ui/pages/ProxyConfigScreen.js +0 -149
- package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +0 -7
- package/dist/ui/pages/SensitiveCommandConfigScreen.js +0 -271
- package/dist/ui/pages/SubAgentConfigScreen.d.ts +0 -9
- package/dist/ui/pages/SubAgentConfigScreen.js +0 -435
- package/dist/ui/pages/SubAgentListScreen.d.ts +0 -9
- package/dist/ui/pages/SubAgentListScreen.js +0 -131
- package/dist/ui/pages/SystemPromptConfigScreen.d.ts +0 -6
- package/dist/ui/pages/SystemPromptConfigScreen.js +0 -326
- package/dist/ui/pages/ThemeSettingsScreen.d.ts +0 -7
- package/dist/ui/pages/ThemeSettingsScreen.js +0 -106
- package/dist/ui/pages/WelcomeScreen.d.ts +0 -7
- package/dist/ui/pages/WelcomeScreen.js +0 -217
- package/dist/ui/themes/index.d.ts +0 -23
- package/dist/ui/themes/index.js +0 -140
- package/dist/utils/apiConfig.d.ts +0 -126
- package/dist/utils/apiConfig.js +0 -423
- package/dist/utils/autoCompress.d.ts +0 -15
- package/dist/utils/autoCompress.js +0 -24
- package/dist/utils/chatExporter.d.ts +0 -9
- package/dist/utils/chatExporter.js +0 -118
- package/dist/utils/checkpointManager.d.ts +0 -74
- package/dist/utils/checkpointManager.js +0 -181
- package/dist/utils/codebaseConfig.d.ts +0 -16
- package/dist/utils/codebaseConfig.js +0 -67
- package/dist/utils/codebaseDatabase.d.ts +0 -102
- package/dist/utils/codebaseDatabase.js +0 -333
- package/dist/utils/codebaseSearchEvents.d.ts +0 -16
- package/dist/utils/codebaseSearchEvents.js +0 -13
- package/dist/utils/commandExecutor.d.ts +0 -13
- package/dist/utils/commandExecutor.js +0 -26
- package/dist/utils/commands/agent.d.ts +0 -2
- package/dist/utils/commands/agent.js +0 -12
- package/dist/utils/commands/clear.d.ts +0 -2
- package/dist/utils/commands/clear.js +0 -12
- package/dist/utils/commands/compact.d.ts +0 -2
- package/dist/utils/commands/compact.js +0 -12
- package/dist/utils/commands/export.d.ts +0 -2
- package/dist/utils/commands/export.js +0 -12
- package/dist/utils/commands/help.d.ts +0 -2
- package/dist/utils/commands/help.js +0 -11
- package/dist/utils/commands/home.d.ts +0 -2
- package/dist/utils/commands/home.js +0 -34
- package/dist/utils/commands/ide.d.ts +0 -2
- package/dist/utils/commands/ide.js +0 -32
- package/dist/utils/commands/init.d.ts +0 -2
- package/dist/utils/commands/init.js +0 -93
- package/dist/utils/commands/mcp.d.ts +0 -2
- package/dist/utils/commands/mcp.js +0 -12
- package/dist/utils/commands/resume.d.ts +0 -2
- package/dist/utils/commands/resume.js +0 -12
- package/dist/utils/commands/review.d.ts +0 -2
- package/dist/utils/commands/review.js +0 -81
- package/dist/utils/commands/role.d.ts +0 -2
- package/dist/utils/commands/role.js +0 -37
- package/dist/utils/commands/todoPicker.d.ts +0 -2
- package/dist/utils/commands/todoPicker.js +0 -12
- package/dist/utils/commands/usage.d.ts +0 -2
- package/dist/utils/commands/usage.js +0 -12
- package/dist/utils/commands/yolo.d.ts +0 -2
- package/dist/utils/commands/yolo.js +0 -12
- package/dist/utils/configManager.d.ts +0 -45
- package/dist/utils/configManager.js +0 -303
- package/dist/utils/contextCompressor.d.ts +0 -16
- package/dist/utils/contextCompressor.js +0 -334
- package/dist/utils/devMode.d.ts +0 -13
- package/dist/utils/devMode.js +0 -54
- package/dist/utils/escapeHandler.d.ts +0 -79
- package/dist/utils/escapeHandler.js +0 -153
- package/dist/utils/fileDialog.d.ts +0 -9
- package/dist/utils/fileDialog.js +0 -74
- package/dist/utils/fileUtils.d.ts +0 -40
- package/dist/utils/fileUtils.js +0 -185
- package/dist/utils/historyManager.d.ts +0 -45
- package/dist/utils/historyManager.js +0 -159
- package/dist/utils/incrementalSnapshot.d.ts +0 -109
- package/dist/utils/incrementalSnapshot.js +0 -383
- package/dist/utils/index.d.ts +0 -11
- package/dist/utils/index.js +0 -18
- package/dist/utils/languageConfig.d.ts +0 -21
- package/dist/utils/languageConfig.js +0 -61
- package/dist/utils/logger.d.ts +0 -37
- package/dist/utils/logger.js +0 -122
- package/dist/utils/mcpToolsManager.d.ts +0 -52
- package/dist/utils/mcpToolsManager.js +0 -878
- package/dist/utils/messageFormatter.d.ts +0 -12
- package/dist/utils/messageFormatter.js +0 -115
- package/dist/utils/notebookManager.d.ts +0 -59
- package/dist/utils/notebookManager.js +0 -213
- package/dist/utils/patch-highlight.d.ts +0 -5
- package/dist/utils/patch-highlight.js +0 -23
- package/dist/utils/processManager.d.ts +0 -27
- package/dist/utils/processManager.js +0 -75
- package/dist/utils/proxyUtils.d.ts +0 -15
- package/dist/utils/proxyUtils.js +0 -50
- package/dist/utils/resourceMonitor.d.ts +0 -65
- package/dist/utils/resourceMonitor.js +0 -175
- package/dist/utils/retryUtils.d.ts +0 -49
- package/dist/utils/retryUtils.js +0 -303
- package/dist/utils/sensitiveCommandManager.d.ts +0 -53
- package/dist/utils/sensitiveCommandManager.js +0 -308
- package/dist/utils/sessionConverter.d.ts +0 -7
- package/dist/utils/sessionConverter.js +0 -306
- package/dist/utils/sessionManager.d.ts +0 -53
- package/dist/utils/sessionManager.js +0 -371
- package/dist/utils/subAgentConfig.d.ts +0 -50
- package/dist/utils/subAgentConfig.js +0 -221
- package/dist/utils/subAgentExecutor.d.ts +0 -40
- package/dist/utils/subAgentExecutor.js +0 -434
- package/dist/utils/terminal.d.ts +0 -5
- package/dist/utils/terminal.js +0 -13
- package/dist/utils/textBuffer.d.ts +0 -99
- package/dist/utils/textBuffer.js +0 -547
- package/dist/utils/textUtils.d.ts +0 -37
- package/dist/utils/textUtils.js +0 -102
- package/dist/utils/themeConfig.d.ts +0 -21
- package/dist/utils/themeConfig.js +0 -61
- package/dist/utils/todoPreprocessor.d.ts +0 -5
- package/dist/utils/todoPreprocessor.js +0 -18
- package/dist/utils/todoScanner.d.ts +0 -8
- package/dist/utils/todoScanner.js +0 -148
- package/dist/utils/toolDisplayConfig.d.ts +0 -16
- package/dist/utils/toolDisplayConfig.js +0 -47
- package/dist/utils/toolExecutor.d.ts +0 -37
- package/dist/utils/toolExecutor.js +0 -224
- package/dist/utils/usageLogger.d.ts +0 -11
- package/dist/utils/usageLogger.js +0 -114
- package/dist/utils/vscodeConnection.d.ts +0 -76
- package/dist/utils/vscodeConnection.js +0 -430
- package/dist/utils/workspaceSnapshot.d.ts +0 -63
- package/dist/utils/workspaceSnapshot.js +0 -300
package/dist/mcp/filesystem.js
DELETED
|
@@ -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
|
-
];
|