@vybestack/llxprt-code-core 0.1.18 → 0.1.19-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +1 -0
  2. package/dist/src/code_assist/converter.d.ts +2 -1
  3. package/dist/src/code_assist/converter.js +1 -1
  4. package/dist/src/code_assist/converter.js.map +1 -1
  5. package/dist/src/code_assist/converter.test.js +48 -1
  6. package/dist/src/code_assist/converter.test.js.map +1 -1
  7. package/dist/src/code_assist/oauth2.js +2 -1
  8. package/dist/src/code_assist/oauth2.js.map +1 -1
  9. package/dist/src/code_assist/server.test.js +4 -1
  10. package/dist/src/code_assist/server.test.js.map +1 -1
  11. package/dist/src/config/config.alwaysAllow.test.d.ts +6 -0
  12. package/dist/src/config/config.alwaysAllow.test.js +84 -0
  13. package/dist/src/config/config.alwaysAllow.test.js.map +1 -0
  14. package/dist/src/config/config.d.ts +80 -1
  15. package/dist/src/config/config.ephemeral.test.d.ts +6 -0
  16. package/dist/src/config/config.ephemeral.test.js +152 -0
  17. package/dist/src/config/config.ephemeral.test.js.map +1 -0
  18. package/dist/src/config/config.js +135 -0
  19. package/dist/src/config/config.js.map +1 -1
  20. package/dist/src/config/config.test.js +8 -0
  21. package/dist/src/config/config.test.js.map +1 -1
  22. package/dist/src/core/client.d.ts +0 -2
  23. package/dist/src/core/client.js +10 -85
  24. package/dist/src/core/client.js.map +1 -1
  25. package/dist/src/core/client.test.js +7 -2
  26. package/dist/src/core/client.test.js.map +1 -1
  27. package/dist/src/core/contentGenerator.js +1 -1
  28. package/dist/src/core/contentGenerator.js.map +1 -1
  29. package/dist/src/core/coreToolScheduler.d.ts +17 -7
  30. package/dist/src/core/coreToolScheduler.js +121 -18
  31. package/dist/src/core/coreToolScheduler.js.map +1 -1
  32. package/dist/src/core/coreToolScheduler.test.js +25 -37
  33. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  34. package/dist/src/core/geminiChat.d.ts +3 -0
  35. package/dist/src/core/geminiChat.js +17 -13
  36. package/dist/src/core/geminiChat.js.map +1 -1
  37. package/dist/src/core/logger.d.ts +1 -0
  38. package/dist/src/core/logger.js +18 -0
  39. package/dist/src/core/logger.js.map +1 -1
  40. package/dist/src/core/logger.test.js +29 -0
  41. package/dist/src/core/logger.test.js.map +1 -1
  42. package/dist/src/core/loggingContentGenerator.d.ts +24 -0
  43. package/dist/src/core/loggingContentGenerator.js +89 -0
  44. package/dist/src/core/loggingContentGenerator.js.map +1 -0
  45. package/dist/src/core/nonInteractiveToolExecutor.js +21 -1
  46. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  47. package/dist/src/core/nonInteractiveToolExecutor.test.js +8 -31
  48. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  49. package/dist/src/core/subagent.d.ts +230 -0
  50. package/dist/src/core/subagent.js +453 -0
  51. package/dist/src/core/subagent.js.map +1 -0
  52. package/dist/src/core/subagent.test.d.ts +6 -0
  53. package/dist/src/core/subagent.test.js +519 -0
  54. package/dist/src/core/subagent.test.js.map +1 -0
  55. package/dist/src/hooks/tool-render-suppression-hook.d.ts +16 -0
  56. package/dist/src/hooks/tool-render-suppression-hook.js +26 -0
  57. package/dist/src/hooks/tool-render-suppression-hook.js.map +1 -0
  58. package/dist/src/hooks/tool-render-suppression-hook.test.d.ts +6 -0
  59. package/dist/src/hooks/tool-render-suppression-hook.test.js +59 -0
  60. package/dist/src/hooks/tool-render-suppression-hook.test.js.map +1 -0
  61. package/dist/src/ide/ide-client.d.ts +22 -1
  62. package/dist/src/ide/ide-client.js +161 -17
  63. package/dist/src/ide/ide-client.js.map +1 -1
  64. package/dist/src/ide/ideContext.d.ts +127 -32
  65. package/dist/src/ide/ideContext.js +45 -0
  66. package/dist/src/ide/ideContext.js.map +1 -1
  67. package/dist/src/index.d.ts +5 -0
  68. package/dist/src/index.js +6 -0
  69. package/dist/src/index.js.map +1 -1
  70. package/dist/src/integration-tests/todo-system.test.js +38 -602
  71. package/dist/src/integration-tests/todo-system.test.js.map +1 -1
  72. package/dist/src/providers/IProviderManager.d.ts +5 -0
  73. package/dist/src/providers/LoggingProviderWrapper.d.ts +53 -0
  74. package/dist/src/providers/LoggingProviderWrapper.js +347 -0
  75. package/dist/src/providers/LoggingProviderWrapper.js.map +1 -0
  76. package/dist/src/providers/ProviderManager.d.ts +20 -0
  77. package/dist/src/providers/ProviderManager.js +214 -1
  78. package/dist/src/providers/ProviderManager.js.map +1 -1
  79. package/dist/src/providers/gemini/GeminiProvider.js +5 -5
  80. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  81. package/dist/src/providers/logging/ProviderContentExtractor.d.ts +27 -0
  82. package/dist/src/providers/logging/ProviderContentExtractor.js +198 -0
  83. package/dist/src/providers/logging/ProviderContentExtractor.js.map +1 -0
  84. package/dist/src/providers/logging/ProviderPerformanceTracker.d.ts +43 -0
  85. package/dist/src/providers/logging/ProviderPerformanceTracker.js +98 -0
  86. package/dist/src/providers/logging/ProviderPerformanceTracker.js.map +1 -0
  87. package/dist/src/providers/openai/OpenAIProvider.js +8 -1
  88. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  89. package/dist/src/providers/openai/OpenAIProvider.test.js +106 -0
  90. package/dist/src/providers/openai/OpenAIProvider.test.js.map +1 -1
  91. package/dist/src/providers/types/IProviderConfig.d.ts +5 -0
  92. package/dist/src/providers/types.d.ts +47 -0
  93. package/dist/src/services/git-stats-service.d.ts +32 -0
  94. package/dist/src/services/git-stats-service.js +22 -0
  95. package/dist/src/services/git-stats-service.js.map +1 -0
  96. package/dist/src/services/loopDetectionService.js +10 -6
  97. package/dist/src/services/loopDetectionService.js.map +1 -1
  98. package/dist/src/services/loopDetectionService.test.js +139 -0
  99. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  100. package/dist/src/services/shellExecutionService.js +69 -9
  101. package/dist/src/services/shellExecutionService.js.map +1 -1
  102. package/dist/src/services/shellExecutionService.test.js +8 -0
  103. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  104. package/dist/src/services/todo-context-tracker.d.ts +31 -0
  105. package/dist/src/services/todo-context-tracker.js +45 -0
  106. package/dist/src/services/todo-context-tracker.js.map +1 -0
  107. package/dist/src/services/tool-call-tracker-service.d.ts +52 -0
  108. package/dist/src/services/tool-call-tracker-service.js +170 -0
  109. package/dist/src/services/tool-call-tracker-service.js.map +1 -0
  110. package/dist/src/services/tool-call-tracker-service.test.d.ts +6 -0
  111. package/dist/src/services/tool-call-tracker-service.test.js +99 -0
  112. package/dist/src/services/tool-call-tracker-service.test.js.map +1 -0
  113. package/dist/src/storage/ConversationFileWriter.d.ts +16 -0
  114. package/dist/src/storage/ConversationFileWriter.js +69 -0
  115. package/dist/src/storage/ConversationFileWriter.js.map +1 -0
  116. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +8 -0
  117. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +56 -3
  118. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  119. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.d.ts +6 -0
  120. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +187 -0
  121. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -0
  122. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +5 -1
  123. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +11 -0
  124. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  125. package/dist/src/telemetry/constants.d.ts +5 -0
  126. package/dist/src/telemetry/constants.js +5 -0
  127. package/dist/src/telemetry/constants.js.map +1 -1
  128. package/dist/src/telemetry/loggers.d.ts +5 -1
  129. package/dist/src/telemetry/loggers.js +87 -1
  130. package/dist/src/telemetry/loggers.js.map +1 -1
  131. package/dist/src/telemetry/loggers.test.circular.js +7 -2
  132. package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
  133. package/dist/src/telemetry/loggers.test.js +7 -2
  134. package/dist/src/telemetry/loggers.test.js.map +1 -1
  135. package/dist/src/telemetry/metrics.d.ts +3 -2
  136. package/dist/src/telemetry/metrics.js +7 -1
  137. package/dist/src/telemetry/metrics.js.map +1 -1
  138. package/dist/src/telemetry/metrics.test.js +50 -0
  139. package/dist/src/telemetry/metrics.test.js.map +1 -1
  140. package/dist/src/telemetry/tool-call-decision.d.ts +13 -0
  141. package/dist/src/telemetry/tool-call-decision.js +29 -0
  142. package/dist/src/telemetry/tool-call-decision.js.map +1 -0
  143. package/dist/src/telemetry/types.d.ts +58 -2
  144. package/dist/src/telemetry/types.js +126 -1
  145. package/dist/src/telemetry/types.js.map +1 -1
  146. package/dist/src/telemetry/uiTelemetry.d.ts +4 -1
  147. package/dist/src/telemetry/uiTelemetry.js +3 -1
  148. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  149. package/dist/src/telemetry/uiTelemetry.test.js +13 -2
  150. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  151. package/dist/src/test-utils/tools.d.ts +23 -0
  152. package/dist/src/test-utils/tools.js +41 -0
  153. package/dist/src/test-utils/tools.js.map +1 -0
  154. package/dist/src/tools/diffOptions.d.ts +2 -0
  155. package/dist/src/tools/diffOptions.js +28 -0
  156. package/dist/src/tools/diffOptions.js.map +1 -1
  157. package/dist/src/tools/diffOptions.test.d.ts +6 -0
  158. package/dist/src/tools/diffOptions.test.js +119 -0
  159. package/dist/src/tools/diffOptions.test.js.map +1 -0
  160. package/dist/src/tools/edit.d.ts +9 -33
  161. package/dist/src/tools/edit.js +167 -132
  162. package/dist/src/tools/edit.js.map +1 -1
  163. package/dist/src/tools/edit.test.js +124 -50
  164. package/dist/src/tools/edit.test.js.map +1 -1
  165. package/dist/src/tools/glob.d.ts +3 -10
  166. package/dist/src/tools/glob.js +97 -99
  167. package/dist/src/tools/glob.js.map +1 -1
  168. package/dist/src/tools/glob.test.js +37 -26
  169. package/dist/src/tools/glob.test.js.map +1 -1
  170. package/dist/src/tools/grep.d.ts +3 -35
  171. package/dist/src/tools/grep.js +117 -88
  172. package/dist/src/tools/grep.js.map +1 -1
  173. package/dist/src/tools/grep.test.js +36 -22
  174. package/dist/src/tools/grep.test.js.map +1 -1
  175. package/dist/src/tools/mcp-client.d.ts +14 -3
  176. package/dist/src/tools/mcp-client.js +82 -6
  177. package/dist/src/tools/mcp-client.js.map +1 -1
  178. package/dist/src/tools/mcp-client.test.js +337 -2
  179. package/dist/src/tools/mcp-client.test.js.map +1 -1
  180. package/dist/src/tools/memoryTool.d.ts +2 -2
  181. package/dist/src/tools/memoryTool.js +1 -0
  182. package/dist/src/tools/memoryTool.js.map +1 -1
  183. package/dist/src/tools/modifiable-tool.d.ts +8 -5
  184. package/dist/src/tools/modifiable-tool.js +4 -1
  185. package/dist/src/tools/modifiable-tool.js.map +1 -1
  186. package/dist/src/tools/modifiable-tool.test.js +3 -3
  187. package/dist/src/tools/modifiable-tool.test.js.map +1 -1
  188. package/dist/src/tools/read-file.d.ts +4 -6
  189. package/dist/src/tools/read-file.js +87 -46
  190. package/dist/src/tools/read-file.js.map +1 -1
  191. package/dist/src/tools/read-file.test.js +207 -126
  192. package/dist/src/tools/read-file.test.js.map +1 -1
  193. package/dist/src/tools/read-many-files.js +8 -2
  194. package/dist/src/tools/read-many-files.js.map +1 -1
  195. package/dist/src/tools/read-many-files.test.js +16 -0
  196. package/dist/src/tools/read-many-files.test.js.map +1 -1
  197. package/dist/src/tools/shell.test.js +17 -0
  198. package/dist/src/tools/shell.test.js.map +1 -1
  199. package/dist/src/tools/todo-events.d.ts +22 -0
  200. package/dist/src/tools/todo-events.js +24 -0
  201. package/dist/src/tools/todo-events.js.map +1 -0
  202. package/dist/src/tools/todo-pause.d.ts +22 -0
  203. package/dist/src/tools/todo-pause.js +93 -0
  204. package/dist/src/tools/todo-pause.js.map +1 -0
  205. package/dist/src/tools/todo-pause.spec.d.ts +6 -0
  206. package/dist/src/tools/todo-pause.spec.js +287 -0
  207. package/dist/src/tools/todo-pause.spec.js.map +1 -0
  208. package/dist/src/tools/todo-read.js.map +1 -1
  209. package/dist/src/tools/todo-schemas.d.ts +232 -4
  210. package/dist/src/tools/todo-schemas.js +13 -0
  211. package/dist/src/tools/todo-schemas.js.map +1 -1
  212. package/dist/src/tools/todo-schemas.test.js +190 -1
  213. package/dist/src/tools/todo-schemas.test.js.map +1 -1
  214. package/dist/src/tools/todo-store.d.ts +1 -4
  215. package/dist/src/tools/todo-store.js +41 -40
  216. package/dist/src/tools/todo-store.js.map +1 -1
  217. package/dist/src/tools/todo-store.test.js +34 -40
  218. package/dist/src/tools/todo-store.test.js.map +1 -1
  219. package/dist/src/tools/todo-write.d.ts +1 -1
  220. package/dist/src/tools/todo-write.js +84 -47
  221. package/dist/src/tools/todo-write.js.map +1 -1
  222. package/dist/src/tools/todo-write.test.js +23 -9
  223. package/dist/src/tools/todo-write.test.js.map +1 -1
  224. package/dist/src/tools/tool-context.d.ts +2 -0
  225. package/dist/src/tools/tool-error.d.ts +4 -0
  226. package/dist/src/tools/tool-error.js +4 -0
  227. package/dist/src/tools/tool-error.js.map +1 -1
  228. package/dist/src/tools/tool-registry.d.ts +12 -6
  229. package/dist/src/tools/tool-registry.js +19 -4
  230. package/dist/src/tools/tool-registry.js.map +1 -1
  231. package/dist/src/tools/tool-registry.test.js +3 -20
  232. package/dist/src/tools/tool-registry.test.js.map +1 -1
  233. package/dist/src/tools/tools.d.ts +134 -39
  234. package/dist/src/tools/tools.js +115 -10
  235. package/dist/src/tools/tools.js.map +1 -1
  236. package/dist/src/tools/web-search.test.js +1 -0
  237. package/dist/src/tools/web-search.test.js.map +1 -1
  238. package/dist/src/tools/write-file.d.ts +6 -2
  239. package/dist/src/tools/write-file.js +106 -16
  240. package/dist/src/tools/write-file.js.map +1 -1
  241. package/dist/src/tools/write-file.test.js +25 -7
  242. package/dist/src/tools/write-file.test.js.map +1 -1
  243. package/dist/src/types/modelParams.d.ts +4 -0
  244. package/dist/src/utils/environmentContext.d.ts +21 -0
  245. package/dist/src/utils/environmentContext.js +90 -0
  246. package/dist/src/utils/environmentContext.js.map +1 -0
  247. package/dist/src/utils/environmentContext.test.d.ts +6 -0
  248. package/dist/src/utils/environmentContext.test.js +139 -0
  249. package/dist/src/utils/environmentContext.test.js.map +1 -0
  250. package/dist/src/utils/errors.d.ts +3 -0
  251. package/dist/src/utils/errors.js +6 -0
  252. package/dist/src/utils/errors.js.map +1 -1
  253. package/dist/src/utils/fileUtils.d.ts +7 -0
  254. package/dist/src/utils/fileUtils.js +11 -10
  255. package/dist/src/utils/fileUtils.js.map +1 -1
  256. package/dist/src/utils/fileUtils.test.js +1 -4
  257. package/dist/src/utils/fileUtils.test.js.map +1 -1
  258. package/dist/src/utils/filesearch/fileSearch.d.ts +1 -0
  259. package/dist/src/utils/filesearch/fileSearch.js +27 -5
  260. package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
  261. package/dist/src/utils/filesearch/fileSearch.test.js +21 -1
  262. package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -1
  263. package/dist/src/utils/memoryDiscovery.js +4 -1
  264. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  265. package/dist/src/utils/shell-utils.js +14 -2
  266. package/dist/src/utils/shell-utils.js.map +1 -1
  267. package/dist/src/utils/shell-utils.shellReplacement.test.d.ts +6 -0
  268. package/dist/src/utils/shell-utils.shellReplacement.test.js +149 -0
  269. package/dist/src/utils/shell-utils.shellReplacement.test.js.map +1 -0
  270. package/package.json +2 -1
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+ import { getDiffStat } from './diffOptions.js';
8
+ describe('getDiffStat', () => {
9
+ const fileName = 'test.txt';
10
+ it('should return 0 for all stats when there are no changes', () => {
11
+ const oldStr = 'line1\nline2\n';
12
+ const aiStr = 'line1\nline2\n';
13
+ const userStr = 'line1\nline2\n';
14
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
15
+ expect(diffStat).toEqual({
16
+ ai_added_lines: 0,
17
+ ai_removed_lines: 0,
18
+ user_added_lines: 0,
19
+ user_removed_lines: 0,
20
+ });
21
+ });
22
+ it('should correctly report AI additions', () => {
23
+ const oldStr = 'line1\nline2\n';
24
+ const aiStr = 'line1\nline2\nline3\n';
25
+ const userStr = 'line1\nline2\nline3\n';
26
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
27
+ expect(diffStat).toEqual({
28
+ ai_added_lines: 1,
29
+ ai_removed_lines: 0,
30
+ user_added_lines: 0,
31
+ user_removed_lines: 0,
32
+ });
33
+ });
34
+ it('should correctly report AI removals', () => {
35
+ const oldStr = 'line1\nline2\nline3\n';
36
+ const aiStr = 'line1\nline3\n';
37
+ const userStr = 'line1\nline3\n';
38
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
39
+ expect(diffStat).toEqual({
40
+ ai_added_lines: 0,
41
+ ai_removed_lines: 1,
42
+ user_added_lines: 0,
43
+ user_removed_lines: 0,
44
+ });
45
+ });
46
+ it('should correctly report AI modifications', () => {
47
+ const oldStr = 'line1\nline2\nline3\n';
48
+ const aiStr = 'line1\nline_two\nline3\n';
49
+ const userStr = 'line1\nline_two\nline3\n';
50
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
51
+ expect(diffStat).toEqual({
52
+ ai_added_lines: 1,
53
+ ai_removed_lines: 1,
54
+ user_added_lines: 0,
55
+ user_removed_lines: 0,
56
+ });
57
+ });
58
+ it('should correctly report user additions', () => {
59
+ const oldStr = 'line1\nline2\n';
60
+ const aiStr = 'line1\nline2\nline3\n';
61
+ const userStr = 'line1\nline2\nline3\nline4\n';
62
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
63
+ expect(diffStat).toEqual({
64
+ ai_added_lines: 1,
65
+ ai_removed_lines: 0,
66
+ user_added_lines: 1,
67
+ user_removed_lines: 0,
68
+ });
69
+ });
70
+ it('should correctly report user removals', () => {
71
+ const oldStr = 'line1\nline2\n';
72
+ const aiStr = 'line1\nline2\nline3\n';
73
+ const userStr = 'line1\nline2\n';
74
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
75
+ expect(diffStat).toEqual({
76
+ ai_added_lines: 1,
77
+ ai_removed_lines: 0,
78
+ user_added_lines: 0,
79
+ user_removed_lines: 1,
80
+ });
81
+ });
82
+ it('should correctly report user modifications', () => {
83
+ const oldStr = 'line1\nline2\n';
84
+ const aiStr = 'line1\nline2\nline3\n';
85
+ const userStr = 'line1\nline2\nline_three\n';
86
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
87
+ expect(diffStat).toEqual({
88
+ ai_added_lines: 1,
89
+ ai_removed_lines: 0,
90
+ user_added_lines: 1,
91
+ user_removed_lines: 1,
92
+ });
93
+ });
94
+ it('should handle complex changes from both AI and user', () => {
95
+ const oldStr = 'line1\nline2\nline3\nline4\n';
96
+ const aiStr = 'line_one\nline2\nline_three\nline4\n';
97
+ const userStr = 'line_one\nline_two\nline_three\nline4\nline5\n';
98
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
99
+ expect(diffStat).toEqual({
100
+ ai_added_lines: 2,
101
+ ai_removed_lines: 2,
102
+ user_added_lines: 2,
103
+ user_removed_lines: 1,
104
+ });
105
+ });
106
+ it('should report a single line modification as one addition and one removal', () => {
107
+ const oldStr = 'hello world';
108
+ const aiStr = 'hello universe';
109
+ const userStr = 'hello universe';
110
+ const diffStat = getDiffStat(fileName, oldStr, aiStr, userStr);
111
+ expect(diffStat).toEqual({
112
+ ai_added_lines: 1,
113
+ ai_removed_lines: 1,
114
+ user_added_lines: 0,
115
+ user_removed_lines: 0,
116
+ });
117
+ });
118
+ });
119
+ //# sourceMappingURL=diffOptions.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffOptions.test.js","sourceRoot":"","sources":["../../../src/tools/diffOptions.test.ts"],"names":[],"mappings":"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"}
@@ -3,9 +3,10 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { BaseTool, ToolCallConfirmationDetails, ToolLocation, ToolResult } from './tools.js';
6
+ import { BaseDeclarativeTool, ToolInvocation, ToolResult } from './tools.js';
7
7
  import { Config } from '../config/config.js';
