@vybestack/llxprt-code-tools 0.10.0-nightly.260613.1adad3b34
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/LICENSE +202 -0
- package/README.md +294 -0
- package/dist/.last_build +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/src/__tests__/fixtures/filesystem-tool-fixtures.d.ts +29 -0
- package/dist/src/__tests__/fixtures/filesystem-tool-fixtures.js +30 -0
- package/dist/src/__tests__/fixtures/filesystem-tool-fixtures.js.map +1 -0
- package/dist/src/__tests__/fixtures/key-storage-fixtures.d.ts +39 -0
- package/dist/src/__tests__/fixtures/key-storage-fixtures.js +53 -0
- package/dist/src/__tests__/fixtures/key-storage-fixtures.js.map +1 -0
- package/dist/src/__tests__/fixtures/provider-formatting-fixtures.d.ts +140 -0
- package/dist/src/__tests__/fixtures/provider-formatting-fixtures.js +157 -0
- package/dist/src/__tests__/fixtures/provider-formatting-fixtures.js.map +1 -0
- package/dist/src/__tests__/red-test-helpers.d.ts +14 -0
- package/dist/src/__tests__/red-test-helpers.js +18 -0
- package/dist/src/__tests__/red-test-helpers.js.map +1 -0
- package/dist/src/formatters/IToolFormatter.d.ts +84 -0
- package/dist/src/formatters/IToolFormatter.js +6 -0
- package/dist/src/formatters/IToolFormatter.js.map +1 -0
- package/dist/src/formatters/ToolFormatter.d.ts +94 -0
- package/dist/src/formatters/ToolFormatter.js +379 -0
- package/dist/src/formatters/ToolFormatter.js.map +1 -0
- package/dist/src/formatters/ToolIdStrategy.d.ts +79 -0
- package/dist/src/formatters/ToolIdStrategy.js +173 -0
- package/dist/src/formatters/ToolIdStrategy.js.map +1 -0
- package/dist/src/formatters/doubleEscapeUtils.d.ts +46 -0
- package/dist/src/formatters/doubleEscapeUtils.js +223 -0
- package/dist/src/formatters/doubleEscapeUtils.js.map +1 -0
- package/dist/src/formatters/index.d.ts +21 -0
- package/dist/src/formatters/index.js +12 -0
- package/dist/src/formatters/index.js.map +1 -0
- package/dist/src/formatters/toolIdNormalization.d.ts +28 -0
- package/dist/src/formatters/toolIdNormalization.js +97 -0
- package/dist/src/formatters/toolIdNormalization.js.map +1 -0
- package/dist/src/formatters/toolNameUtils.d.ts +43 -0
- package/dist/src/formatters/toolNameUtils.js +143 -0
- package/dist/src/formatters/toolNameUtils.js.map +1 -0
- package/dist/src/index.d.ts +88 -0
- package/dist/src/index.js +68 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces/IAsyncTaskService.d.ts +57 -0
- package/dist/src/interfaces/IAsyncTaskService.js +6 -0
- package/dist/src/interfaces/IAsyncTaskService.js.map +1 -0
- package/dist/src/interfaces/IIdeService.d.ts +62 -0
- package/dist/src/interfaces/IIdeService.js +6 -0
- package/dist/src/interfaces/IIdeService.js.map +1 -0
- package/dist/src/interfaces/ILspService.d.ts +54 -0
- package/dist/src/interfaces/ILspService.js +6 -0
- package/dist/src/interfaces/ILspService.js.map +1 -0
- package/dist/src/interfaces/IMcpToolService.d.ts +47 -0
- package/dist/src/interfaces/IMcpToolService.js +6 -0
- package/dist/src/interfaces/IMcpToolService.js.map +1 -0
- package/dist/src/interfaces/IPromptRegistryService.d.ts +51 -0
- package/dist/src/interfaces/IPromptRegistryService.js +6 -0
- package/dist/src/interfaces/IPromptRegistryService.js.map +1 -0
- package/dist/src/interfaces/ISettingsService.d.ts +50 -0
- package/dist/src/interfaces/ISettingsService.js +6 -0
- package/dist/src/interfaces/ISettingsService.js.map +1 -0
- package/dist/src/interfaces/IShellExecutionService.d.ts +55 -0
- package/dist/src/interfaces/IShellExecutionService.js +6 -0
- package/dist/src/interfaces/IShellExecutionService.js.map +1 -0
- package/dist/src/interfaces/IShellToolHost.d.ts +176 -0
- package/dist/src/interfaces/IShellToolHost.js +6 -0
- package/dist/src/interfaces/IShellToolHost.js.map +1 -0
- package/dist/src/interfaces/ISkillService.d.ts +69 -0
- package/dist/src/interfaces/ISkillService.js +6 -0
- package/dist/src/interfaces/ISkillService.js.map +1 -0
- package/dist/src/interfaces/IStorageService.d.ts +42 -0
- package/dist/src/interfaces/IStorageService.js +6 -0
- package/dist/src/interfaces/IStorageService.js.map +1 -0
- package/dist/src/interfaces/ISubagentService.d.ts +112 -0
- package/dist/src/interfaces/ISubagentService.js +6 -0
- package/dist/src/interfaces/ISubagentService.js.map +1 -0
- package/dist/src/interfaces/ITaskToolHost.d.ts +185 -0
- package/dist/src/interfaces/ITaskToolHost.js +6 -0
- package/dist/src/interfaces/ITaskToolHost.js.map +1 -0
- package/dist/src/interfaces/ITodoService.d.ts +79 -0
- package/dist/src/interfaces/ITodoService.js +6 -0
- package/dist/src/interfaces/ITodoService.js.map +1 -0
- package/dist/src/interfaces/IToolHost.d.ts +91 -0
- package/dist/src/interfaces/IToolHost.js +6 -0
- package/dist/src/interfaces/IToolHost.js.map +1 -0
- package/dist/src/interfaces/IToolKeyStorage.d.ts +62 -0
- package/dist/src/interfaces/IToolKeyStorage.js +6 -0
- package/dist/src/interfaces/IToolKeyStorage.js.map +1 -0
- package/dist/src/interfaces/IToolMessageBus.d.ts +63 -0
- package/dist/src/interfaces/IToolMessageBus.js +6 -0
- package/dist/src/interfaces/IToolMessageBus.js.map +1 -0
- package/dist/src/interfaces/IToolRegistryHost.d.ts +43 -0
- package/dist/src/interfaces/IToolRegistryHost.js +6 -0
- package/dist/src/interfaces/IToolRegistryHost.js.map +1 -0
- package/dist/src/interfaces/IWebSearchService.d.ts +16 -0
- package/dist/src/interfaces/IWebSearchService.js +7 -0
- package/dist/src/interfaces/IWebSearchService.js.map +1 -0
- package/dist/src/interfaces/index.d.ts +33 -0
- package/dist/src/interfaces/index.js +6 -0
- package/dist/src/interfaces/index.js.map +1 -0
- package/dist/src/tools/activate-skill.d.ts +26 -0
- package/dist/src/tools/activate-skill.js +121 -0
- package/dist/src/tools/activate-skill.js.map +1 -0
- package/dist/src/tools/apply-patch.d.ts +65 -0
- package/dist/src/tools/apply-patch.js +528 -0
- package/dist/src/tools/apply-patch.js.map +1 -0
- package/dist/src/tools/ast-edit/ast-config.d.ts +67 -0
- package/dist/src/tools/ast-edit/ast-config.js +72 -0
- package/dist/src/tools/ast-edit/ast-config.js.map +1 -0
- package/dist/src/tools/ast-edit/ast-edit-invocation.d.ts +40 -0
- package/dist/src/tools/ast-edit/ast-edit-invocation.js +410 -0
- package/dist/src/tools/ast-edit/ast-edit-invocation.js.map +1 -0
- package/dist/src/tools/ast-edit/ast-query-extractor.d.ts +21 -0
- package/dist/src/tools/ast-edit/ast-query-extractor.js +178 -0
- package/dist/src/tools/ast-edit/ast-query-extractor.js.map +1 -0
- package/dist/src/tools/ast-edit/ast-read-file-invocation.d.ts +26 -0
- package/dist/src/tools/ast-edit/ast-read-file-invocation.js +149 -0
- package/dist/src/tools/ast-edit/ast-read-file-invocation.js.map +1 -0
- package/dist/src/tools/ast-edit/constants.d.ts +30 -0
- package/dist/src/tools/ast-edit/constants.js +36 -0
- package/dist/src/tools/ast-edit/constants.js.map +1 -0
- package/dist/src/tools/ast-edit/context-collector.d.ts +25 -0
- package/dist/src/tools/ast-edit/context-collector.js +115 -0
- package/dist/src/tools/ast-edit/context-collector.js.map +1 -0
- package/dist/src/tools/ast-edit/context-optimizer.d.ts +29 -0
- package/dist/src/tools/ast-edit/context-optimizer.js +86 -0
- package/dist/src/tools/ast-edit/context-optimizer.js.map +1 -0
- package/dist/src/tools/ast-edit/cross-file-analyzer.d.ts +41 -0
- package/dist/src/tools/ast-edit/cross-file-analyzer.js +294 -0
- package/dist/src/tools/ast-edit/cross-file-analyzer.js.map +1 -0
- package/dist/src/tools/ast-edit/edit-calculator.d.ts +71 -0
- package/dist/src/tools/ast-edit/edit-calculator.js +249 -0
- package/dist/src/tools/ast-edit/edit-calculator.js.map +1 -0
- package/dist/src/tools/ast-edit/edit-helpers.d.ts +22 -0
- package/dist/src/tools/ast-edit/edit-helpers.js +36 -0
- package/dist/src/tools/ast-edit/edit-helpers.js.map +1 -0
- package/dist/src/tools/ast-edit/language-analysis.d.ts +19 -0
- package/dist/src/tools/ast-edit/language-analysis.js +123 -0
- package/dist/src/tools/ast-edit/language-analysis.js.map +1 -0
- package/dist/src/tools/ast-edit/local-context-analyzer.d.ts +84 -0
- package/dist/src/tools/ast-edit/local-context-analyzer.js +267 -0
- package/dist/src/tools/ast-edit/local-context-analyzer.js.map +1 -0
- package/dist/src/tools/ast-edit/repository-context-provider.d.ts +22 -0
- package/dist/src/tools/ast-edit/repository-context-provider.js +139 -0
- package/dist/src/tools/ast-edit/repository-context-provider.js.map +1 -0
- package/dist/src/tools/ast-edit/types.d.ts +155 -0
- package/dist/src/tools/ast-edit/types.js +7 -0
- package/dist/src/tools/ast-edit/types.js.map +1 -0
- package/dist/src/tools/ast-edit/workspace-context-provider.d.ts +22 -0
- package/dist/src/tools/ast-edit/workspace-context-provider.js +39 -0
- package/dist/src/tools/ast-edit/workspace-context-provider.js.map +1 -0
- package/dist/src/tools/ast-edit.d.ts +43 -0
- package/dist/src/tools/ast-edit.js +183 -0
- package/dist/src/tools/ast-edit.js.map +1 -0
- package/dist/src/tools/ast-grep.d.ts +22 -0
- package/dist/src/tools/ast-grep.js +291 -0
- package/dist/src/tools/ast-grep.js.map +1 -0
- package/dist/src/tools/check-async-tasks.d.ts +46 -0
- package/dist/src/tools/check-async-tasks.js +241 -0
- package/dist/src/tools/check-async-tasks.js.map +1 -0
- package/dist/src/tools/codesearch.d.ts +25 -0
- package/dist/src/tools/codesearch.js +153 -0
- package/dist/src/tools/codesearch.js.map +1 -0
- package/dist/src/tools/delete_line_range.d.ts +41 -0
- package/dist/src/tools/delete_line_range.js +238 -0
- package/dist/src/tools/delete_line_range.js.map +1 -0
- package/dist/src/tools/direct-web-fetch.d.ts +22 -0
- package/dist/src/tools/direct-web-fetch.js +215 -0
- package/dist/src/tools/direct-web-fetch.js.map +1 -0
- package/dist/src/tools/edit-utils.d.ts +53 -0
- package/dist/src/tools/edit-utils.js +250 -0
- package/dist/src/tools/edit-utils.js.map +1 -0
- package/dist/src/tools/edit.d.ts +70 -0
- package/dist/src/tools/edit.js +816 -0
- package/dist/src/tools/edit.js.map +1 -0
- package/dist/src/tools/exa-web-search.d.ts +27 -0
- package/dist/src/tools/exa-web-search.js +153 -0
- package/dist/src/tools/exa-web-search.js.map +1 -0
- package/dist/src/tools/glob.d.ts +55 -0
- package/dist/src/tools/glob.js +284 -0
- package/dist/src/tools/glob.js.map +1 -0
- package/dist/src/tools/google-web-fetch.d.ts +34 -0
- package/dist/src/tools/google-web-fetch.js +417 -0
- package/dist/src/tools/google-web-fetch.js.map +1 -0
- package/dist/src/tools/google-web-search-invocation.d.ts +53 -0
- package/dist/src/tools/google-web-search-invocation.js +180 -0
- package/dist/src/tools/google-web-search-invocation.js.map +1 -0
- package/dist/src/tools/google-web-search.d.ts +16 -0
- package/dist/src/tools/google-web-search.js +34 -0
- package/dist/src/tools/google-web-search.js.map +1 -0
- package/dist/src/tools/grep.d.ts +56 -0
- package/dist/src/tools/grep.js +883 -0
- package/dist/src/tools/grep.js.map +1 -0
- package/dist/src/tools/insert_at_line.d.ts +41 -0
- package/dist/src/tools/insert_at_line.js +287 -0
- package/dist/src/tools/insert_at_line.js.map +1 -0
- package/dist/src/tools/list-subagents.d.ts +31 -0
- package/dist/src/tools/list-subagents.js +122 -0
- package/dist/src/tools/list-subagents.js.map +1 -0
- package/dist/src/tools/ls.d.ts +71 -0
- package/dist/src/tools/ls.js +238 -0
- package/dist/src/tools/ls.js.map +1 -0
- package/dist/src/tools/memoryTool.d.ts +66 -0
- package/dist/src/tools/memoryTool.js +435 -0
- package/dist/src/tools/memoryTool.js.map +1 -0
- package/dist/src/tools/modifiable-tool.d.ts +37 -0
- package/dist/src/tools/modifiable-tool.js +120 -0
- package/dist/src/tools/modifiable-tool.js.map +1 -0
- package/dist/src/tools/read-file.d.ts +49 -0
- package/dist/src/tools/read-file.js +279 -0
- package/dist/src/tools/read-file.js.map +1 -0
- package/dist/src/tools/read-many-files.d.ts +60 -0
- package/dist/src/tools/read-many-files.js +529 -0
- package/dist/src/tools/read-many-files.js.map +1 -0
- package/dist/src/tools/read_line_range.d.ts +47 -0
- package/dist/src/tools/read_line_range.js +248 -0
- package/dist/src/tools/read_line_range.js.map +1 -0
- package/dist/src/tools/ripGrep.d.ts +41 -0
- package/dist/src/tools/ripGrep.js +395 -0
- package/dist/src/tools/ripGrep.js.map +1 -0
- package/dist/src/tools/shell.d.ts +60 -0
- package/dist/src/tools/shell.js +735 -0
- package/dist/src/tools/shell.js.map +1 -0
- package/dist/src/tools/structural-analysis.d.ts +27 -0
- package/dist/src/tools/structural-analysis.js +1089 -0
- package/dist/src/tools/structural-analysis.js.map +1 -0
- package/dist/src/tools/stubs.d.ts +10 -0
- package/dist/src/tools/stubs.js +6 -0
- package/dist/src/tools/stubs.js.map +1 -0
- package/dist/src/tools/task.d.ts +41 -0
- package/dist/src/tools/task.js +195 -0
- package/dist/src/tools/task.js.map +1 -0
- package/dist/src/tools/todo-events.d.ts +22 -0
- package/dist/src/tools/todo-events.js +24 -0
- package/dist/src/tools/todo-events.js.map +1 -0
- package/dist/src/tools/todo-pause.d.ts +29 -0
- package/dist/src/tools/todo-pause.js +172 -0
- package/dist/src/tools/todo-pause.js.map +1 -0
- package/dist/src/tools/todo-read.d.ts +18 -0
- package/dist/src/tools/todo-read.js +98 -0
- package/dist/src/tools/todo-read.js.map +1 -0
- package/dist/src/tools/todo-store.d.ts +35 -0
- package/dist/src/tools/todo-store.js +124 -0
- package/dist/src/tools/todo-store.js.map +1 -0
- package/dist/src/tools/todo-write.d.ts +29 -0
- package/dist/src/tools/todo-write.js +366 -0
- package/dist/src/tools/todo-write.js.map +1 -0
- package/dist/src/tools/tool-registry.d.ts +156 -0
- package/dist/src/tools/tool-registry.js +623 -0
- package/dist/src/tools/tool-registry.js.map +1 -0
- package/dist/src/tools/tools.d.ts +403 -0
- package/dist/src/tools/tools.js +519 -0
- package/dist/src/tools/tools.js.map +1 -0
- package/dist/src/tools/write-file.d.ts +45 -0
- package/dist/src/tools/write-file.js +320 -0
- package/dist/src/tools/write-file.js.map +1 -0
- package/dist/src/types/index.d.ts +17 -0
- package/dist/src/types/index.js +9 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/provider-content-types.d.ts +56 -0
- package/dist/src/types/provider-content-types.js +6 -0
- package/dist/src/types/provider-content-types.js.map +1 -0
- package/dist/src/types/todo-schemas.d.ts +263 -0
- package/dist/src/types/todo-schemas.js +32 -0
- package/dist/src/types/todo-schemas.js.map +1 -0
- package/dist/src/types/tool-confirmation-types.d.ts +34 -0
- package/dist/src/types/tool-confirmation-types.js +29 -0
- package/dist/src/types/tool-confirmation-types.js.map +1 -0
- package/dist/src/types/tool-context.d.ts +31 -0
- package/dist/src/types/tool-context.js +6 -0
- package/dist/src/types/tool-context.js.map +1 -0
- package/dist/src/types/tool-error.d.ts +69 -0
- package/dist/src/types/tool-error.js +94 -0
- package/dist/src/types/tool-error.js.map +1 -0
- package/dist/src/types/tool-names.d.ts +57 -0
- package/dist/src/types/tool-names.js +64 -0
- package/dist/src/types/tool-names.js.map +1 -0
- package/dist/src/utils/EmojiFilter.d.ts +134 -0
- package/dist/src/utils/EmojiFilter.js +370 -0
- package/dist/src/utils/EmojiFilter.js.map +1 -0
- package/dist/src/utils/ast-grep-utils.d.ts +42 -0
- package/dist/src/utils/ast-grep-utils.js +140 -0
- package/dist/src/utils/ast-grep-utils.js.map +1 -0
- package/dist/src/utils/debugLogger.d.ts +11 -0
- package/dist/src/utils/debugLogger.js +16 -0
- package/dist/src/utils/debugLogger.js.map +1 -0
- package/dist/src/utils/diffOptions.d.ts +14 -0
- package/dist/src/utils/diffOptions.js +46 -0
- package/dist/src/utils/diffOptions.js.map +1 -0
- package/dist/src/utils/editor.d.ts +39 -0
- package/dist/src/utils/editor.js +212 -0
- package/dist/src/utils/editor.js.map +1 -0
- package/dist/src/utils/ensure-dirs.d.ts +10 -0
- package/dist/src/utils/ensure-dirs.js +16 -0
- package/dist/src/utils/ensure-dirs.js.map +1 -0
- package/dist/src/utils/errors.d.ts +59 -0
- package/dist/src/utils/errors.js +178 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/fetch.d.ts +11 -0
- package/dist/src/utils/fetch.js +74 -0
- package/dist/src/utils/fetch.js.map +1 -0
- package/dist/src/utils/fileUtils.d.ts +32 -0
- package/dist/src/utils/fileUtils.js +363 -0
- package/dist/src/utils/fileUtils.js.map +1 -0
- package/dist/src/utils/fuzzy-replacer.d.ts +61 -0
- package/dist/src/utils/fuzzy-replacer.js +492 -0
- package/dist/src/utils/fuzzy-replacer.js.map +1 -0
- package/dist/src/utils/gitLineChanges.d.ts +12 -0
- package/dist/src/utils/gitLineChanges.js +171 -0
- package/dist/src/utils/gitLineChanges.js.map +1 -0
- package/dist/src/utils/gitUtils.d.ts +6 -0
- package/dist/src/utils/gitUtils.js +31 -0
- package/dist/src/utils/gitUtils.js.map +1 -0
- package/dist/src/utils/lsp-diagnostics-helper.d.ts +27 -0
- package/dist/src/utils/lsp-diagnostics-helper.js +62 -0
- package/dist/src/utils/lsp-diagnostics-helper.js.map +1 -0
- package/dist/src/utils/mediaUtils.d.ts +38 -0
- package/dist/src/utils/mediaUtils.js +22 -0
- package/dist/src/utils/mediaUtils.js.map +1 -0
- package/dist/src/utils/pathValidation.d.ts +7 -0
- package/dist/src/utils/pathValidation.js +39 -0
- package/dist/src/utils/pathValidation.js.map +1 -0
- package/dist/src/utils/paths.d.ts +7 -0
- package/dist/src/utils/paths.js +158 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/resolveTextSearchTarget.d.ts +17 -0
- package/dist/src/utils/resolveTextSearchTarget.js +45 -0
- package/dist/src/utils/resolveTextSearchTarget.js.map +1 -0
- package/dist/src/utils/retry.d.ts +17 -0
- package/dist/src/utils/retry.js +185 -0
- package/dist/src/utils/retry.js.map +1 -0
- package/dist/src/utils/ripgrepPathResolver.d.ts +23 -0
- package/dist/src/utils/ripgrepPathResolver.js +179 -0
- package/dist/src/utils/ripgrepPathResolver.js.map +1 -0
- package/dist/src/utils/safeJsonStringify.d.ts +9 -0
- package/dist/src/utils/safeJsonStringify.js +21 -0
- package/dist/src/utils/safeJsonStringify.js.map +1 -0
- package/dist/src/utils/schemaValidator.d.ts +13 -0
- package/dist/src/utils/schemaValidator.js +275 -0
- package/dist/src/utils/schemaValidator.js.map +1 -0
- package/dist/src/utils/terminalSerializer.d.ts +33 -0
- package/dist/src/utils/terminalSerializer.js +6 -0
- package/dist/src/utils/terminalSerializer.js.map +1 -0
- package/dist/src/utils/todoContextTracker.d.ts +12 -0
- package/dist/src/utils/todoContextTracker.js +28 -0
- package/dist/src/utils/todoContextTracker.js.map +1 -0
- package/dist/src/utils/todoFormatter.d.ts +24 -0
- package/dist/src/utils/todoFormatter.js +157 -0
- package/dist/src/utils/todoFormatter.js.map +1 -0
- package/dist/src/utils/todoReminderService.d.ts +22 -0
- package/dist/src/utils/todoReminderService.js +43 -0
- package/dist/src/utils/todoReminderService.js.map +1 -0
- package/dist/src/utils/tool-key-storage-facade.d.ts +32 -0
- package/dist/src/utils/tool-key-storage-facade.js +56 -0
- package/dist/src/utils/tool-key-storage-facade.js.map +1 -0
- package/dist/src/utils/tool-key-storage-types.d.ts +56 -0
- package/dist/src/utils/tool-key-storage-types.js +56 -0
- package/dist/src/utils/tool-key-storage-types.js.map +1 -0
- package/dist/src/utils/toolOutputLimiter.d.ts +29 -0
- package/dist/src/utils/toolOutputLimiter.js +107 -0
- package/dist/src/utils/toolOutputLimiter.js.map +1 -0
- package/dist/src/utils/unicodeUtils.d.ts +55 -0
- package/dist/src/utils/unicodeUtils.js +129 -0
- package/dist/src/utils/unicodeUtils.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/* @plan PLAN-20250212-LSP.P31 */
|
|
7
|
+
/* @requirement REQ-DIAG-010, REQ-GRACE-050, REQ-GRACE-055 */
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as Diff from 'diff';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import { BaseDeclarativeTool, BaseToolInvocation, Kind, ToolConfirmationOutcome, } from './tools.js';
|
|
13
|
+
import { ToolErrorType } from '../types/tool-error.js';
|
|
14
|
+
import { makeRelative, shortenPath } from '../utils/paths.js';
|
|
15
|
+
import { isNodeError } from '../utils/errors.js';
|
|
16
|
+
import { DEFAULT_CREATE_PATCH_OPTIONS, getDiffStat, } from '../utils/diffOptions.js';
|
|
17
|
+
import { ReadFileTool } from './read-file.js';
|
|
18
|
+
import { fuzzyReplace } from '../utils/fuzzy-replacer.js';
|
|
19
|
+
import { EDIT_TOOL_NAME } from '../types/tool-names.js';
|
|
20
|
+
import { collectLspDiagnosticsBlock } from '../utils/lsp-diagnostics-helper.js';
|
|
21
|
+
import { debugLogger } from '../utils/debugLogger.js';
|
|
22
|
+
import { ensureParentDirectoriesExist } from '../utils/ensure-dirs.js';
|
|
23
|
+
import { validatePathWithinWorkspace } from '../utils/pathValidation.js';
|
|
24
|
+
import { getEmojiFilter, validateEditState, applyReplacement, countLineGuardedOccurrences, applyLineGuardedReplacement, } from './edit-utils.js';
|
|
25
|
+
function createDefaultToolHost() {
|
|
26
|
+
return {
|
|
27
|
+
getTargetDir: () => process.cwd(),
|
|
28
|
+
getWorkspaceRoots: () => [path.parse(process.cwd()).root],
|
|
29
|
+
getApprovalMode: () => 'auto',
|
|
30
|
+
setApprovalMode: () => { },
|
|
31
|
+
isInteractive: () => false,
|
|
32
|
+
hasFeatureFlag: () => false,
|
|
33
|
+
getFileService: () => ({
|
|
34
|
+
shouldGitIgnoreFile: () => false,
|
|
35
|
+
shouldLlxprtIgnoreFile: () => false,
|
|
36
|
+
filterFiles: (paths) => paths,
|
|
37
|
+
}),
|
|
38
|
+
getFileFilteringOptions: () => ({
|
|
39
|
+
respectGitIgnore: true,
|
|
40
|
+
respectLlxprtIgnore: true,
|
|
41
|
+
}),
|
|
42
|
+
getFileExclusions: () => [],
|
|
43
|
+
getReadManyFilesExclusions: () => [],
|
|
44
|
+
getFileFilteringRespectLlxprtIgnore: () => true,
|
|
45
|
+
getLlxprtIgnoreFilePath: () => null,
|
|
46
|
+
recordFileRead: () => { },
|
|
47
|
+
getLlxprtIgnorePatterns: () => [],
|
|
48
|
+
getEphemeralSettings: () => ({}),
|
|
49
|
+
getDebugMode: () => false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function getTargetDirCompat(host) {
|
|
53
|
+
return host.getTargetDir?.() ?? process.cwd();
|
|
54
|
+
}
|
|
55
|
+
function getWorkspaceRootsCompat(host) {
|
|
56
|
+
const maybeHost = host;
|
|
57
|
+
return (maybeHost.getWorkspaceContext?.().getDirectories?.() ??
|
|
58
|
+
maybeHost.getWorkspaceRoots?.() ?? [
|
|
59
|
+
maybeHost.getTargetDir?.() ?? process.cwd(),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
function toIdeConnectionStatus(status) {
|
|
63
|
+
if (typeof status === 'string') {
|
|
64
|
+
return status === 'connected' || status === 'connecting'
|
|
65
|
+
? status
|
|
66
|
+
: 'disconnected';
|
|
67
|
+
}
|
|
68
|
+
if (typeof status === 'object' && status !== null && 'status' in status) {
|
|
69
|
+
return toIdeConnectionStatus(status.status);
|
|
70
|
+
}
|
|
71
|
+
return 'disconnected';
|
|
72
|
+
}
|
|
73
|
+
function hasMessageBusShape(value) {
|
|
74
|
+
return (typeof value === 'object' &&
|
|
75
|
+
value !== null &&
|
|
76
|
+
('requestConfirmation' in value ||
|
|
77
|
+
'publishPolicyUpdate' in value ||
|
|
78
|
+
'publish' in value ||
|
|
79
|
+
'subscribe' in value));
|
|
80
|
+
}
|
|
81
|
+
function hasIdeServiceShape(value) {
|
|
82
|
+
return (typeof value === 'object' &&
|
|
83
|
+
value !== null &&
|
|
84
|
+
('applyDiff' in value ||
|
|
85
|
+
'openDiff' in value ||
|
|
86
|
+
'getConnectionStatus' in value));
|
|
87
|
+
}
|
|
88
|
+
function hasLspServiceShape(value) {
|
|
89
|
+
return (typeof value === 'object' &&
|
|
90
|
+
value !== null &&
|
|
91
|
+
('waitForDiagnostics' in value ||
|
|
92
|
+
'getDiagnostics' in value ||
|
|
93
|
+
'getLspConfig' in value));
|
|
94
|
+
}
|
|
95
|
+
function getLegacyIdeService(host) {
|
|
96
|
+
const maybeHost = host;
|
|
97
|
+
if (typeof maybeHost.getIdeMode !== 'function' ||
|
|
98
|
+
typeof maybeHost.getIdeClient !== 'function') {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const getLegacyIdeClient = () => {
|
|
102
|
+
if (maybeHost.getIdeMode?.() !== true) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const ideClient = maybeHost.getIdeClient?.();
|
|
106
|
+
if (typeof ideClient !== 'object' ||
|
|
107
|
+
ideClient === null ||
|
|
108
|
+
!('openDiff' in ideClient)) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return ideClient;
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
applyDiff: async ({ filePath, diff }) => {
|
|
115
|
+
const legacyIdeClient = getLegacyIdeClient();
|
|
116
|
+
if (legacyIdeClient?.openDiff === undefined) {
|
|
117
|
+
return { status: 'rejected', content: undefined };
|
|
118
|
+
}
|
|
119
|
+
const result = await legacyIdeClient.openDiff(filePath, diff);
|
|
120
|
+
return result.status === 'accepted'
|
|
121
|
+
? { status: 'accepted', content: result.content }
|
|
122
|
+
: { status: 'rejected', content: undefined };
|
|
123
|
+
},
|
|
124
|
+
getConnectionStatus: () => toIdeConnectionStatus(getLegacyIdeClient()?.getConnectionStatus?.()),
|
|
125
|
+
openDiff: async ({ filePath, newContent }) => {
|
|
126
|
+
await getLegacyIdeClient()?.openDiff?.(filePath, newContent);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function getLegacyLspService(host) {
|
|
131
|
+
const maybeHost = host;
|
|
132
|
+
const lspClient = maybeHost.getLspServiceClient?.();
|
|
133
|
+
if (typeof lspClient !== 'object' || lspClient === null) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
getDiagnostics: () => [],
|
|
138
|
+
waitForDiagnostics: async (filePath, _timeout) => {
|
|
139
|
+
const isAlive = lspClient.isAlive?.();
|
|
140
|
+
if (isAlive !== true) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const checkFile = lspClient.checkFile;
|
|
144
|
+
if (typeof checkFile !== 'function') {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
const diagnostics = await checkFile.call(lspClient, filePath);
|
|
148
|
+
return Array.isArray(diagnostics) ? diagnostics : [];
|
|
149
|
+
},
|
|
150
|
+
getLspConfig: () => maybeHost.getLspConfig?.(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export { applyReplacement, countLineGuardedOccurrences, applyLineGuardedReplacement, } from './edit-utils.js';
|
|
154
|
+
class EditToolInvocation extends BaseToolInvocation {
|
|
155
|
+
host;
|
|
156
|
+
ideService;
|
|
157
|
+
lspService;
|
|
158
|
+
constructor(host, ideService, lspService, params, messageBus, toolName, displayName) {
|
|
159
|
+
super(params, messageBus, toolName, displayName);
|
|
160
|
+
this.host = host;
|
|
161
|
+
this.ideService = ideService;
|
|
162
|
+
this.lspService = lspService;
|
|
163
|
+
}
|
|
164
|
+
getToolName() {
|
|
165
|
+
return EditTool.Name;
|
|
166
|
+
}
|
|
167
|
+
getFilePath() {
|
|
168
|
+
// Use absolute_path if provided, otherwise fall back to file_path
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: empty string paths are invalid, fall back to file_path
|
|
170
|
+
return this.params.absolute_path || this.params.file_path || '';
|
|
171
|
+
}
|
|
172
|
+
toolLocations() {
|
|
173
|
+
return [{ path: this.getFilePath() }];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Counts occurrences of oldString in content, trying fuzzy then strict.
|
|
177
|
+
*/
|
|
178
|
+
countOccurrences(currentContent, finalOldString, finalNewString, expectedReplacements, replaceLine, filePath) {
|
|
179
|
+
if (finalOldString === '') {
|
|
180
|
+
return { occurrences: 0, error: undefined };
|
|
181
|
+
}
|
|
182
|
+
if (replaceLine !== undefined && replaceLine > 0) {
|
|
183
|
+
const lines = currentContent.split('\n');
|
|
184
|
+
if (replaceLine > lines.length) {
|
|
185
|
+
return {
|
|
186
|
+
occurrences: 0,
|
|
187
|
+
error: {
|
|
188
|
+
display: `Failed to edit: replaceBeginLineNumber is out of range.`,
|
|
189
|
+
raw: `Failed to edit: replaceBeginLineNumber=${replaceLine} is out of range for ${filePath} (total lines: ${lines.length}). No edits made.`,
|
|
190
|
+
type: ToolErrorType.INVALID_TOOL_PARAMS,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const count = countLineGuardedOccurrences(currentContent, finalOldString, replaceLine);
|
|
195
|
+
return { occurrences: count, error: undefined };
|
|
196
|
+
}
|
|
197
|
+
const fuzzyResult = fuzzyReplace(currentContent, finalOldString, finalNewString, expectedReplacements > 1);
|
|
198
|
+
if (fuzzyResult) {
|
|
199
|
+
return { occurrences: fuzzyResult.occurrences, error: undefined };
|
|
200
|
+
}
|
|
201
|
+
let count = 0;
|
|
202
|
+
let pos = currentContent.indexOf(finalOldString);
|
|
203
|
+
while (pos !== -1) {
|
|
204
|
+
count++;
|
|
205
|
+
pos = currentContent.indexOf(finalOldString, pos + finalOldString.length);
|
|
206
|
+
}
|
|
207
|
+
return { occurrences: count, error: undefined };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Validates the edit parameters after reading file content and builds the
|
|
211
|
+
* appropriate error object if any validation fails.
|
|
212
|
+
*/
|
|
213
|
+
validateEditState(filteredParams, currentContent, fileExists, filePath, occurrences, expectedReplacements, finalOldString, finalNewString) {
|
|
214
|
+
return validateEditState(filteredParams, currentContent, fileExists, filePath, occurrences, expectedReplacements, finalOldString, finalNewString);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Applies the replacement to produce newContent, handling replaceLine.
|
|
218
|
+
*/
|
|
219
|
+
computeNewContent(currentContent, fileExists, isNewFile, filteredParams, finalOldString, finalNewString, expectedReplacements, filePath) {
|
|
220
|
+
const replaceLine = filteredParams.replaceBeginLineNumber;
|
|
221
|
+
if (fileExists &&
|
|
222
|
+
replaceLine !== undefined &&
|
|
223
|
+
replaceLine > 0 &&
|
|
224
|
+
currentContent !== null) {
|
|
225
|
+
const lines = currentContent.split('\n');
|
|
226
|
+
if (replaceLine > lines.length) {
|
|
227
|
+
return {
|
|
228
|
+
newContent: currentContent,
|
|
229
|
+
error: {
|
|
230
|
+
display: `Failed to edit: replaceBeginLineNumber is out of range.`,
|
|
231
|
+
raw: `Failed to edit: replaceBeginLineNumber=${replaceLine} is out of range for ${filePath} (total lines: ${lines.length}). No edits made.`,
|
|
232
|
+
type: ToolErrorType.INVALID_TOOL_PARAMS,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const result = applyLineGuardedReplacement(currentContent, finalOldString, finalNewString, expectedReplacements, replaceLine);
|
|
237
|
+
return {
|
|
238
|
+
newContent: result,
|
|
239
|
+
error: undefined,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const newContent = applyReplacement(currentContent, finalOldString, finalNewString, isNewFile, expectedReplacements);
|
|
243
|
+
return { newContent, error: undefined };
|
|
244
|
+
}
|
|
245
|
+
async readTextFile(filePath) {
|
|
246
|
+
const fileSystemService = this.host.getFileSystemService?.();
|
|
247
|
+
if (fileSystemService !== undefined) {
|
|
248
|
+
return fileSystemService.readTextFile(filePath);
|
|
249
|
+
}
|
|
250
|
+
return fs.readFile(filePath, 'utf8');
|
|
251
|
+
}
|
|
252
|
+
async writeTextFile(filePath, content) {
|
|
253
|
+
const fileSystemService = this.host.getFileSystemService?.();
|
|
254
|
+
if (fileSystemService !== undefined) {
|
|
255
|
+
await fileSystemService.writeTextFile(filePath, content);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Reads the current file content, handling ENOENT for new files.
|
|
262
|
+
*/
|
|
263
|
+
async readFileState(filePath) {
|
|
264
|
+
try {
|
|
265
|
+
let currentContent = await this.readTextFile(filePath);
|
|
266
|
+
currentContent = currentContent.replace(/\r\n/g, '\n');
|
|
267
|
+
return { currentContent, fileExists: true };
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
if (!isNodeError(err) || err.code !== 'ENOENT') {
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
return { currentContent: null, fileExists: false };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Reads file state and validates the edit, returning resolved values.
|
|
278
|
+
*/
|
|
279
|
+
async resolveFileEditState(filteredParams, filePath, expectedReplacements) {
|
|
280
|
+
const { currentContent, fileExists } = await this.readFileState(filePath);
|
|
281
|
+
let isNewFile = false;
|
|
282
|
+
const finalOldString = filteredParams.old_string;
|
|
283
|
+
const finalNewString = filteredParams.new_string;
|
|
284
|
+
let occurrences = 0;
|
|
285
|
+
let error = undefined;
|
|
286
|
+
if (filteredParams.old_string === '' && !fileExists) {
|
|
287
|
+
isNewFile = true;
|
|
288
|
+
}
|
|
289
|
+
else if (!fileExists) {
|
|
290
|
+
error = {
|
|
291
|
+
display: `File not found. Cannot apply edit. Use an empty old_string to create a new file.`,
|
|
292
|
+
raw: `File not found: ${filePath}`,
|
|
293
|
+
type: ToolErrorType.FILE_NOT_FOUND,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
else if (currentContent !== null) {
|
|
297
|
+
const countResult = this.countOccurrences(currentContent, finalOldString, finalNewString, expectedReplacements, filteredParams.replaceBeginLineNumber, filePath);
|
|
298
|
+
occurrences = countResult.occurrences;
|
|
299
|
+
error = countResult.error;
|
|
300
|
+
error ??= this.validateEditState(filteredParams, currentContent, fileExists, filePath, occurrences, expectedReplacements, finalOldString, finalNewString);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
error = {
|
|
304
|
+
display: `Failed to read content of file.`,
|
|
305
|
+
raw: `Failed to read content of existing file: ${filePath}`,
|
|
306
|
+
type: ToolErrorType.READ_CONTENT_FAILURE,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
currentContent,
|
|
311
|
+
fileExists,
|
|
312
|
+
isNewFile,
|
|
313
|
+
finalOldString,
|
|
314
|
+
finalNewString,
|
|
315
|
+
occurrences,
|
|
316
|
+
error,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Calculates the potential outcome of an edit operation.
|
|
321
|
+
* @param params Parameters for the edit operation
|
|
322
|
+
* @returns An object describing the potential edit outcome
|
|
323
|
+
* @throws File system errors if reading the file fails unexpectedly (e.g., permissions)
|
|
324
|
+
*/
|
|
325
|
+
async calculateEdit(params, _abortSignal) {
|
|
326
|
+
// Apply emoji filtering to edit content
|
|
327
|
+
// NOTE: old_string is NOT filtered because it needs to match existing content exactly
|
|
328
|
+
// Only new_string is filtered to remove emojis from the replacement text
|
|
329
|
+
const filter = getEmojiFilter(this.host);
|
|
330
|
+
const newStringResult = filter.filterFileContent(params.new_string, 'edit');
|
|
331
|
+
// Handle blocking in error mode (only check new_string, not old_string)
|
|
332
|
+
if (newStringResult.blocked) {
|
|
333
|
+
return {
|
|
334
|
+
currentContent: null,
|
|
335
|
+
newContent: '',
|
|
336
|
+
occurrences: 0,
|
|
337
|
+
error: {
|
|
338
|
+
display: 'Cannot edit files with emojis in content',
|
|
339
|
+
raw: 'Emoji filtering blocked the edit operation',
|
|
340
|
+
type: ToolErrorType.INVALID_TOOL_PARAMS,
|
|
341
|
+
},
|
|
342
|
+
isNewFile: false,
|
|
343
|
+
filterResult: newStringResult,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// Use filtered content for the edit (only filter new_string)
|
|
347
|
+
const filteredParams = {
|
|
348
|
+
...params,
|
|
349
|
+
new_string: newStringResult.filtered,
|
|
350
|
+
};
|
|
351
|
+
const expectedReplacements = filteredParams.expected_replacements ?? 1;
|
|
352
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: empty string paths are invalid, fall back to file_path
|
|
353
|
+
const filePath = params.absolute_path || params.file_path || '';
|
|
354
|
+
const { currentContent, fileExists, isNewFile, finalOldString, finalNewString, occurrences, error, } = await this.resolveFileEditState(filteredParams, filePath, expectedReplacements);
|
|
355
|
+
let newContent;
|
|
356
|
+
let resolvedError = error;
|
|
357
|
+
if (!resolvedError) {
|
|
358
|
+
const contentResult = this.computeNewContent(currentContent, fileExists, isNewFile, filteredParams, finalOldString, finalNewString, expectedReplacements, filePath);
|
|
359
|
+
newContent = contentResult.newContent;
|
|
360
|
+
resolvedError = contentResult.error;
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
newContent = currentContent ?? '';
|
|
364
|
+
}
|
|
365
|
+
if (!resolvedError && fileExists && currentContent === newContent) {
|
|
366
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: empty string paths are invalid, fall back to file_path
|
|
367
|
+
const fp = params.absolute_path || params.file_path || '';
|
|
368
|
+
resolvedError = {
|
|
369
|
+
display: 'No changes to apply. The new content is identical to the current content.',
|
|
370
|
+
raw: `No changes to apply. The new content is identical to the current content in file: ${fp}`,
|
|
371
|
+
type: ToolErrorType.EDIT_NO_CHANGE,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
currentContent,
|
|
376
|
+
newContent,
|
|
377
|
+
occurrences,
|
|
378
|
+
error: resolvedError,
|
|
379
|
+
isNewFile,
|
|
380
|
+
filterResult: newStringResult,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Returns confirmation details for this edit operation.
|
|
385
|
+
* Called by getMessageBusDecision before surfacing operations to the policy engine/message bus.
|
|
386
|
+
*/
|
|
387
|
+
getConfirmationDetails() {
|
|
388
|
+
// This is a synchronous method, so we can't calculate the edit here
|
|
389
|
+
// Instead, we'll need to handle confirmation in shouldConfirmExecute, which
|
|
390
|
+
// is invoked when the scheduler needs the diff payload for ASK_USER flows.
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Handles the confirmation prompt for the Edit tool in the CLI.
|
|
395
|
+
* It needs to calculate the diff to show the user.
|
|
396
|
+
*/
|
|
397
|
+
async shouldConfirmExecute(abortSignal) {
|
|
398
|
+
const approvalMode = this.host.getApprovalMode();
|
|
399
|
+
if (approvalMode === 'auto' || approvalMode === 'yolo') {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
let editData;
|
|
403
|
+
try {
|
|
404
|
+
editData = await this.calculateEdit(this.params, abortSignal);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
if (abortSignal.aborted) {
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
411
|
+
debugLogger.log(`Error preparing edit: ${errorMsg}`);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
if (editData.error) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
// NOTE: Emoji filtering was already applied to new_string in calculateEdit()
|
|
418
|
+
// We should NOT filter the entire file content here
|
|
419
|
+
const filteredNewContent = editData.newContent;
|
|
420
|
+
// Also filter the original new_string parameter for use in onConfirm
|
|
421
|
+
const filter = getEmojiFilter(this.host);
|
|
422
|
+
const filteredNewStringParam = filter.filterFileContent(this.params.new_string, 'edit');
|
|
423
|
+
const filteredNewString = typeof filteredNewStringParam.filtered === 'string'
|
|
424
|
+
? filteredNewStringParam.filtered
|
|
425
|
+
: this.params.new_string;
|
|
426
|
+
const filePath = this.getFilePath();
|
|
427
|
+
const fileName = path.basename(filePath);
|
|
428
|
+
const fileDiff = Diff.createPatch(fileName, editData.currentContent ?? '', filteredNewContent, 'Current', 'Proposed', DEFAULT_CREATE_PATCH_OPTIONS);
|
|
429
|
+
const ideConfirmation = this.ideService !== undefined &&
|
|
430
|
+
toIdeConnectionStatus(this.ideService.getConnectionStatus()) ===
|
|
431
|
+
'connected'
|
|
432
|
+
? this.ideService.applyDiff({ filePath, diff: filteredNewContent })
|
|
433
|
+
: undefined;
|
|
434
|
+
const confirmationDetails = {
|
|
435
|
+
type: 'edit',
|
|
436
|
+
title: `Confirm Edit: ${shortenPath(makeRelative(filePath, getTargetDirCompat(this.host)))}`,
|
|
437
|
+
fileName,
|
|
438
|
+
filePath,
|
|
439
|
+
fileDiff,
|
|
440
|
+
originalContent: editData.currentContent,
|
|
441
|
+
newContent: filteredNewContent,
|
|
442
|
+
onConfirm: async (outcome) => {
|
|
443
|
+
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
|
444
|
+
// No need to publish a policy update as the default policy for
|
|
445
|
+
// AUTO_EDIT already reflects always approving edit.
|
|
446
|
+
const legacyAutoEdit = this.host
|
|
447
|
+
.constructor?.name !== 'CoreToolHostAdapter';
|
|
448
|
+
this.host.setApprovalMode(legacyAutoEdit ? 'autoEdit' : 'auto');
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
await this.publishPolicyUpdate(outcome);
|
|
452
|
+
}
|
|
453
|
+
if (ideConfirmation) {
|
|
454
|
+
const result = await ideConfirmation;
|
|
455
|
+
if (result.status === 'accepted' && result.content) {
|
|
456
|
+
// Task(chrstn): See https://github.com/google-gemini/gemini-cli/pull/5618#discussion_r2255413084
|
|
457
|
+
// for info on a possible race condition where the file is modified on disk while being edited.
|
|
458
|
+
// FIX: IDE confirmation is for visual review only
|
|
459
|
+
// The IDE returns the entire file content, not just the replacement text
|
|
460
|
+
// We should use our original calculated replacement, not the IDE's full file content
|
|
461
|
+
// Otherwise we'd replace a small string with the entire file, causing duplication
|
|
462
|
+
// Use the filtered version of the original new_string parameter
|
|
463
|
+
this.params.new_string = filteredNewString;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
// DON'T modify params - they need to stay as the original strings
|
|
468
|
+
// The filtering has already been applied in calculateEdit()
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
ideConfirmation,
|
|
472
|
+
};
|
|
473
|
+
return confirmationDetails;
|
|
474
|
+
}
|
|
475
|
+
getDescription() {
|
|
476
|
+
const filePath = this.getFilePath();
|
|
477
|
+
const relativePath = makeRelative(filePath, getTargetDirCompat(this.host));
|
|
478
|
+
if (this.params.old_string === '') {
|
|
479
|
+
return `Create ${shortenPath(relativePath)}`;
|
|
480
|
+
}
|
|
481
|
+
const oldStringSnippet = this.params.old_string.split('\n')[0].substring(0, 30) +
|
|
482
|
+
(this.params.old_string.length > 30 ? '...' : '');
|
|
483
|
+
const newStringSnippet = this.params.new_string.split('\n')[0].substring(0, 30) +
|
|
484
|
+
(this.params.new_string.length > 30 ? '...' : '');
|
|
485
|
+
if (this.params.old_string === this.params.new_string) {
|
|
486
|
+
return `No file changes to ${shortenPath(relativePath)}`;
|
|
487
|
+
}
|
|
488
|
+
return `${shortenPath(relativePath)}: ${oldStringSnippet} => ${newStringSnippet}`;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Tracks git stats for the edit if logging is enabled.
|
|
492
|
+
*/
|
|
493
|
+
async trackGitStats(filePath, currentContent, newContent) {
|
|
494
|
+
if (this.host.getConversationLoggingEnabled?.() !== true)
|
|
495
|
+
return null;
|
|
496
|
+
const gitStatsService = this.host.getGitStatsService?.();
|
|
497
|
+
if (!gitStatsService)
|
|
498
|
+
return null;
|
|
499
|
+
try {
|
|
500
|
+
return await gitStatsService.trackFileEdit(filePath,
|
|
501
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: undefined currentContent should default to empty
|
|
502
|
+
currentContent || '', newContent);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
debugLogger.warn('Failed to track git stats:', error);
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Builds the LLM success message parts for a successful edit.
|
|
511
|
+
*/
|
|
512
|
+
buildSuccessMessage(editData, filePath) {
|
|
513
|
+
const parts = [
|
|
514
|
+
editData.isNewFile
|
|
515
|
+
? `Created new file: ${filePath} with provided content.`
|
|
516
|
+
: `Successfully modified file: ${filePath} (${editData.occurrences} replacements).`,
|
|
517
|
+
];
|
|
518
|
+
if (this.params.modified_by_user === true) {
|
|
519
|
+
parts.push(`User modified the \`new_string\` content to be: ${this.params.new_string}.`);
|
|
520
|
+
}
|
|
521
|
+
if (editData.filterResult?.systemFeedback) {
|
|
522
|
+
parts.push(`\n\n<system-reminder>\n${editData.filterResult.systemFeedback}\n</system-reminder>`);
|
|
523
|
+
}
|
|
524
|
+
return parts;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Appends LSP diagnostics to the message parts.
|
|
528
|
+
*/
|
|
529
|
+
async appendDiagnostics(llmParts, filePath) {
|
|
530
|
+
try {
|
|
531
|
+
const diagBlock = this.lspService === undefined
|
|
532
|
+
? null
|
|
533
|
+
: await collectLspDiagnosticsBlock(this.lspService, this.host, filePath);
|
|
534
|
+
if (diagBlock) {
|
|
535
|
+
llmParts.push(diagBlock);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
// LSP failure must never fail the edit (REQ-GRACE-050, REQ-GRACE-055)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Builds the ToolResult for a successful write, including diff, diagnostics,
|
|
544
|
+
* and optional git-stats metadata.
|
|
545
|
+
*/
|
|
546
|
+
async buildWriteResult(editData, filePath) {
|
|
547
|
+
const gitStats = await this.trackGitStats(filePath, editData.currentContent, editData.newContent);
|
|
548
|
+
const fileName = path.basename(filePath);
|
|
549
|
+
const originallyProposedContent = this.params.ai_proposed_content ?? editData.newContent;
|
|
550
|
+
const diffStat = getDiffStat(fileName, editData.currentContent ?? '', originallyProposedContent, editData.newContent);
|
|
551
|
+
const fileDiff = Diff.createPatch(fileName, editData.currentContent ?? '', editData.newContent, 'Current', 'Proposed', DEFAULT_CREATE_PATCH_OPTIONS);
|
|
552
|
+
const displayResult = {
|
|
553
|
+
fileDiff,
|
|
554
|
+
fileName,
|
|
555
|
+
filePath: this.params.file_path,
|
|
556
|
+
originalContent: editData.currentContent,
|
|
557
|
+
newContent: editData.newContent,
|
|
558
|
+
diffStat,
|
|
559
|
+
isNewFile: editData.isNewFile,
|
|
560
|
+
};
|
|
561
|
+
const llmSuccessMessageParts = this.buildSuccessMessage(editData, filePath);
|
|
562
|
+
// @plan PLAN-20250212-LSP.P31
|
|
563
|
+
// @requirement REQ-DIAG-010
|
|
564
|
+
await this.appendDiagnostics(llmSuccessMessageParts, filePath);
|
|
565
|
+
const result = {
|
|
566
|
+
llmContent: llmSuccessMessageParts.join('\n\n'),
|
|
567
|
+
returnDisplay: displayResult,
|
|
568
|
+
};
|
|
569
|
+
if (gitStats != null) {
|
|
570
|
+
result.metadata = {
|
|
571
|
+
...result.metadata,
|
|
572
|
+
gitStats,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Executes the edit operation with the given parameters.
|
|
579
|
+
* @param params Parameters for the edit operation
|
|
580
|
+
* @returns Result of the edit operation
|
|
581
|
+
*/
|
|
582
|
+
async execute(signal) {
|
|
583
|
+
let editData;
|
|
584
|
+
try {
|
|
585
|
+
editData = await this.calculateEdit(this.params, signal);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (signal.aborted) {
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
592
|
+
return {
|
|
593
|
+
llmContent: `Error preparing edit: ${errorMsg}`,
|
|
594
|
+
returnDisplay: `Error preparing edit: ${errorMsg}`,
|
|
595
|
+
error: {
|
|
596
|
+
message: errorMsg,
|
|
597
|
+
type: ToolErrorType.EDIT_PREPARATION_FAILURE,
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (editData.error) {
|
|
602
|
+
return {
|
|
603
|
+
llmContent: editData.error.raw,
|
|
604
|
+
returnDisplay: `Error: ${editData.error.display}`,
|
|
605
|
+
error: {
|
|
606
|
+
message: editData.error.raw,
|
|
607
|
+
type: editData.error.type,
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const filePath = this.getFilePath();
|
|
612
|
+
try {
|
|
613
|
+
await ensureParentDirectoriesExist(filePath);
|
|
614
|
+
await this.writeTextFile(filePath, editData.newContent);
|
|
615
|
+
return await this.buildWriteResult(editData, filePath);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
619
|
+
return {
|
|
620
|
+
llmContent: `Error executing edit: ${errorMsg}`,
|
|
621
|
+
returnDisplay: `Error writing file: ${errorMsg}`,
|
|
622
|
+
error: {
|
|
623
|
+
message: errorMsg,
|
|
624
|
+
type: ToolErrorType.FILE_WRITE_FAILURE,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Implementation of the Edit tool logic
|
|
632
|
+
*/
|
|
633
|
+
export class EditTool extends BaseDeclarativeTool {
|
|
634
|
+
host;
|
|
635
|
+
static Name = EDIT_TOOL_NAME;
|
|
636
|
+
ideService;
|
|
637
|
+
lspService;
|
|
638
|
+
constructor(host = createDefaultToolHost(), messageBusOrIdeService, ideServiceOrLspService, lspService) {
|
|
639
|
+
const secondArgumentIsMessageBus = hasMessageBusShape(messageBusOrIdeService);
|
|
640
|
+
const explicitIdeService = secondArgumentIsMessageBus
|
|
641
|
+
? ideServiceOrLspService
|
|
642
|
+
: messageBusOrIdeService;
|
|
643
|
+
const ideService = hasIdeServiceShape(explicitIdeService)
|
|
644
|
+
? explicitIdeService
|
|
645
|
+
: getLegacyIdeService(host);
|
|
646
|
+
const messageBus = secondArgumentIsMessageBus
|
|
647
|
+
? messageBusOrIdeService
|
|
648
|
+
: undefined;
|
|
649
|
+
const explicitLspService = secondArgumentIsMessageBus
|
|
650
|
+
? lspService
|
|
651
|
+
: ideServiceOrLspService;
|
|
652
|
+
const resolvedLspService = hasLspServiceShape(explicitLspService)
|
|
653
|
+
? explicitLspService
|
|
654
|
+
: getLegacyLspService(host);
|
|
655
|
+
super(EditTool.Name, 'Edit', `Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when \`expected_replacements\` is specified. This tool requires providing significant context around the change to ensure precise targeting. Always use the ${ReadFileTool.Name} tool to examine the file's current content before attempting a text replacement.
|
|
656
|
+
|
|
657
|
+
The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response.
|
|
658
|
+
|
|
659
|
+
Expectation for required parameters:
|
|
660
|
+
1. \`file_path\` MUST be an absolute path; otherwise an error will be thrown.
|
|
661
|
+
2. \`old_string\` MUST be the exact literal text to replace (including all whitespace, indentation, newlines, and surrounding code etc.).
|
|
662
|
+
3. \`new_string\` MUST be the exact literal text to replace \`old_string\` with (also including all whitespace, indentation, newlines, and surrounding code etc.). Ensure the resulting code is correct and idiomatic.
|
|
663
|
+
4. NEVER escape \`old_string\` or \`new_string\`, that would break the exact literal text requirement.
|
|
664
|
+
5. If you do not provide \`replaceBeginLineNumber\` and the same text appears multiple times in the file, the tool will return an error instead of applying an ambiguous change.
|
|
665
|
+
**Multiple replacements:** Set \`expected_replacements\` to the number of occurrences you want to replace. The tool will replace ALL occurrences that match \`old_string\` exactly. Ensure the number of replacements matches your expectation.`, Kind.Edit, {
|
|
666
|
+
properties: {
|
|
667
|
+
absolute_path: {
|
|
668
|
+
description: process.platform === 'win32'
|
|
669
|
+
? "The absolute path to the file to modify (e.g., 'C:\\Users\\project\\file.txt'). Must be an absolute path."
|
|
670
|
+
: "The absolute path to the file to modify (e.g., '/home/user/project/file.txt'). Must start with '/'.",
|
|
671
|
+
type: 'string',
|
|
672
|
+
},
|
|
673
|
+
file_path: {
|
|
674
|
+
description: 'Alternative parameter name for absolute_path (for backward compatibility). The absolute path to the file to modify.',
|
|
675
|
+
type: 'string',
|
|
676
|
+
},
|
|
677
|
+
old_string: {
|
|
678
|
+
description: 'The exact literal text to replace, preferably unescaped. For single replacements (default), include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. For multiple replacements, specify expected_replacements parameter. If this string is not the exact literal text (i.e. you escaped it) or does not match exactly, the tool will fail.',
|
|
679
|
+
type: 'string',
|
|
680
|
+
},
|
|
681
|
+
new_string: {
|
|
682
|
+
description: 'The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic.',
|
|
683
|
+
type: 'string',
|
|
684
|
+
},
|
|
685
|
+
expected_replacements: {
|
|
686
|
+
type: 'number',
|
|
687
|
+
description: 'Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.',
|
|
688
|
+
minimum: 1,
|
|
689
|
+
},
|
|
690
|
+
replaceBeginLineNumber: {
|
|
691
|
+
type: 'number',
|
|
692
|
+
description: 'Optional 1-based line number where the replacement should begin. Strongly recommended to always set this to guard against misinterpreting the file structure, especially when similar text appears multiple times.',
|
|
693
|
+
minimum: 1,
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
required: ['old_string', 'new_string'],
|
|
697
|
+
type: 'object',
|
|
698
|
+
}, false, false, messageBus);
|
|
699
|
+
this.host = host;
|
|
700
|
+
this.ideService = ideService;
|
|
701
|
+
this.lspService = resolvedLspService;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Validates the parameters for the Edit tool
|
|
705
|
+
* @param params Parameters to validate
|
|
706
|
+
* @returns Error message string or null if valid
|
|
707
|
+
*/
|
|
708
|
+
validateToolParamValues(params) {
|
|
709
|
+
// Accept either absolute_path or file_path
|
|
710
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: empty string paths are invalid
|
|
711
|
+
const filePath = params.absolute_path || params.file_path || '';
|
|
712
|
+
if (filePath.trim() === '') {
|
|
713
|
+
return "Either 'absolute_path' or 'file_path' parameter must be provided and non-empty.";
|
|
714
|
+
}
|
|
715
|
+
if (!path.isAbsolute(filePath)) {
|
|
716
|
+
return `File path must be absolute: ${filePath}`;
|
|
717
|
+
}
|
|
718
|
+
const pathError = validatePathWithinWorkspace(getWorkspaceRootsCompat(this.host), filePath);
|
|
719
|
+
if (pathError) {
|
|
720
|
+
return pathError;
|
|
721
|
+
}
|
|
722
|
+
// Validate that empty old_string with multiple replacements is not allowed
|
|
723
|
+
const expectedReplacements = params.expected_replacements ?? 1;
|
|
724
|
+
if (params.old_string === '' && expectedReplacements > 1) {
|
|
725
|
+
return `Cannot perform multiple replacements with empty old_string (would cause infinite loop)`;
|
|
726
|
+
}
|
|
727
|
+
const replaceLine = params.replaceBeginLineNumber;
|
|
728
|
+
if (replaceLine !== undefined &&
|
|
729
|
+
(!Number.isFinite(replaceLine) ||
|
|
730
|
+
!Number.isInteger(replaceLine) ||
|
|
731
|
+
replaceLine <= 0)) {
|
|
732
|
+
return `replaceBeginLineNumber must be a positive integer (1-based)`;
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
createInvocation(params, messageBus, toolName, displayName) {
|
|
737
|
+
// Normalize parameters: if file_path is provided but not absolute_path, copy it over
|
|
738
|
+
const normalizedParams = { ...params };
|
|
739
|
+
if (!normalizedParams.absolute_path && normalizedParams.file_path) {
|
|
740
|
+
normalizedParams.absolute_path = normalizedParams.file_path;
|
|
741
|
+
}
|
|
742
|
+
return new EditToolInvocation(this.host, this.ideService, this.lspService, normalizedParams, messageBus, toolName ?? this.name, displayName ?? this.displayName);
|
|
743
|
+
}
|
|
744
|
+
async readTextFile(filePath) {
|
|
745
|
+
const fileSystemService = this.host.getFileSystemService?.();
|
|
746
|
+
if (fileSystemService !== undefined) {
|
|
747
|
+
return fileSystemService.readTextFile(filePath);
|
|
748
|
+
}
|
|
749
|
+
return fs.readFile(filePath, 'utf8');
|
|
750
|
+
}
|
|
751
|
+
async execute(params, signal = new AbortController().signal) {
|
|
752
|
+
return this.build(params).execute(signal);
|
|
753
|
+
}
|
|
754
|
+
getModifyContext(_) {
|
|
755
|
+
return {
|
|
756
|
+
getFilePath: (params) =>
|
|
757
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: empty string paths are invalid, fall back to file_path
|
|
758
|
+
params.absolute_path || params.file_path || '',
|
|
759
|
+
getCurrentContent: async (params) => {
|
|
760
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: empty string paths are invalid, fall back to file_path
|
|
761
|
+
const filePath = params.absolute_path || params.file_path || '';
|
|
762
|
+
try {
|
|
763
|
+
return await this.readTextFile(filePath);
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
if (!isNodeError(err) || err.code !== 'ENOENT')
|
|
767
|
+
throw err;
|
|
768
|
+
return '';
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
getProposedContent: async (params) => {
|
|
772
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy coalescing: empty string paths are invalid, fall back to file_path
|
|
773
|
+
const filePath = params.absolute_path || params.file_path || '';
|
|
774
|
+
try {
|
|
775
|
+
const raw = await this.readTextFile(filePath);
|
|
776
|
+
const currentContent = raw.replace(/\r\n/g, '\n');
|
|
777
|
+
const replaceLine = params.replaceBeginLineNumber;
|
|
778
|
+
if (replaceLine !== undefined &&
|
|
779
|
+
replaceLine > 0 &&
|
|
780
|
+
params.old_string !== '') {
|
|
781
|
+
// When replaceBeginLineNumber is set, only replace occurrences
|
|
782
|
+
// whose start falls on the specified line.
|
|
783
|
+
const expectedReplacements = params.expected_replacements ?? 1;
|
|
784
|
+
const eligibleCount = countLineGuardedOccurrences(currentContent, params.old_string, replaceLine);
|
|
785
|
+
// If eligible count doesn't match expected_replacements, execute()
|
|
786
|
+
// would reject with a mismatch error. Return unchanged content to
|
|
787
|
+
// avoid presenting a partial proposal that would never be written.
|
|
788
|
+
if (eligibleCount !== expectedReplacements) {
|
|
789
|
+
return currentContent;
|
|
790
|
+
}
|
|
791
|
+
return applyLineGuardedReplacement(currentContent, params.old_string, params.new_string, expectedReplacements, replaceLine);
|
|
792
|
+
}
|
|
793
|
+
const isNewFile = params.old_string === '' && currentContent === '';
|
|
794
|
+
return applyReplacement(currentContent, params.old_string, params.new_string, isNewFile, params.expected_replacements ?? 1);
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
if (!isNodeError(err) || err.code !== 'ENOENT')
|
|
798
|
+
throw err;
|
|
799
|
+
// File does not exist: if old_string is empty, this is a new-file creation.
|
|
800
|
+
if (params.old_string === '') {
|
|
801
|
+
return params.new_string;
|
|
802
|
+
}
|
|
803
|
+
return '';
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
createUpdatedParams: (oldContent, modifiedProposedContent, originalParams) => ({
|
|
807
|
+
...originalParams,
|
|
808
|
+
ai_proposed_content: oldContent,
|
|
809
|
+
old_string: oldContent,
|
|
810
|
+
new_string: modifiedProposedContent,
|
|
811
|
+
modified_by_user: true,
|
|
812
|
+
}),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
//# sourceMappingURL=edit.js.map
|