8
- import { ModifiableTool, ModifyContext } from './modifiable-tool.js';
8
+ import { ModifiableDeclarativeTool, ModifyContext } from './modifiable-tool.js';
9
+ export declare function applyReplacement(currentContent: string | null, oldString: string, newString: string, isNewFile: boolean): string;
9
10
  /**
10
11
  * Parameters for the Edit tool
11
12
  */
@@ -31,11 +32,15 @@ export interface EditToolParams {
31
32
  * Whether the edit was modified manually by the user.
32
33
  */
33
34
  modified_by_user?: boolean;
35
+ /**
36
+ * Initially proposed string.
37
+ */
38
+ ai_proposed_string?: string;
34
39
  }
35
40
  /**
36
41
  * Implementation of the Edit tool logic
37
42
  */
38
- export declare class EditTool extends BaseTool<EditToolParams, ToolResult> implements ModifiableTool<EditToolParams> {
43
+ export declare class EditTool extends BaseDeclarativeTool<EditToolParams, ToolResult> implements ModifiableDeclarativeTool<EditToolParams> {
39
44
  private readonly config;
40
45
  static readonly Name = "replace";
41
46
  constructor(config: Config);
@@ -45,35 +50,6 @@ export declare class EditTool extends BaseTool<EditToolParams, ToolResult> imple
45
50
  * @returns Error message string or null if valid
46
51
  */
47
52
  validateToolParams(params: EditToolParams): string | null;
48
- /**
49
- * Determines any file locations affected by the tool execution
50
- * @param params Parameters for the tool execution
51
- * @returns A list of such paths
52
- */
53
- toolLocations(params: EditToolParams): ToolLocation[];
54
- private _applyReplacement;
55
- /**
56
- * Calculates the potential outcome of an edit operation.
57
- * @param params Parameters for the edit operation
58
- * @returns An object describing the potential edit outcome
59
- * @throws File system errors if reading the file fails unexpectedly (e.g., permissions)
60
- */
61
- private calculateEdit;
62
- /**
63
- * Handles the confirmation prompt for the Edit tool in the CLI.
64
- * It needs to calculate the diff to show the user.
65
- */
66
- shouldConfirmExecute(params: EditToolParams, abortSignal: AbortSignal): Promise<ToolCallConfirmationDetails | false>;
67
- getDescription(params: EditToolParams): string;
68
- /**
69
- * Executes the edit operation with the given parameters.
70
- * @param params Parameters for the edit operation
71
- * @returns Result of the edit operation
72
- */
73
- execute(params: EditToolParams, signal: AbortSignal): Promise<ToolResult>;
74
- /**
75
- * Creates parent directories if they don't exist
76
- */
77
- private ensureParentDirectoriesExist;
53
+ protected createInvocation(params: EditToolParams): ToolInvocation<EditToolParams, ToolResult>;
78
54
  getModifyContext(_: AbortSignal): ModifyContext<EditToolParams>;
79
55
  }
@@ -6,7 +6,7 @@
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as Diff from 'diff';
9
- import { BaseTool, Icon, ToolConfirmationOutcome, } from './tools.js';
9
+ import { BaseDeclarativeTool, Icon, ToolConfirmationOutcome, } from './tools.js';
10
10
  import { ToolErrorType } from './tool-error.js';
11
11
  import { Type } from '@google/genai';
12
12
  import { SchemaValidator } from '../utils/schemaValidator.js';
@@ -14,91 +14,33 @@ import { makeRelative, shortenPath } from '../utils/paths.js';
14
14
  import { isNodeError } from '../utils/errors.js';
15
15
  import { ApprovalMode } from '../config/config.js';
16
16
  import { ensureCorrectEdit } from '../utils/editCorrector.js';
17
- import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js';
17
+ import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js';
18
18
  import { ReadFileTool } from './read-file.js';
19
- /**
20
- * Implementation of the Edit tool logic
21
- */
22
- export class EditTool extends BaseTool {
23
- config;
24
- static Name = 'replace';
25
- constructor(config) {
26
- 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.
27
-
28
- The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response.
29
-
30
- Expectation for required parameters:
31
- 1. \`file_path\` MUST be an absolute path; otherwise an error will be thrown.
32
- 2. \`old_string\` MUST be the exact literal text to replace (including all whitespace, indentation, newlines, and surrounding code etc.).
33
- 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.
34
- 4. NEVER escape \`old_string\` or \`new_string\`, that would break the exact literal text requirement.
35
- **Important:** If ANY of the above are not satisfied, the tool will fail. CRITICAL for \`old_string\`: Must uniquely identify the single instance to change. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string matches multiple locations, or does not match exactly, the tool will fail.
36
- **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.`, Icon.Pencil, {
37
- properties: {
38
- file_path: {
39
- description: "The absolute path to the file to modify. Must start with '/'.",
40
- type: Type.STRING,
41
- },
42
- old_string: {
43
- 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.',
44
- type: Type.STRING,
45
- },
46
- new_string: {
47
- description: 'The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic.',
48
- type: Type.STRING,
49
- },
50
- expected_replacements: {
51
- type: Type.NUMBER,
52
- description: 'Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.',
53
- minimum: 1,
54
- },
55
- },
56
- required: ['file_path', 'old_string', 'new_string'],
57
- type: Type.OBJECT,
58
- });
59
- this.config = config;
19
+ import { IDEConnectionStatus } from '../ide/ide-client.js';
20
+ import { getGitStatsService } from '../services/git-stats-service.js';
21
+ export function applyReplacement(currentContent, oldString, newString, isNewFile) {
22
+ if (isNewFile) {
23
+ return newString;
60
24
  }
61
- /**
62
- * Validates the parameters for the Edit tool
63
- * @param params Parameters to validate
64
- * @returns Error message string or null if valid
65
- */
66
- validateToolParams(params) {
67
- const errors = SchemaValidator.validate(this.schema.parameters, params);
68
- if (errors) {
69
- return errors;
70
- }
71
- if (!path.isAbsolute(params.file_path)) {
72
- return `File path must be absolute: ${params.file_path}`;
73
- }
74
- const workspaceContext = this.config.getWorkspaceContext();
75
- if (!workspaceContext.isPathWithinWorkspace(params.file_path)) {
76
- const directories = workspaceContext.getDirectories();
77
- return `File path must be within one of the workspace directories: ${directories.join(', ')}`;
78
- }
79
- return null;
25
+ if (currentContent === null) {
26
+ // Should not happen if not a new file, but defensively return empty or newString if oldString is also empty
27
+ return oldString === '' ? newString : '';
80
28
  }
81
- /**
82
- * Determines any file locations affected by the tool execution
83
- * @param params Parameters for the tool execution
84
- * @returns A list of such paths
85
- */
86
- toolLocations(params) {
87
- return [{ path: params.file_path }];
29
+ // If oldString is empty and it's not a new file, do not modify the content.
30
+ if (oldString === '' && !isNewFile) {
31
+ return currentContent;
88
32
  }
89
- _applyReplacement(currentContent, oldString, newString, isNewFile) {
90
- if (isNewFile) {
91
- return newString;
92
- }
93
- if (currentContent === null) {
94
- // Should not happen if not a new file, but defensively return empty or newString if oldString is also empty
95
- return oldString === '' ? newString : '';
96
- }
97
- // If oldString is empty and it's not a new file, do not modify the content.
98
- if (oldString === '' && !isNewFile) {
99
- return currentContent;
100
- }
101
- return currentContent.replaceAll(oldString, newString);
33
+ return currentContent.replaceAll(oldString, newString);
34
+ }
35
+ class EditToolInvocation {
36
+ config;
37
+ params;
38
+ constructor(config, params) {
39
+ this.config = config;
40
+ this.params = params;
41
+ }
42
+ toolLocations() {
43
+ return [{ path: this.params.file_path }];
102
44
  }
103
45
  /**
104
46
  * Calculates the potential outcome of an edit operation.
@@ -185,7 +127,7 @@ Expectation for required parameters:
185
127
  type: ToolErrorType.READ_CONTENT_FAILURE,
186
128
  };
187
129
  }
188
- const newContent = this._applyReplacement(currentContent, finalOldString, finalNewString, isNewFile);
130
+ const newContent = applyReplacement(currentContent, finalOldString, finalNewString, isNewFile);
189
131
  return {
190
132
  currentContent,
191
133
  newContent,
@@ -198,20 +140,15 @@ Expectation for required parameters:
198
140
  * Handles the confirmation prompt for the Edit tool in the CLI.
199
141
  * It needs to calculate the diff to show the user.
200
142
  */
201
- async shouldConfirmExecute(params, abortSignal) {
143
+ async shouldConfirmExecute(abortSignal) {
202
144
  const approvalMode = this.config.getApprovalMode();
203
145
  if (approvalMode === ApprovalMode.AUTO_EDIT ||
204
146
  approvalMode === ApprovalMode.YOLO) {
205
147
  return false;
206
148
  }
207
- const validationError = this.validateToolParams(params);
208
- if (validationError) {
209
- console.error(`[EditTool Wrapper] Attempted confirmation with invalid parameters: ${validationError}`);
210
- return false;
211
- }
212
149
  let editData;
213
150
  try {
214
- editData = await this.calculateEdit(params, abortSignal);
151
+ editData = await this.calculateEdit(this.params, abortSignal);
215
152
  }
216
153
  catch (error) {
217
154
  console.error('Failed to calculate edit:', error);
@@ -220,12 +157,19 @@ Expectation for required parameters:
220
157
  if (editData.error) {
221
158
  return false;
222
159
  }
223
- const fileName = path.basename(params.file_path);
160
+ const fileName = path.basename(this.params.file_path);
224
161
  const fileDiff = Diff.createPatch(fileName, editData.currentContent ?? '', editData.newContent, 'Current', 'Proposed', DEFAULT_DIFF_OPTIONS);
162
+ const ideClient = this.config.getIdeClient();
163
+ const ideConfirmation = this.config.getIdeModeFeature() &&
164
+ this.config.getIdeMode() &&
165
+ ideClient?.getConnectionStatus().status === IDEConnectionStatus.Connected
166
+ ? ideClient.openDiff(this.params.file_path, editData.newContent)
167
+ : undefined;
225
168
  const confirmationDetails = {
226
169
  type: 'edit',
227
- title: `Confirm Edit: ${shortenPath(makeRelative(params.file_path, this.config.getTargetDir()))}`,
170
+ title: `Confirm Edit: ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`,
228
171
  fileName,
172
+ filePath: this.params.file_path,
229
173
  fileDiff,
230
174
  originalContent: editData.currentContent,
231
175
  newContent: editData.newContent,
@@ -233,23 +177,30 @@ Expectation for required parameters:
233
177
  if (outcome === ToolConfirmationOutcome.ProceedAlways) {
234
178
  this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
235
179
  }
180
+ if (ideConfirmation) {
181
+ const result = await ideConfirmation;
182
+ if (result.status === 'accepted' && result.content) {
183
+ // TODO(chrstn): See https://github.com/google-gemini/gemini-cli/pull/5618#discussion_r2255413084
184
+ // for info on a possible race condition where the file is modified on disk while being edited.
185
+ this.params.old_string = editData.currentContent ?? '';
186
+ this.params.new_string = result.content;
187
+ }
188
+ }
236
189
  },
190
+ ideConfirmation,
237
191
  };
238
192
  return confirmationDetails;
239
193
  }
240
- getDescription(params) {
241
- if (!params.file_path || !params.old_string || !params.new_string) {
242
- return `Model did not provide valid parameters for edit tool`;
243
- }
244
- const relativePath = makeRelative(params.file_path, this.config.getTargetDir());
245
- if (params.old_string === '') {
194
+ getDescription() {
195
+ const relativePath = makeRelative(this.params.file_path, this.config.getTargetDir());
196
+ if (this.params.old_string === '') {
246
197
  return `Create ${shortenPath(relativePath)}`;
247
198
  }
248
- const oldStringSnippet = params.old_string.split('\n')[0].substring(0, 30) +
249
- (params.old_string.length > 30 ? '...' : '');
250
- const newStringSnippet = params.new_string.split('\n')[0].substring(0, 30) +
251
- (params.new_string.length > 30 ? '...' : '');
252
- if (params.old_string === params.new_string) {
199
+ const oldStringSnippet = this.params.old_string.split('\n')[0].substring(0, 30) +
200
+ (this.params.old_string.length > 30 ? '...' : '');
201
+ const newStringSnippet = this.params.new_string.split('\n')[0].substring(0, 30) +
202
+ (this.params.new_string.length > 30 ? '...' : '');
203
+ if (this.params.old_string === this.params.new_string) {
253
204
  return `No file changes to ${shortenPath(relativePath)}`;
254
205
  }
255
206
  return `${shortenPath(relativePath)}: ${oldStringSnippet} => ${newStringSnippet}`;
@@ -259,21 +210,10 @@ Expectation for required parameters:
259
210
  * @param params Parameters for the edit operation
260
211
  * @returns Result of the edit operation
261
212
  */
262
- async execute(params, signal) {
263
- const validationError = this.validateToolParams(params);
264
- if (validationError) {
265
- return {
266
- llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
267
- returnDisplay: `Error: ${validationError}`,
268
- error: {
269
- message: validationError,
270
- type: ToolErrorType.INVALID_TOOL_PARAMS,
271
- },
272
- };
273
- }
213
+ async execute(signal) {
274
214
  let editData;
275
215
  try {
276
- editData = await this.calculateEdit(params, signal);
216
+ editData = await this.calculateEdit(this.params, signal);
277
217
  }
278
218
  catch (error) {
279
219
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -297,37 +237,62 @@ Expectation for required parameters:
297
237
  };
298
238
  }
299
239
  try {
300
- this.ensureParentDirectoriesExist(params.file_path);
301
- fs.writeFileSync(params.file_path, editData.newContent, 'utf8');
240
+ this.ensureParentDirectoriesExist(this.params.file_path);
241
+ fs.writeFileSync(this.params.file_path, editData.newContent, 'utf8');
242
+ // Track git stats if logging is enabled and service is available
243
+ let gitStats = null;
244
+ if (this.config.getConversationLoggingEnabled()) {
245
+ const gitStatsService = getGitStatsService();
246
+ if (gitStatsService) {
247
+ try {
248
+ gitStats = await gitStatsService.trackFileEdit(this.params.file_path, editData.currentContent || '', editData.newContent);
249
+ }
250
+ catch (error) {
251
+ // Don't fail the edit if git stats tracking fails
252
+ console.warn('Failed to track git stats:', error);
253
+ }
254
+ }
255
+ }
302
256
  let displayResult;
303
257
  if (editData.isNewFile) {
304
- displayResult = `Created ${shortenPath(makeRelative(params.file_path, this.config.getTargetDir()))}`;
258
+ displayResult = `Created ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`;
305
259
  }
306
260
  else {
307
261
  // Generate diff for display, even though core logic doesn't technically need it
308
262
  // The CLI wrapper will use this part of the ToolResult
309
- const fileName = path.basename(params.file_path);
263
+ const fileName = path.basename(this.params.file_path);
310
264
  const fileDiff = Diff.createPatch(fileName, editData.currentContent ?? '', // Should not be null here if not isNewFile
311
265
  editData.newContent, 'Current', 'Proposed', DEFAULT_DIFF_OPTIONS);
266
+ const originallyProposedContent = this.params.ai_proposed_string || this.params.new_string;
267
+ const diffStat = getDiffStat(fileName, editData.currentContent ?? '', originallyProposedContent, this.params.new_string);
312
268
  displayResult = {
313
269
  fileDiff,
314
270
  fileName,
315
271
  originalContent: editData.currentContent,
316
272
  newContent: editData.newContent,
273
+ diffStat,
317
274
  };
318
275
  }
319
276
  const llmSuccessMessageParts = [
320
277
  editData.isNewFile
321
- ? `Created new file: ${params.file_path} with provided content.`
322
- : `Successfully modified file: ${params.file_path} (${editData.occurrences} replacements).`,
278
+ ? `Created new file: ${this.params.file_path} with provided content.`
279
+ : `Successfully modified file: ${this.params.file_path} (${editData.occurrences} replacements).`,
323
280
  ];
324
- if (params.modified_by_user) {
325
- llmSuccessMessageParts.push(`User modified the \`new_string\` content to be: ${params.new_string}.`);
281
+ if (this.params.modified_by_user) {
282
+ llmSuccessMessageParts.push(`User modified the \`new_string\` content to be: ${this.params.new_string}.`);
326
283
  }
327
- return {
284
+ const result = {
328
285
  llmContent: llmSuccessMessageParts.join(' '),
329
286
  returnDisplay: displayResult,
330
287
  };
288
+ // Include git stats in metadata if available
289
+ if (gitStats) {
290
+ result.metadata = {
291
+ ...result.metadata,
292
+ gitStats,
293
+ };
294
+ }
295
+ return result;
331
296
  }
332
297
  catch (error) {
333
298
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -350,6 +315,72 @@ Expectation for required parameters:
350
315
  fs.mkdirSync(dirName, { recursive: true });
351
316
  }
352
317
  }
318
+ }
319
+ /**
320
+ * Implementation of the Edit tool logic
321
+ */
322
+ export class EditTool extends BaseDeclarativeTool {
323
+ config;
324
+ static Name = 'replace';
325
+ constructor(config) {
326
+ 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.
327
+
328
+ The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response.
329
+
330
+ Expectation for required parameters:
331
+ 1. \`file_path\` MUST be an absolute path; otherwise an error will be thrown.
332
+ 2. \`old_string\` MUST be the exact literal text to replace (including all whitespace, indentation, newlines, and surrounding code etc.).
333
+ 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.
334
+ 4. NEVER escape \`old_string\` or \`new_string\`, that would break the exact literal text requirement.
335
+ **Important:** If ANY of the above are not satisfied, the tool will fail. CRITICAL for \`old_string\`: Must uniquely identify the single instance to change. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string matches multiple locations, or does not match exactly, the tool will fail.
336
+ **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.`, Icon.Pencil, {
337
+ properties: {
338
+ file_path: {
339
+ description: "The absolute path to the file to modify. Must start with '/'.",
340
+ type: Type.STRING,
341
+ },
342
+ old_string: {
343
+ 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.',
344
+ type: Type.STRING,
345
+ },
346
+ new_string: {
347
+ description: 'The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic.',
348
+ type: Type.STRING,
349
+ },
350
+ expected_replacements: {
351
+ type: Type.NUMBER,
352
+ description: 'Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.',
353
+ minimum: 1,
354
+ },
355
+ },
356
+ required: ['file_path', 'old_string', 'new_string'],
357
+ type: Type.OBJECT,
358
+ });
359
+ this.config = config;
360
+ }
361
+ /**
362
+ * Validates the parameters for the Edit tool
363
+ * @param params Parameters to validate
364
+ * @returns Error message string or null if valid
365
+ */
366
+ validateToolParams(params) {
367
+ const errors = SchemaValidator.validate(this.schema.parameters, params);
368
+ if (errors) {
369
+ return errors;
370
+ }
371
+ if (!path.isAbsolute(params.file_path)) {
372
+ return `File path must be absolute: ${params.file_path}`;
373
+ }
374
+ const workspaceContext = this.config.getWorkspaceContext();
375
+ if (!workspaceContext.isPathWithinWorkspace(params.file_path)) {
376
+ const directories = workspaceContext.getDirectories();
377
+ return `File path must be within one of the workspace directories: ${directories.join(', ')}`;
378
+ }
379
+ return null;
380
+ }
381
+ createInvocation(params) {
382
+ return new EditToolInvocation(this.config, params);
383
+ }
353
384
  getModifyContext(_) {
354
385
  return {
355
386
  getFilePath: (params) => params.file_path,
@@ -366,7 +397,7 @@ Expectation for required parameters:
366
397
  getProposedContent: async (params) => {
367
398
  try {
368
399
  const currentContent = fs.readFileSync(params.file_path, 'utf8');
369
- return this._applyReplacement(currentContent, params.old_string, params.new_string, params.old_string === '' && currentContent === '');
400
+ return applyReplacement(currentContent, params.old_string, params.new_string, params.old_string === '' && currentContent === '');
370
401
  }
371
402
  catch (err) {
372
403
  if (!isNodeError(err) || err.code !== 'ENOENT')
@@ -374,12 +405,16 @@ Expectation for required parameters:
374
405
  return '';
375
406
  }
376
407
  },
377
- createUpdatedParams: (oldContent, modifiedProposedContent, originalParams) => ({
378
- ...originalParams,
379
- old_string: oldContent,
380
- new_string: modifiedProposedContent,
381
- modified_by_user: true,
382
- }),
408
+ createUpdatedParams: (oldContent, modifiedProposedContent, originalParams) => {
409
+ const content = originalParams.new_string;
410
+ return {
411
+ ...originalParams,
412
+ ai_proposed_string: content,
413
+ old_string: oldContent,
414
+ new_string: modifiedProposedContent,
415
+ modified_by_user: true,
416
+ };
417
+ },
383
418
  };
384
419
  }
385
420
  }