@mxf-dev/core 1.0.1 → 2.0.1

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 (303) hide show
  1. package/README.md +18 -0
  2. package/dist/adapters/FrameworkAdapter.js +1 -1
  3. package/dist/adapters/FrameworkAdapter.js.map +1 -1
  4. package/dist/config/ConfigManager.d.ts +9 -8
  5. package/dist/config/ConfigManager.d.ts.map +1 -1
  6. package/dist/config/ConfigManager.js +47 -17
  7. package/dist/config/ConfigManager.js.map +1 -1
  8. package/dist/config/memory-strata.config.d.ts +25 -0
  9. package/dist/config/memory-strata.config.d.ts.map +1 -0
  10. package/dist/config/memory-strata.config.js +107 -0
  11. package/dist/config/memory-strata.config.js.map +1 -0
  12. package/dist/constants/ToolNames.d.ts +1 -29
  13. package/dist/constants/ToolNames.d.ts.map +1 -1
  14. package/dist/constants/ToolNames.js +4 -17
  15. package/dist/constants/ToolNames.js.map +1 -1
  16. package/dist/database/DatabaseAdapterFactory.d.ts +14 -2
  17. package/dist/database/DatabaseAdapterFactory.d.ts.map +1 -1
  18. package/dist/database/DatabaseAdapterFactory.js +21 -6
  19. package/dist/database/DatabaseAdapterFactory.js.map +1 -1
  20. package/dist/database/adapters/mongodb/MongoBaseRepository.d.ts +7 -1
  21. package/dist/database/adapters/mongodb/MongoBaseRepository.d.ts.map +1 -1
  22. package/dist/database/adapters/mongodb/MongoBaseRepository.js +12 -2
  23. package/dist/database/adapters/mongodb/MongoBaseRepository.js.map +1 -1
  24. package/dist/events/ClientEventBus.d.ts +16 -77
  25. package/dist/events/ClientEventBus.d.ts.map +1 -1
  26. package/dist/events/ClientEventBus.js +38 -311
  27. package/dist/events/ClientEventBus.js.map +1 -1
  28. package/dist/events/EventBus.d.ts +1 -1
  29. package/dist/events/EventBus.d.ts.map +1 -1
  30. package/dist/events/EventBus.js +5 -4
  31. package/dist/events/EventBus.js.map +1 -1
  32. package/dist/events/EventBusBase.d.ts +139 -34
  33. package/dist/events/EventBusBase.d.ts.map +1 -1
  34. package/dist/events/EventBusBase.js +366 -88
  35. package/dist/events/EventBusBase.js.map +1 -1
  36. package/dist/events/EventNames.d.ts +10 -1
  37. package/dist/events/EventNames.d.ts.map +1 -1
  38. package/dist/events/EventNames.js +3 -0
  39. package/dist/events/EventNames.js.map +1 -1
  40. package/dist/events/PublicEvents.d.ts +6 -2
  41. package/dist/events/PublicEvents.d.ts.map +1 -1
  42. package/dist/events/PublicEvents.js +7 -4
  43. package/dist/events/PublicEvents.js.map +1 -1
  44. package/dist/events/ServerEventBus.d.ts +23 -112
  45. package/dist/events/ServerEventBus.d.ts.map +1 -1
  46. package/dist/events/ServerEventBus.js +58 -323
  47. package/dist/events/ServerEventBus.js.map +1 -1
  48. package/dist/events/event-definitions/LlmBudgetEvents.d.ts +68 -0
  49. package/dist/events/event-definitions/LlmBudgetEvents.d.ts.map +1 -0
  50. package/dist/events/event-definitions/LlmBudgetEvents.js +35 -0
  51. package/dist/events/event-definitions/LlmBudgetEvents.js.map +1 -0
  52. package/dist/interfaces/IMemoryPersistence.d.ts +41 -0
  53. package/dist/interfaces/IMemoryPersistence.d.ts.map +1 -0
  54. package/dist/interfaces/IMemoryPersistence.js +15 -0
  55. package/dist/interfaces/IMemoryPersistence.js.map +1 -0
  56. package/dist/middleware/MxpMiddleware.d.ts +8 -1
  57. package/dist/middleware/MxpMiddleware.d.ts.map +1 -1
  58. package/dist/middleware/MxpMiddleware.js +42 -20
  59. package/dist/middleware/MxpMiddleware.js.map +1 -1
  60. package/dist/models/channelKey.d.ts +1 -0
  61. package/dist/models/channelKey.d.ts.map +1 -1
  62. package/dist/models/channelKey.js +5 -0
  63. package/dist/models/channelKey.js.map +1 -1
  64. package/dist/models/memoryUtility.d.ts +39 -0
  65. package/dist/models/memoryUtility.d.ts.map +1 -0
  66. package/dist/models/memoryUtility.js +39 -0
  67. package/dist/models/memoryUtility.js.map +1 -0
  68. package/dist/models/user.d.ts.map +1 -1
  69. package/dist/models/user.js +23 -1
  70. package/dist/models/user.js.map +1 -1
  71. package/dist/mxp/BinaryProtocolLayer.d.ts +24 -8
  72. package/dist/mxp/BinaryProtocolLayer.d.ts.map +1 -1
  73. package/dist/mxp/BinaryProtocolLayer.js +90 -103
  74. package/dist/mxp/BinaryProtocolLayer.js.map +1 -1
  75. package/dist/mxp/ContextCompressionEngine.d.ts +57 -10
  76. package/dist/mxp/ContextCompressionEngine.d.ts.map +1 -1
  77. package/dist/mxp/ContextCompressionEngine.js +170 -128
  78. package/dist/mxp/ContextCompressionEngine.js.map +1 -1
  79. package/dist/mxp/MxpTokenOptimizer.d.ts +7 -3
  80. package/dist/mxp/MxpTokenOptimizer.d.ts.map +1 -1
  81. package/dist/mxp/MxpTokenOptimizer.js +8 -5
  82. package/dist/mxp/MxpTokenOptimizer.js.map +1 -1
  83. package/dist/prompts/MxfAgentSystemPrompt.d.ts +1 -1
  84. package/dist/prompts/MxfAgentSystemPrompt.d.ts.map +1 -1
  85. package/dist/prompts/MxfAgentSystemPrompt.js +47 -57
  86. package/dist/prompts/MxfAgentSystemPrompt.js.map +1 -1
  87. package/dist/protocols/mcp/ToolError.d.ts +123 -0
  88. package/dist/protocols/mcp/ToolError.d.ts.map +1 -0
  89. package/dist/protocols/mcp/ToolError.js +108 -0
  90. package/dist/protocols/mcp/ToolError.js.map +1 -0
  91. package/dist/protocols/mcp/defineTool.d.ts +133 -0
  92. package/dist/protocols/mcp/defineTool.d.ts.map +1 -0
  93. package/dist/protocols/mcp/defineTool.js +236 -0
  94. package/dist/protocols/mcp/defineTool.js.map +1 -0
  95. package/dist/protocols/mcp/security/HttpTargetGuard.d.ts +38 -0
  96. package/dist/protocols/mcp/security/HttpTargetGuard.d.ts.map +1 -0
  97. package/dist/protocols/mcp/security/HttpTargetGuard.js +227 -0
  98. package/dist/protocols/mcp/security/HttpTargetGuard.js.map +1 -0
  99. package/dist/protocols/mcp/security/McpSecurityGuard.d.ts +9 -1
  100. package/dist/protocols/mcp/security/McpSecurityGuard.d.ts.map +1 -1
  101. package/dist/protocols/mcp/security/McpSecurityGuard.js +10 -2
  102. package/dist/protocols/mcp/security/McpSecurityGuard.js.map +1 -1
  103. package/dist/protocols/mcp/security/McpToolPolicy.d.ts +81 -0
  104. package/dist/protocols/mcp/security/McpToolPolicy.d.ts.map +1 -0
  105. package/dist/protocols/mcp/security/McpToolPolicy.js +202 -0
  106. package/dist/protocols/mcp/security/McpToolPolicy.js.map +1 -0
  107. package/dist/protocols/mcp/services/ExternalMcpServerManager.d.ts +51 -18
  108. package/dist/protocols/mcp/services/ExternalMcpServerManager.d.ts.map +1 -1
  109. package/dist/protocols/mcp/services/ExternalMcpServerManager.js +256 -283
  110. package/dist/protocols/mcp/services/ExternalMcpServerManager.js.map +1 -1
  111. package/dist/protocols/mcp/services/ExternalServerConfigs.d.ts +39 -6
  112. package/dist/protocols/mcp/services/ExternalServerConfigs.d.ts.map +1 -1
  113. package/dist/protocols/mcp/services/ExternalServerConfigs.js +76 -18
  114. package/dist/protocols/mcp/services/ExternalServerConfigs.js.map +1 -1
  115. package/dist/protocols/mcp/tools/AnalyticsTools.js +1 -1
  116. package/dist/protocols/mcp/tools/AnalyticsTools.js.map +1 -1
  117. package/dist/protocols/mcp/tools/CodeAnalysisTools.d.ts.map +1 -1
  118. package/dist/protocols/mcp/tools/CodeAnalysisTools.js +15 -0
  119. package/dist/protocols/mcp/tools/CodeAnalysisTools.js.map +1 -1
  120. package/dist/protocols/mcp/tools/EffectivenessTools.js +3 -3
  121. package/dist/protocols/mcp/tools/EffectivenessTools.js.map +1 -1
  122. package/dist/protocols/mcp/tools/GitTools.d.ts +9 -369
  123. package/dist/protocols/mcp/tools/GitTools.d.ts.map +1 -1
  124. package/dist/protocols/mcp/tools/GitTools.js +222 -517
  125. package/dist/protocols/mcp/tools/GitTools.js.map +1 -1
  126. package/dist/protocols/mcp/tools/InfrastructureTools.d.ts +56 -24
  127. package/dist/protocols/mcp/tools/InfrastructureTools.d.ts.map +1 -1
  128. package/dist/protocols/mcp/tools/InfrastructureTools.js +117 -26
  129. package/dist/protocols/mcp/tools/InfrastructureTools.js.map +1 -1
  130. package/dist/protocols/mcp/tools/MemorySearchTools.d.ts +0 -3
  131. package/dist/protocols/mcp/tools/MemorySearchTools.d.ts.map +1 -1
  132. package/dist/protocols/mcp/tools/MemorySearchTools.js +58 -1
  133. package/dist/protocols/mcp/tools/MemorySearchTools.js.map +1 -1
  134. package/dist/protocols/mcp/tools/OrparTools.d.ts +8 -0
  135. package/dist/protocols/mcp/tools/OrparTools.d.ts.map +1 -1
  136. package/dist/protocols/mcp/tools/OrparTools.js +66 -0
  137. package/dist/protocols/mcp/tools/OrparTools.js.map +1 -1
  138. package/dist/protocols/mcp/tools/SafetyTools.d.ts +7 -525
  139. package/dist/protocols/mcp/tools/SafetyTools.d.ts.map +1 -1
  140. package/dist/protocols/mcp/tools/SafetyTools.js +349 -514
  141. package/dist/protocols/mcp/tools/SafetyTools.js.map +1 -1
  142. package/dist/protocols/mcp/tools/TaskPlanningTools.d.ts.map +1 -1
  143. package/dist/protocols/mcp/tools/TaskPlanningTools.js +56 -26
  144. package/dist/protocols/mcp/tools/TaskPlanningTools.js.map +1 -1
  145. package/dist/protocols/mcp/tools/TestTools.d.ts +15 -376
  146. package/dist/protocols/mcp/tools/TestTools.d.ts.map +1 -1
  147. package/dist/protocols/mcp/tools/TestTools.js +241 -363
  148. package/dist/protocols/mcp/tools/TestTools.js.map +1 -1
  149. package/dist/protocols/mcp/tools/TypeScriptTools.d.ts +6 -276
  150. package/dist/protocols/mcp/tools/TypeScriptTools.d.ts.map +1 -1
  151. package/dist/protocols/mcp/tools/TypeScriptTools.js +223 -337
  152. package/dist/protocols/mcp/tools/TypeScriptTools.js.map +1 -1
  153. package/dist/protocols/mcp/tools/WebTools.d.ts +15 -728
  154. package/dist/protocols/mcp/tools/WebTools.d.ts.map +1 -1
  155. package/dist/protocols/mcp/tools/WebTools.js +151 -257
  156. package/dist/protocols/mcp/tools/WebTools.js.map +1 -1
  157. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.d.ts +5 -3
  158. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.d.ts.map +1 -1
  159. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.js +31 -6
  160. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.js.map +1 -1
  161. package/dist/schemas/EventPayloadSchema.d.ts +21 -0
  162. package/dist/schemas/EventPayloadSchema.d.ts.map +1 -1
  163. package/dist/schemas/EventPayloadSchema.js +14 -0
  164. package/dist/schemas/EventPayloadSchema.js.map +1 -1
  165. package/dist/services/BackgroundTaskManager.d.ts.map +1 -1
  166. package/dist/services/BackgroundTaskManager.js +12 -4
  167. package/dist/services/BackgroundTaskManager.js.map +1 -1
  168. package/dist/services/BrowserManager.d.ts +1 -1
  169. package/dist/services/BrowserManager.d.ts.map +1 -1
  170. package/dist/services/BrowserManager.js +26 -3
  171. package/dist/services/BrowserManager.js.map +1 -1
  172. package/dist/services/MemoryCompressor.d.ts +11 -2
  173. package/dist/services/MemoryCompressor.d.ts.map +1 -1
  174. package/dist/services/MemoryCompressor.js +8 -26
  175. package/dist/services/MemoryCompressor.js.map +1 -1
  176. package/dist/services/MemoryService.d.ts +16 -3
  177. package/dist/services/MemoryService.d.ts.map +1 -1
  178. package/dist/services/MemoryService.js +79 -55
  179. package/dist/services/MemoryService.js.map +1 -1
  180. package/dist/services/MxfMLService.d.ts +5 -0
  181. package/dist/services/MxfMLService.d.ts.map +1 -1
  182. package/dist/services/MxfMLService.js +32 -6
  183. package/dist/services/MxfMLService.js.map +1 -1
  184. package/dist/services/PredictiveAnalyticsService.d.ts +45 -9
  185. package/dist/services/PredictiveAnalyticsService.d.ts.map +1 -1
  186. package/dist/services/PredictiveAnalyticsService.js +82 -44
  187. package/dist/services/PredictiveAnalyticsService.js.map +1 -1
  188. package/dist/services/QValueManager.d.ts +7 -0
  189. package/dist/services/QValueManager.d.ts.map +1 -1
  190. package/dist/services/QValueManager.js +9 -0
  191. package/dist/services/QValueManager.js.map +1 -1
  192. package/dist/services/RewardSignalProcessor.d.ts +38 -1
  193. package/dist/services/RewardSignalProcessor.d.ts.map +1 -1
  194. package/dist/services/RewardSignalProcessor.js +109 -7
  195. package/dist/services/RewardSignalProcessor.js.map +1 -1
  196. package/dist/services/SharedStateService.d.ts +16 -2
  197. package/dist/services/SharedStateService.d.ts.map +1 -1
  198. package/dist/services/SharedStateService.js +44 -3
  199. package/dist/services/SharedStateService.js.map +1 -1
  200. package/dist/services/TaskEffectivenessService.d.ts +34 -18
  201. package/dist/services/TaskEffectivenessService.d.ts.map +1 -1
  202. package/dist/services/TaskEffectivenessService.js +91 -155
  203. package/dist/services/TaskEffectivenessService.js.map +1 -1
  204. package/dist/services/WebSearchService.d.ts.map +1 -1
  205. package/dist/services/WebSearchService.js.map +1 -1
  206. package/dist/services/dag/TaskDagService.d.ts.map +1 -1
  207. package/dist/services/dag/TaskDagService.js +2 -0
  208. package/dist/services/dag/TaskDagService.js.map +1 -1
  209. package/dist/services/orpar-memory/OrparMemoryCoordinator.d.ts +7 -0
  210. package/dist/services/orpar-memory/OrparMemoryCoordinator.d.ts.map +1 -1
  211. package/dist/services/orpar-memory/OrparMemoryCoordinator.js +71 -10
  212. package/dist/services/orpar-memory/OrparMemoryCoordinator.js.map +1 -1
  213. package/dist/types/MemoryUtilityTypes.d.ts +2 -0
  214. package/dist/types/MemoryUtilityTypes.d.ts.map +1 -1
  215. package/dist/types/MemoryUtilityTypes.js.map +1 -1
  216. package/dist/utils/Logger.d.ts +43 -3
  217. package/dist/utils/Logger.d.ts.map +1 -1
  218. package/dist/utils/Logger.js +78 -36
  219. package/dist/utils/Logger.js.map +1 -1
  220. package/dist/utils/MxpEncryption.d.ts +19 -6
  221. package/dist/utils/MxpEncryption.d.ts.map +1 -1
  222. package/dist/utils/MxpEncryption.js +48 -16
  223. package/dist/utils/MxpEncryption.js.map +1 -1
  224. package/package.json +19 -4
  225. package/src/adapters/FrameworkAdapter.ts +1 -1
  226. package/src/config/ConfigManager.ts +52 -19
  227. package/src/config/memory-strata.config.ts +136 -0
  228. package/src/constants/ToolNames.ts +4 -18
  229. package/src/database/DatabaseAdapterFactory.ts +23 -6
  230. package/src/database/adapters/mongodb/MongoBaseRepository.ts +16 -2
  231. package/src/events/ClientEventBus.ts +68 -403
  232. package/src/events/EventBus.ts +7 -8
  233. package/src/events/EventBusBase.ts +479 -116
  234. package/src/events/EventNames.ts +6 -0
  235. package/src/events/PublicEvents.ts +7 -4
  236. package/src/events/ServerEventBus.ts +78 -473
  237. package/src/events/event-definitions/LlmBudgetEvents.ts +74 -0
  238. package/src/interfaces/IMemoryPersistence.ts +51 -0
  239. package/src/middleware/MxpMiddleware.ts +51 -24
  240. package/src/models/channelKey.ts +18 -4
  241. package/src/models/memoryUtility.ts +64 -0
  242. package/src/models/user.ts +36 -4
  243. package/src/mxp/BinaryProtocolLayer.ts +108 -124
  244. package/src/mxp/ContextCompressionEngine.ts +219 -161
  245. package/src/mxp/MxpTokenOptimizer.ts +10 -10
  246. package/src/prompts/MxfAgentSystemPrompt.ts +45 -55
  247. package/src/protocols/mcp/ToolError.ts +201 -0
  248. package/src/protocols/mcp/defineTool.ts +379 -0
  249. package/src/protocols/mcp/security/HttpTargetGuard.ts +267 -0
  250. package/src/protocols/mcp/security/McpSecurityGuard.ts +10 -3
  251. package/src/protocols/mcp/security/McpToolPolicy.ts +221 -0
  252. package/src/protocols/mcp/services/ExternalMcpServerManager.ts +340 -322
  253. package/src/protocols/mcp/services/ExternalServerConfigs.ts +82 -19
  254. package/src/protocols/mcp/tools/AnalyticsTools.ts +1 -1
  255. package/src/protocols/mcp/tools/CodeAnalysisTools.ts +15 -0
  256. package/src/protocols/mcp/tools/EffectivenessTools.ts +3 -3
  257. package/src/protocols/mcp/tools/GitTools.ts +290 -569
  258. package/src/protocols/mcp/tools/InfrastructureTools.ts +189 -43
  259. package/src/protocols/mcp/tools/MemorySearchTools.ts +77 -1
  260. package/src/protocols/mcp/tools/OrparTools.ts +87 -0
  261. package/src/protocols/mcp/tools/SafetyTools.ts +501 -579
  262. package/src/protocols/mcp/tools/TaskPlanningTools.ts +81 -39
  263. package/src/protocols/mcp/tools/TestTools.ts +339 -419
  264. package/src/protocols/mcp/tools/TypeScriptTools.ts +316 -376
  265. package/src/protocols/mcp/tools/WebTools.ts +209 -307
  266. package/src/protocols/mcp/tools/shell/ShellExecuteHandler.ts +53 -14
  267. package/src/schemas/EventPayloadSchema.ts +37 -0
  268. package/src/services/BackgroundTaskManager.ts +12 -4
  269. package/src/services/BrowserManager.ts +41 -1
  270. package/src/services/MemoryCompressor.ts +20 -32
  271. package/src/services/MemoryService.ts +96 -58
  272. package/src/services/MxfMLService.ts +41 -7
  273. package/src/services/PredictiveAnalyticsService.ts +101 -51
  274. package/src/services/QValueManager.ts +10 -0
  275. package/src/services/RewardSignalProcessor.ts +138 -9
  276. package/src/services/SharedStateService.ts +56 -8
  277. package/src/services/TaskEffectivenessService.ts +114 -187
  278. package/src/services/WebSearchService.ts +2 -1
  279. package/src/services/dag/TaskDagService.ts +2 -0
  280. package/src/services/orpar-memory/OrparMemoryCoordinator.ts +86 -10
  281. package/src/types/MemoryUtilityTypes.ts +2 -0
  282. package/src/utils/Logger.ts +116 -40
  283. package/src/utils/MxpEncryption.ts +69 -34
  284. package/dist/mxp/MxpEventForwardingEnhancer.d.ts +0 -134
  285. package/dist/mxp/MxpEventForwardingEnhancer.d.ts.map +0 -1
  286. package/dist/mxp/MxpEventForwardingEnhancer.js +0 -361
  287. package/dist/mxp/MxpEventForwardingEnhancer.js.map +0 -1
  288. package/dist/protocols/mcp/services/ToolSchemaEnhancements.d.ts +0 -56
  289. package/dist/protocols/mcp/services/ToolSchemaEnhancements.d.ts.map +0 -1
  290. package/dist/protocols/mcp/services/ToolSchemaEnhancements.js +0 -57
  291. package/dist/protocols/mcp/services/ToolSchemaEnhancements.js.map +0 -1
  292. package/dist/protocols/mcp/tools/ControlLoopLifecycle.d.ts +0 -270
  293. package/dist/protocols/mcp/tools/ControlLoopLifecycle.d.ts.map +0 -1
  294. package/dist/protocols/mcp/tools/ControlLoopLifecycle.js +0 -256
  295. package/dist/protocols/mcp/tools/ControlLoopLifecycle.js.map +0 -1
  296. package/dist/protocols/mcp/tools/LspTools.d.ts +0 -495
  297. package/dist/protocols/mcp/tools/LspTools.d.ts.map +0 -1
  298. package/dist/protocols/mcp/tools/LspTools.js +0 -469
  299. package/dist/protocols/mcp/tools/LspTools.js.map +0 -1
  300. package/src/mxp/MxpEventForwardingEnhancer.ts +0 -471
  301. package/src/protocols/mcp/services/ToolSchemaEnhancements.ts +0 -92
  302. package/src/protocols/mcp/tools/ControlLoopLifecycle.ts +0 -302
  303. package/src/protocols/mcp/tools/LspTools.ts +0 -476
@@ -19,7 +19,10 @@
19
19
  */
20
20
 
21
21
  import { Logger } from '../../../utils/Logger.js';
22
- import { executeShellCommand } from './InfrastructureTools.js';
22
+ import { TOOL_CATEGORIES } from '../../../constants/ToolNames.js';
23
+ import { defineTool, ToolRunContext } from '../defineTool.js';
24
+ import { ToolError } from '../ToolError.js';
25
+ import { executeShellCommand, ShellCommandResult } from './InfrastructureTools.js';
23
26
  import * as fs from 'fs/promises';
24
27
  import * as path from 'path';
25
28
 
@@ -340,19 +343,61 @@ function calculateReviewScore(issues: any[], strictness: string): number {
340
343
 
341
344
  /**
342
345
  * Safety Tools for MXF
343
- *
344
- * Provides safety mechanisms for self-modification including rollback,
345
- * backup, and validation tools to ensure safe autonomous code changes.
346
+ *
347
+ * Branch, backup, rollback, test and benchmark tools used to keep autonomous
348
+ * code changes recoverable.
349
+ *
350
+ * Every git invocation here goes through executeShellCommand, so it is validated
351
+ * by the security guard first. That matters most for rollback_changes, which
352
+ * runs `git reset --hard` and `git checkout` — the guard is what stands between
353
+ * a model's rollback request and an unrecoverable working tree.
346
354
  */
347
355
 
348
- export const createFeatureBranchTool = {
356
+ /** Run a git subcommand, throwing a ToolError when git reports failure. */
357
+ async function runGit(
358
+ args: string[],
359
+ workingDirectory: string,
360
+ context: ToolRunContext
361
+ ): Promise<ShellCommandResult> {
362
+ const result = await executeShellCommand('git', args, {
363
+ context: {
364
+ agentId: context.agentId,
365
+ channelId: context.channelId,
366
+ requestId: context.requestId
367
+ },
368
+ workingDirectory,
369
+ captureOutput: true
370
+ });
371
+
372
+ if (result.exitCode !== 0) {
373
+ throw ToolError.executionFailed(
374
+ `git ${args.join(' ')} failed (exit ${result.exitCode}): ` +
375
+ `${result.stderr?.trim() || 'no error output'}`,
376
+ { details: { exitCode: result.exitCode, args } }
377
+ );
378
+ }
379
+
380
+ return result;
381
+ }
382
+
383
+ const workingDirectoryProperty = {
384
+ type: 'string',
385
+ description: 'Working directory (defaults to the server working directory)'
386
+ };
387
+
388
+ export const createFeatureBranchTool = defineTool<
389
+ { branchName: string; baseBranch?: string; workingDirectory?: string },
390
+ { branchName: string; baseBranch: string; headCommit: string; pulled: boolean }
391
+ >({
349
392
  name: 'create_feature_branch',
350
- description: 'Create a new feature branch to isolate experimental changes',
393
+ category: TOOL_CATEGORIES.VERSION_CONTROL,
394
+ description: 'Create and switch to a new branch off a base branch, to isolate changes.',
351
395
  inputSchema: {
352
396
  type: 'object',
353
397
  properties: {
354
398
  branchName: {
355
399
  type: 'string',
400
+ minLength: 1,
356
401
  description: 'Name for the new feature branch'
357
402
  },
358
403
  baseBranch: {
@@ -360,294 +405,284 @@ export const createFeatureBranchTool = {
360
405
  default: 'main',
361
406
  description: 'Base branch to create the feature branch from'
362
407
  },
363
- workingDirectory: {
364
- type: 'string',
365
- description: 'Working directory (defaults to current directory)'
366
- }
408
+ workingDirectory: workingDirectoryProperty
367
409
  },
368
410
  required: ['branchName']
369
411
  },
370
- handler: async (args: any, context: any) => {
371
-
412
+ run: async (input, context) => {
413
+ const workingDir = input.workingDirectory || process.cwd();
414
+ const baseBranch = input.baseBranch ?? 'main';
415
+
416
+ await runGit(['checkout', baseBranch], workingDir, context);
417
+
418
+ // A pull can fail for reasons that do not invalidate the branch (no
419
+ // remote, offline). Record whether it worked rather than failing the tool
420
+ // or pretending it succeeded.
421
+ let pulled = true;
372
422
  try {
373
- const workingDir = args.workingDirectory || process.cwd();
374
-
375
- // Ensure we're on the base branch and it's up to date
376
- const checkoutResult = await executeShellCommand('git', ['checkout', args.baseBranch || 'main'], {
377
- workingDirectory: workingDir,
378
- captureOutput: true
379
- });
380
-
381
- if (checkoutResult.exitCode !== 0) {
382
- return {
383
- success: false,
384
- branchName: args.branchName,
385
- error: `Failed to checkout base branch: ${checkoutResult.stderr}`
386
- };
387
- }
388
-
389
- // Pull latest changes
390
- const pullResult = await executeShellCommand('git', ['pull', 'origin', args.baseBranch || 'main'], {
391
- workingDirectory: workingDir,
392
- captureOutput: true
393
- });
394
-
395
- if (pullResult.exitCode !== 0) {
396
- logger.warn('Failed to pull latest changes', { error: pullResult.stderr });
397
- }
398
-
399
- // Create and checkout new branch
400
- const branchResult = await executeShellCommand('git', ['checkout', '-b', args.branchName], {
401
- workingDirectory: workingDir,
402
- captureOutput: true
403
- });
404
-
405
- if (branchResult.exitCode !== 0) {
406
- return {
407
- success: false,
408
- branchName: args.branchName,
409
- error: `Failed to create branch: ${branchResult.stderr}`
410
- };
411
- }
412
-
413
- // Get current commit hash for reference
414
- const hashResult = await executeShellCommand('git', ['rev-parse', 'HEAD'], {
415
- workingDirectory: workingDir,
416
- captureOutput: true
417
- });
418
-
419
- const currentHash = hashResult.stdout?.trim() || 'unknown';
420
-
421
- return {
422
- success: true,
423
- branchName: args.branchName,
424
- baseBranch: args.baseBranch || 'main',
425
- currentHash,
426
- error: null
427
- };
423
+ await runGit(['pull', 'origin', baseBranch], workingDir, context);
428
424
  } catch (error) {
429
- logger.error('Create feature branch failed', { error });
430
- return {
431
- success: false,
432
- branchName: args.branchName,
433
- error: error instanceof Error ? error.message : String(error)
434
- };
425
+ pulled = false;
426
+ logger.warn(
427
+ `Could not pull ${baseBranch} before branching: ` +
428
+ `${error instanceof Error ? error.message : String(error)}`
429
+ );
435
430
  }
431
+
432
+ await runGit(['checkout', '-b', input.branchName], workingDir, context);
433
+
434
+ const hashResult = await runGit(['rev-parse', 'HEAD'], workingDir, context);
435
+
436
+ return {
437
+ branchName: input.branchName,
438
+ baseBranch,
439
+ headCommit: hashResult.stdout?.trim() ?? '',
440
+ pulled
441
+ };
436
442
  }
437
- };
443
+ });
438
444
 
439
- export const runFullTestSuiteTool = {
445
+ export const runFullTestSuiteTool = defineTool<
446
+ { testCommand?: string; coverage?: boolean; bail?: boolean; workingDirectory?: string },
447
+ {
448
+ passed: boolean;
449
+ command: string;
450
+ durationMs: number;
451
+ testResults: ReturnType<typeof parseTestResults>;
452
+ coverage: ReturnType<typeof parseCoverageResults> | null;
453
+ output: string;
454
+ }
455
+ >({
440
456
  name: 'run_full_test_suite',
441
- description: 'Run the complete test suite to validate changes',
457
+ category: TOOL_CATEGORIES.INFRASTRUCTURE,
458
+ description:
459
+ 'Run the project test suite. passed is false when tests fail. ' +
460
+ 'Uses the project test script unless a command is given.',
442
461
  inputSchema: {
443
462
  type: 'object',
444
463
  properties: {
445
464
  testCommand: {
446
465
  type: 'string',
447
- description: 'Custom test command (defaults to npm test)'
466
+ description: 'Test command to run (defaults to the project test script)'
448
467
  },
449
468
  coverage: {
450
469
  type: 'boolean',
451
470
  default: true,
452
- description: 'Generate test coverage report'
471
+ description: 'Generate a coverage report (jest only)'
453
472
  },
454
473
  bail: {
455
474
  type: 'boolean',
456
475
  default: false,
457
- description: 'Stop on first test failure'
476
+ description: 'Stop at the first failure (jest only)'
458
477
  },
459
- workingDirectory: {
460
- type: 'string',
461
- description: 'Working directory (defaults to current directory)'
462
- }
478
+ workingDirectory: workingDirectoryProperty
463
479
  }
464
480
  },
465
- handler: async (args: any, context: any) => {
466
-
467
- try {
468
- const workingDir = args.workingDirectory || process.cwd();
469
-
470
- // Determine test command
471
- let testCmd: string[];
472
- if (args.testCommand) {
473
- testCmd = args.testCommand.split(' ');
474
- } else {
475
- // Try to detect test framework
476
- const packageJsonPath = path.join(workingDir, 'package.json');
477
- try {
478
- const packageJson = await fs.readFile(packageJsonPath, 'utf-8');
479
- const pkg = JSON.parse(packageJson);
480
-
481
- if (pkg.scripts?.test) {
482
- testCmd = ['npm', 'test'];
483
- } else if (pkg.devDependencies?.jest || pkg.dependencies?.jest) {
484
- testCmd = ['npx', 'jest'];
485
- } else if (pkg.devDependencies?.mocha || pkg.dependencies?.mocha) {
486
- testCmd = ['npx', 'mocha'];
487
- } else {
488
- testCmd = ['npm', 'test'];
489
- }
490
- } catch {
491
- testCmd = ['npm', 'test'];
492
- }
493
- }
494
-
495
- // Add coverage if requested
496
- if (args.coverage && testCmd.includes('jest')) {
497
- testCmd.push('--coverage');
481
+ run: async (input, context) => {
482
+ const workingDir = input.workingDirectory || process.cwd();
483
+
484
+ // Work out what to run. An explicit command wins; otherwise read
485
+ // package.json. A project with no test script and no known runner is a
486
+ // precondition failure — running `npm test` anyway would fail confusingly.
487
+ let testCmd: string[];
488
+
489
+ if (input.testCommand) {
490
+ testCmd = input.testCommand.split(/\s+/).filter(Boolean);
491
+ } else {
492
+ const packageJsonPath = path.join(workingDir, 'package.json');
493
+ let pkg: { scripts?: Record<string, string>; dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
494
+
495
+ try {
496
+ pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
497
+ } catch {
498
+ throw ToolError.notFound(
499
+ `Could not read ${packageJsonPath}. ` +
500
+ `Pass testCommand explicitly, or run this from the project root.`
501
+ );
498
502
  }
499
-
500
- // Add bail if requested
501
- if (args.bail && testCmd.includes('jest')) {
502
- testCmd.push('--bail');
503
+
504
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
505
+
506
+ if (pkg.scripts?.test) {
507
+ testCmd = ['npm', 'test'];
508
+ } else if (deps.jest) {
509
+ testCmd = ['npx', 'jest'];
510
+ } else if (deps.mocha) {
511
+ testCmd = ['npx', 'mocha'];
512
+ } else if (deps.vitest) {
513
+ testCmd = ['npx', 'vitest', 'run'];
514
+ } else {
515
+ throw ToolError.notFound(
516
+ `${packageJsonPath} has no "test" script and no jest, mocha or vitest dependency. ` +
517
+ `Pass testCommand explicitly.`
518
+ );
503
519
  }
504
-
505
- const startTime = Date.now();
506
-
507
- const result = await executeShellCommand(testCmd[0], testCmd.slice(1), {
508
- workingDirectory: workingDir,
509
- captureOutput: true
510
- });
511
-
512
- const duration = Date.now() - startTime;
513
-
514
- // Parse test results
515
- const testResults = parseTestResults(result.stdout || '');
516
-
517
- return {
518
- success: result.exitCode === 0,
519
- output: result.stdout || '',
520
- errors: result.stderr || '',
521
- duration,
522
- testResults,
523
- coverage: args.coverage ? parseCoverageResults(result.stdout || '') : null,
524
- error: result.exitCode !== 0 ? result.stderr : null
525
- };
526
- } catch (error) {
527
- logger.error('Run full test suite failed', { error });
528
- return {
529
- success: false,
530
- output: '',
531
- errors: '',
532
- duration: 0,
533
- testResults: null,
534
- coverage: null,
535
- error: error instanceof Error ? error.message : String(error)
536
- };
537
520
  }
521
+
522
+ const isJest = testCmd.includes('jest');
523
+ if (input.coverage !== false && isJest) {
524
+ testCmd.push('--coverage');
525
+ }
526
+ if (input.bail && isJest) {
527
+ testCmd.push('--bail');
528
+ }
529
+
530
+ const startedAt = Date.now();
531
+
532
+ const result = await executeShellCommand(testCmd[0], testCmd.slice(1), {
533
+ context: {
534
+ agentId: context.agentId,
535
+ channelId: context.channelId,
536
+ requestId: context.requestId
537
+ },
538
+ workingDirectory: workingDir,
539
+ captureOutput: true
540
+ });
541
+
542
+ // A failing suite exits non-zero. That is the tool working, so it comes
543
+ // back as passed: false rather than as a tool error.
544
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
545
+
546
+ return {
547
+ passed: result.exitCode === 0,
548
+ command: testCmd.join(' '),
549
+ durationMs: Date.now() - startedAt,
550
+ testResults: parseTestResults(output),
551
+ coverage: input.coverage !== false ? parseCoverageResults(output) : null,
552
+ output
553
+ };
538
554
  }
539
- };
555
+ });
540
556
 
541
- export const performanceBenchmarkTool = {
557
+ export const performanceBenchmarkTool = defineTool<
558
+ { benchmarkCommand?: string; baselineFile?: string; thresholdPercent?: number; workingDirectory?: string },
559
+ {
560
+ passed: boolean;
561
+ command: string;
562
+ durationMs: number;
563
+ benchmarkResults: ReturnType<typeof parseBenchmarkResults>;
564
+ baselineComparison: ReturnType<typeof compareWithBaseline> | null;
565
+ output: string;
566
+ }
567
+ >({
542
568
  name: 'performance_benchmark',
543
- description: 'Run performance benchmarks to ensure changes don\'t degrade performance',
569
+ category: TOOL_CATEGORIES.INFRASTRUCTURE,
570
+ description:
571
+ 'Run the project benchmark script and, when a baseline file is given, compare ' +
572
+ 'against it and flag regressions past the threshold.',
544
573
  inputSchema: {
545
574
  type: 'object',
546
575
  properties: {
547
576
  benchmarkCommand: {
548
577
  type: 'string',
549
- description: 'Custom benchmark command (defaults to npm run benchmark)'
578
+ description: 'Benchmark command (defaults to the project benchmark script)'
550
579
  },
551
580
  baselineFile: {
552
581
  type: 'string',
553
- description: 'Path to baseline performance file'
582
+ description: 'Path to a JSON baseline file to compare against'
554
583
  },
555
584
  thresholdPercent: {
556
585
  type: 'number',
557
586
  default: 10,
558
- description: 'Performance degradation threshold percentage'
587
+ minimum: 0,
588
+ description: 'Regression threshold, as a percentage'
559
589
  },
560
- workingDirectory: {
561
- type: 'string',
562
- description: 'Working directory (defaults to current directory)'
563
- }
590
+ workingDirectory: workingDirectoryProperty
564
591
  }
565
592
  },
566
- handler: async (args: any, context: any) => {
567
-
568
- try {
569
- const workingDir = args.workingDirectory || process.cwd();
570
-
571
- // Determine benchmark command
572
- let benchmarkCmd: string[];
573
- if (args.benchmarkCommand) {
574
- benchmarkCmd = args.benchmarkCommand.split(' ');
593
+ run: async (input, context) => {
594
+ const workingDir = input.workingDirectory || process.cwd();
595
+
596
+ let benchmarkCmd: string[];
597
+
598
+ if (input.benchmarkCommand) {
599
+ benchmarkCmd = input.benchmarkCommand.split(/\s+/).filter(Boolean);
600
+ } else {
601
+ const packageJsonPath = path.join(workingDir, 'package.json');
602
+ let pkg: { scripts?: Record<string, string> };
603
+
604
+ try {
605
+ pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
606
+ } catch {
607
+ throw ToolError.notFound(
608
+ `Could not read ${packageJsonPath}. Pass benchmarkCommand explicitly.`
609
+ );
610
+ }
611
+
612
+ if (pkg.scripts?.benchmark) {
613
+ benchmarkCmd = ['npm', 'run', 'benchmark'];
614
+ } else if (pkg.scripts?.perf) {
615
+ benchmarkCmd = ['npm', 'run', 'perf'];
575
616
  } else {
576
- // Check for common benchmark scripts
577
- const packageJsonPath = path.join(workingDir, 'package.json');
578
- try {
579
- const packageJson = await fs.readFile(packageJsonPath, 'utf-8');
580
- const pkg = JSON.parse(packageJson);
581
-
582
- if (pkg.scripts?.benchmark) {
583
- benchmarkCmd = ['npm', 'run', 'benchmark'];
584
- } else if (pkg.scripts?.perf) {
585
- benchmarkCmd = ['npm', 'run', 'perf'];
586
- } else {
587
- return {
588
- success: false,
589
- benchmarkResults: null,
590
- baselineComparison: null,
591
- error: 'No benchmark script found in package.json'
592
- };
593
- }
594
- } catch {
595
- return {
596
- success: false,
597
- benchmarkResults: null,
598
- baselineComparison: null,
599
- error: 'Could not read package.json'
600
- };
601
- }
617
+ throw ToolError.notFound(
618
+ `${packageJsonPath} has no "benchmark" or "perf" script. ` +
619
+ `Pass benchmarkCommand explicitly.`
620
+ );
602
621
  }
603
-
604
- const startTime = Date.now();
605
-
606
- const result = await executeShellCommand(benchmarkCmd[0], benchmarkCmd.slice(1), {
607
- workingDirectory: workingDir,
608
- captureOutput: true
609
- });
610
-
611
- const duration = Date.now() - startTime;
612
-
613
- // Parse benchmark results
614
- const benchmarkResults = parseBenchmarkResults(result.stdout || '');
615
-
616
- // Compare with baseline if provided
617
- let baselineComparison = null;
618
- if (args.baselineFile) {
619
- try {
620
- const baselineContent = await fs.readFile(args.baselineFile, 'utf-8');
621
- const baseline = JSON.parse(baselineContent);
622
- baselineComparison = compareWithBaseline(benchmarkResults, baseline, args.thresholdPercent);
623
- } catch (error) {
624
- logger.warn('Could not read baseline file', { error });
625
- }
622
+ }
623
+
624
+ const startedAt = Date.now();
625
+
626
+ const result = await executeShellCommand(benchmarkCmd[0], benchmarkCmd.slice(1), {
627
+ context: {
628
+ agentId: context.agentId,
629
+ channelId: context.channelId,
630
+ requestId: context.requestId
631
+ },
632
+ workingDirectory: workingDir,
633
+ captureOutput: true
634
+ });
635
+
636
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
637
+ const benchmarkResults = parseBenchmarkResults(output);
638
+
639
+ // A baseline the caller named but that cannot be read is an error: a
640
+ // silent null here would read as "no regression".
641
+ let baselineComparison = null;
642
+ if (input.baselineFile) {
643
+ let baseline: unknown;
644
+ try {
645
+ baseline = JSON.parse(await fs.readFile(input.baselineFile, 'utf-8'));
646
+ } catch (error) {
647
+ throw ToolError.notFound(
648
+ `Could not read the baseline file ${input.baselineFile}: ` +
649
+ `${error instanceof Error ? error.message : String(error)}`
650
+ );
626
651
  }
627
-
628
- return {
629
- success: result.exitCode === 0,
630
- output: result.stdout || '',
652
+ baselineComparison = compareWithBaseline(
631
653
  benchmarkResults,
632
- baselineComparison,
633
- duration,
634
- error: result.exitCode !== 0 ? result.stderr : null
635
- };
636
- } catch (error) {
637
- logger.error('Performance benchmark failed', { error });
638
- return {
639
- success: false,
640
- benchmarkResults: null,
641
- baselineComparison: null,
642
- error: error instanceof Error ? error.message : String(error)
643
- };
654
+ baseline,
655
+ input.thresholdPercent ?? 10
656
+ );
644
657
  }
658
+
659
+ return {
660
+ passed: result.exitCode === 0,
661
+ command: benchmarkCmd.join(' '),
662
+ durationMs: Date.now() - startedAt,
663
+ benchmarkResults,
664
+ baselineComparison,
665
+ output
666
+ };
645
667
  }
646
- };
668
+ });
647
669
 
648
- export const rollbackChangesTool = {
670
+ export const rollbackChangesTool = defineTool<
671
+ {
672
+ rollbackType?: 'commit' | 'branch' | 'stash';
673
+ targetCommit?: string;
674
+ targetBranch?: string;
675
+ preserveChanges?: boolean;
676
+ workingDirectory?: string;
677
+ },
678
+ { rollbackType: string; currentBranch: string; hasUncommittedChanges: boolean; preservedChanges: boolean }
679
+ >({
649
680
  name: 'rollback_changes',
650
- description: 'Rollback changes to a previous safe state',
681
+ category: TOOL_CATEGORIES.VERSION_CONTROL,
682
+ description:
683
+ 'Roll the repository back to a previous state: reset to a commit, switch to a ' +
684
+ 'branch, or pop the latest stash. With preserveChanges the working tree is kept ' +
685
+ '(soft reset / stash); without it, a commit rollback discards uncommitted work.',
651
686
  inputSchema: {
652
687
  type: 'object',
653
688
  properties: {
@@ -655,138 +690,90 @@ export const rollbackChangesTool = {
655
690
  type: 'string',
656
691
  enum: ['commit', 'branch', 'stash'],
657
692
  default: 'commit',
658
- description: 'Type of rollback to perform'
693
+ description: 'What to roll back to'
659
694
  },
660
695
  targetCommit: {
661
696
  type: 'string',
662
- description: 'Commit hash to rollback to (for commit rollback)'
697
+ description: 'Commit to reset to (required when rollbackType is "commit")'
663
698
  },
664
699
  targetBranch: {
665
700
  type: 'string',
666
- description: 'Branch to rollback to (for branch rollback)'
701
+ description: 'Branch to switch to (required when rollbackType is "branch")'
667
702
  },
668
703
  preserveChanges: {
669
704
  type: 'boolean',
670
705
  default: false,
671
- description: 'Preserve changes in working directory'
706
+ description: 'Keep uncommitted work (soft reset, or stash before switching)'
672
707
  },
673
- workingDirectory: {
674
- type: 'string',
675
- description: 'Working directory (defaults to current directory)'
676
- }
708
+ workingDirectory: workingDirectoryProperty
677
709
  }
678
710
  },
679
- handler: async (args: any, context: any) => {
680
-
681
- try {
682
- const workingDir = args.workingDirectory || process.cwd();
683
- let result;
684
-
685
- switch (args.rollbackType) {
686
- case 'commit':
687
- if (!args.targetCommit) {
688
- return {
689
- success: false,
690
- rollbackType: args.rollbackType,
691
- error: 'Target commit required for commit rollback'
692
- };
693
- }
694
-
695
- if (args.preserveChanges) {
696
- // Soft reset to preserve changes
697
- result = await executeShellCommand('git', ['reset', '--soft', args.targetCommit], {
698
- workingDirectory: workingDir,
699
- captureOutput: true
700
- });
701
- } else {
702
- // Hard reset to discard changes
703
- result = await executeShellCommand('git', ['reset', '--hard', args.targetCommit], {
704
- workingDirectory: workingDir,
705
- captureOutput: true
706
- });
707
- }
708
- break;
709
-
710
- case 'branch':
711
- if (!args.targetBranch) {
712
- return {
713
- success: false,
714
- rollbackType: args.rollbackType,
715
- error: 'Target branch required for branch rollback'
716
- };
717
- }
718
-
719
- // Stash current changes if preserving
720
- if (args.preserveChanges) {
721
- await executeShellCommand('git', ['stash', 'push', '-m', 'Rollback preservation stash'], {
722
- workingDirectory: workingDir,
723
- captureOutput: true
724
- });
725
- }
726
-
727
- // Checkout target branch
728
- result = await executeShellCommand('git', ['checkout', args.targetBranch], {
729
- workingDirectory: workingDir,
730
- captureOutput: true
731
- });
732
- break;
733
-
734
- case 'stash':
735
- // Pop the latest stash
736
- result = await executeShellCommand('git', ['stash', 'pop'], {
737
- workingDirectory: workingDir,
738
- captureOutput: true
739
- });
740
- break;
741
-
742
- default:
743
- return {
744
- success: false,
745
- rollbackType: args.rollbackType,
746
- error: 'Invalid rollback type'
747
- };
711
+ run: async (input, context) => {
712
+ const workingDir = input.workingDirectory || process.cwd();
713
+ const rollbackType = input.rollbackType ?? 'commit';
714
+ const preserveChanges = input.preserveChanges ?? false;
715
+
716
+ switch (rollbackType) {
717
+ case 'commit': {
718
+ if (!input.targetCommit) {
719
+ throw ToolError.invalidInput(
720
+ 'targetCommit is required when rollbackType is "commit".'
721
+ );
722
+ }
723
+ // --hard discards uncommitted work irrecoverably; --soft keeps it.
724
+ const mode = preserveChanges ? '--soft' : '--hard';
725
+ await runGit(['reset', mode, input.targetCommit], workingDir, context);
726
+ break;
748
727
  }
749
-
750
- if (result.exitCode !== 0) {
751
- return {
752
- success: false,
753
- rollbackType: args.rollbackType,
754
- error: result.stderr || 'Rollback failed'
755
- };
728
+
729
+ case 'branch': {
730
+ if (!input.targetBranch) {
731
+ throw ToolError.invalidInput(
732
+ 'targetBranch is required when rollbackType is "branch".'
733
+ );
734
+ }
735
+ if (preserveChanges) {
736
+ await runGit(
737
+ ['stash', 'push', '-m', 'Rollback preservation stash'],
738
+ workingDir,
739
+ context
740
+ );
741
+ }
742
+ await runGit(['checkout', input.targetBranch], workingDir, context);
743
+ break;
756
744
  }
757
-
758
- // Get current status after rollback
759
- const statusResult = await executeShellCommand('git', ['status', '--porcelain'], {
760
- workingDirectory: workingDir,
761
- captureOutput: true
762
- });
763
-
764
- const currentBranchResult = await executeShellCommand('git', ['branch', '--show-current'], {
765
- workingDirectory: workingDir,
766
- captureOutput: true
767
- });
768
-
769
- return {
770
- success: true,
771
- rollbackType: args.rollbackType,
772
- currentBranch: currentBranchResult.stdout?.trim() || 'unknown',
773
- hasUncommittedChanges: (statusResult.stdout || '').trim().length > 0,
774
- error: null
775
- };
776
- } catch (error) {
777
- logger.error('Rollback changes failed', { error });
778
- return {
779
- success: false,
780
- rollbackType: args.rollbackType,
781
- error: error instanceof Error ? error.message : String(error)
782
- };
745
+
746
+ case 'stash':
747
+ await runGit(['stash', 'pop'], workingDir, context);
748
+ break;
783
749
  }
750
+
751
+ const statusResult = await runGit(['status', '--porcelain'], workingDir, context);
752
+ const branchResult = await runGit(['branch', '--show-current'], workingDir, context);
753
+
754
+ return {
755
+ rollbackType,
756
+ currentBranch: branchResult.stdout?.trim() ?? '',
757
+ hasUncommittedChanges: (statusResult.stdout ?? '').trim().length > 0,
758
+ preservedChanges: preserveChanges
759
+ };
784
760
  }
785
- };
761
+ });
786
762
 
787
- export const createBackupTool = {
763
+ export const createBackupTool = defineTool<
764
+ {
765
+ backupType?: 'commit' | 'branch' | 'stash' | 'archive';
766
+ backupName?: string;
767
+ includeUntracked?: boolean;
768
+ workingDirectory?: string;
769
+ },
770
+ { backupType: string; backupName: string; backupId: string; timestamp: string }
771
+ >({
788
772
  name: 'create_backup',
789
- description: 'Create a backup of the current state before making changes',
773
+ category: TOOL_CATEGORIES.VERSION_CONTROL,
774
+ description:
775
+ 'Snapshot the current state before making changes, as a commit, a branch, a stash, ' +
776
+ 'or a tar.gz archive. Returns the identifier needed to restore it.',
790
777
  inputSchema: {
791
778
  type: 'object',
792
779
  properties: {
@@ -794,157 +781,114 @@ export const createBackupTool = {
794
781
  type: 'string',
795
782
  enum: ['commit', 'branch', 'stash', 'archive'],
796
783
  default: 'commit',
797
- description: 'Type of backup to create'
784
+ description: 'How to store the backup'
798
785
  },
799
786
  backupName: {
800
787
  type: 'string',
801
- description: 'Name for the backup'
788
+ description: 'Name for the backup (defaults to a timestamp)'
802
789
  },
803
790
  includeUntracked: {
804
791
  type: 'boolean',
805
792
  default: true,
806
- description: 'Include untracked files in backup'
793
+ description: 'Include untracked files'
807
794
  },
808
- workingDirectory: {
809
- type: 'string',
810
- description: 'Working directory (defaults to current directory)'
811
- }
795
+ workingDirectory: workingDirectoryProperty
812
796
  }
813
797
  },
814
- handler: async (args: any, context: any) => {
815
-
816
- try {
817
- const workingDir = args.workingDirectory || process.cwd();
818
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
819
- const backupName = args.backupName || `backup-${timestamp}`;
820
-
821
- let result;
822
- let backupId;
823
-
824
- switch (args.backupType) {
825
- case 'commit':
826
- // Add all changes
827
- if (args.includeUntracked) {
828
- await executeShellCommand('git', ['add', '-A'], {
829
- workingDirectory: workingDir,
830
- captureOutput: true
831
- });
832
- } else {
833
- await executeShellCommand('git', ['add', '-u'], {
834
- workingDirectory: workingDir,
835
- captureOutput: true
836
- });
837
- }
838
-
839
- // Create commit
840
- result = await executeShellCommand('git', ['commit', '-m', `Backup: ${backupName}`], {
841
- workingDirectory: workingDir,
842
- captureOutput: true
843
- });
844
-
845
- if (result.exitCode === 0) {
846
- const hashResult = await executeShellCommand('git', ['rev-parse', 'HEAD'], {
847
- workingDirectory: workingDir,
848
- captureOutput: true
849
- });
850
- backupId = hashResult.stdout?.trim();
851
- }
852
- break;
853
-
854
- case 'branch':
855
- // Create backup branch
856
- result = await executeShellCommand('git', ['checkout', '-b', backupName], {
857
- workingDirectory: workingDir,
858
- captureOutput: true
859
- });
860
-
861
- if (result.exitCode === 0) {
862
- backupId = backupName;
863
-
864
- // Switch back to original branch
865
- const originalBranchResult = await executeShellCommand('git', ['checkout', '-'], {
866
- workingDirectory: workingDir,
867
- captureOutput: true
868
- });
869
-
870
- if (originalBranchResult.exitCode !== 0) {
871
- logger.warn('Could not switch back to original branch');
872
- }
873
- }
874
- break;
875
-
876
- case 'stash':
877
- // Create stash
878
- const stashArgs = ['stash', 'push', '-m', backupName];
879
- if (args.includeUntracked) {
880
- stashArgs.push('--include-untracked');
881
- }
882
-
883
- result = await executeShellCommand('git', stashArgs, {
884
- workingDirectory: workingDir,
885
- captureOutput: true
886
- });
887
-
888
- if (result.exitCode === 0) {
889
- backupId = backupName;
890
- }
891
- break;
892
-
893
- case 'archive':
894
- // Create archive file
895
- const archiveName = `${backupName}.tar.gz`;
896
- const archivePath = path.join(workingDir, archiveName);
897
-
898
- result = await executeShellCommand('git', ['archive', '--format=tar.gz', '--output', archivePath, 'HEAD'], {
899
- workingDirectory: workingDir,
900
- captureOutput: true
901
- });
902
-
903
- if (result.exitCode === 0) {
904
- backupId = archivePath;
905
- }
906
- break;
907
-
908
- default:
909
- return {
910
- success: false,
911
- backupType: args.backupType,
912
- error: 'Invalid backup type'
913
- };
798
+ run: async (input, context) => {
799
+ const workingDir = input.workingDirectory || process.cwd();
800
+ const backupType = input.backupType ?? 'commit';
801
+ const includeUntracked = input.includeUntracked ?? true;
802
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
803
+ const backupName = input.backupName || `backup-${timestamp}`;
804
+
805
+ let backupId: string;
806
+
807
+ switch (backupType) {
808
+ case 'commit': {
809
+ await runGit(['add', includeUntracked ? '-A' : '-u'], workingDir, context);
810
+ await runGit(['commit', '-m', `Backup: ${backupName}`], workingDir, context);
811
+
812
+ const hashResult = await runGit(['rev-parse', 'HEAD'], workingDir, context);
813
+ backupId = hashResult.stdout?.trim() ?? '';
814
+ break;
914
815
  }
915
-
916
- if (result.exitCode !== 0) {
917
- return {
918
- success: false,
919
- backupType: args.backupType,
920
- backupName,
921
- error: result.stderr || 'Backup creation failed'
922
- };
816
+
817
+ case 'branch': {
818
+ await runGit(['checkout', '-b', backupName], workingDir, context);
819
+ // Return to where we were — a backup that leaves the caller on the
820
+ // backup branch is a trap.
821
+ await runGit(['checkout', '-'], workingDir, context);
822
+ backupId = backupName;
823
+ break;
824
+ }
825
+
826
+ case 'stash': {
827
+ const stashArgs = ['stash', 'push', '-m', backupName];
828
+ if (includeUntracked) {
829
+ stashArgs.push('--include-untracked');
830
+ }
831
+ await runGit(stashArgs, workingDir, context);
832
+ backupId = backupName;
833
+ break;
834
+ }
835
+
836
+ case 'archive': {
837
+ const archivePath = path.join(workingDir, `${backupName}.tar.gz`);
838
+ await runGit(
839
+ ['archive', '--format=tar.gz', '--output', archivePath, 'HEAD'],
840
+ workingDir,
841
+ context
842
+ );
843
+ backupId = archivePath;
844
+ break;
923
845
  }
924
-
925
- return {
926
- success: true,
927
- backupType: args.backupType,
928
- backupName,
929
- backupId,
930
- timestamp,
931
- error: null
932
- };
933
- } catch (error) {
934
- logger.error('Create backup failed', { error });
935
- return {
936
- success: false,
937
- backupType: args.backupType,
938
- backupName: args.backupName,
939
- error: error instanceof Error ? error.message : String(error)
940
- };
941
846
  }
847
+
848
+ return {
849
+ backupType,
850
+ backupName,
851
+ backupId,
852
+ timestamp
853
+ };
942
854
  }
943
- };
855
+ });
944
856
 
945
- export const codeReviewAgentTool = {
857
+ export const codeReviewAgentTool = defineTool<
858
+ {
859
+ reviewType?: 'diff' | 'files' | 'pull_request';
860
+ files?: string[];
861
+ focusAreas?: string[];
862
+ strictness?: 'low' | 'medium' | 'high';
863
+ workingDirectory?: string;
864
+ },
865
+ {
866
+ summary: {
867
+ filesReviewed: number;
868
+ totalIssues: number;
869
+ criticalIssues: number;
870
+ highIssues: number;
871
+ mediumIssues: number;
872
+ lowIssues: number;
873
+ overallScore: number;
874
+ };
875
+ issues: any[];
876
+ recommendations: any[];
877
+ score: number;
878
+ }
879
+ >({
946
880
  name: 'code_review_agent',
947
- description: 'AI-powered code review agent that analyzes changes for quality, security, and best practices',
881
+ category: TOOL_CATEGORIES.INFRASTRUCTURE,
882
+ // What this actually does: match a fixed set of regex patterns per focus area
883
+ // (eval(, innerHTML=, hardcoded secrets, TODO/FIXME, debugger, console.log,
884
+ // `any`, long functions...) and score the hits. No model is involved. The old
885
+ // description called it an "AI-powered code review agent", which set an
886
+ // expectation the implementation cannot meet.
887
+ description:
888
+ 'Scan changed files for a fixed set of known-bad patterns (eval, innerHTML assignment, ' +
889
+ 'hardcoded secrets, debugger statements, TODO comments, long functions) and return the ' +
890
+ 'hits with a score. This is pattern matching, not a semantic review — it will not catch ' +
891
+ 'logic errors, and it will flag patterns that are fine in context.',
948
892
  inputSchema: {
949
893
  type: 'object',
950
894
  properties: {
@@ -952,117 +896,95 @@ export const codeReviewAgentTool = {
952
896
  type: 'string',
953
897
  enum: ['diff', 'files', 'pull_request'],
954
898
  default: 'diff',
955
- description: 'Type of code review to perform'
899
+ description: 'Which files to scan: the working diff, an explicit list, or the diff against main'
956
900
  },
957
901
  files: {
958
902
  type: 'array',
959
903
  items: { type: 'string' },
960
- description: 'Specific files to review (for files review type)'
904
+ description: 'Files to scan (required when reviewType is "files")'
961
905
  },
962
906
  focusAreas: {
963
907
  type: 'array',
964
- items: { type: 'string' },
908
+ items: {
909
+ type: 'string',
910
+ enum: ['quality', 'security', 'performance', 'maintainability']
911
+ },
965
912
  default: ['quality', 'security', 'performance', 'maintainability'],
966
- description: 'Areas to focus on during review'
913
+ description: 'Which pattern sets to apply'
967
914
  },
968
915
  strictness: {
969
916
  type: 'string',
970
917
  enum: ['low', 'medium', 'high'],
971
918
  default: 'medium',
972
- description: 'Review strictness level'
919
+ description: 'How heavily each finding counts against the score'
973
920
  },
974
- workingDirectory: {
975
- type: 'string',
976
- description: 'Working directory (defaults to current directory)'
977
- }
921
+ workingDirectory: workingDirectoryProperty
978
922
  }
979
923
  },
980
- handler: async (args: any, context: any) => {
981
-
982
- try {
983
- const workingDir = args.workingDirectory || process.cwd();
984
- const reviewResults: {
985
- summary: any;
986
- issues: any[];
987
- recommendations: any[];
988
- score: number;
989
- } = {
990
- summary: {},
991
- issues: [],
992
- recommendations: [],
993
- score: 0
994
- };
995
-
996
- let filesToReview: string[] = [];
997
-
998
- // Get files to review based on type
999
- switch (args.reviewType) {
1000
- case 'diff':
1001
- const diffResult = await executeShellCommand('git', ['diff', '--name-only', 'HEAD'], {
1002
- workingDirectory: workingDir,
1003
- captureOutput: true
1004
- });
1005
-
1006
- if (diffResult.exitCode === 0) {
1007
- filesToReview = diffResult.stdout?.split('\n').filter(f => f.trim()) || [];
1008
- }
1009
- break;
1010
-
1011
- case 'files':
1012
- filesToReview = args.files || [];
1013
- break;
1014
-
1015
- case 'pull_request':
1016
- // Compare with main branch
1017
- const prDiffResult = await executeShellCommand('git', ['diff', '--name-only', 'main...HEAD'], {
1018
- workingDirectory: workingDir,
1019
- captureOutput: true
1020
- });
1021
-
1022
- if (prDiffResult.exitCode === 0) {
1023
- filesToReview = prDiffResult.stdout?.split('\n').filter(f => f.trim()) || [];
1024
- }
1025
- break;
924
+ run: async (input, context) => {
925
+ const workingDir = input.workingDirectory || process.cwd();
926
+ const reviewType = input.reviewType ?? 'diff';
927
+ const strictness = input.strictness ?? 'medium';
928
+ const focusAreas = input.focusAreas ?? ['quality', 'security', 'performance', 'maintainability'];
929
+
930
+ let filesToReview: string[];
931
+
932
+ switch (reviewType) {
933
+ case 'diff': {
934
+ const diffResult = await runGit(['diff', '--name-only', 'HEAD'], workingDir, context);
935
+ filesToReview = (diffResult.stdout ?? '').split('\n').filter(f => f.trim());
936
+ break;
1026
937
  }
1027
-
1028
- // Review each file
1029
- for (const file of filesToReview) {
1030
- const fileReview = await reviewFile(file, args.focusAreas, args.strictness, workingDir);
1031
- reviewResults.issues.push(...fileReview.issues);
1032
- reviewResults.recommendations.push(...fileReview.recommendations);
938
+
939
+ case 'pull_request': {
940
+ const prDiffResult = await runGit(
941
+ ['diff', '--name-only', 'main...HEAD'],
942
+ workingDir,
943
+ context
944
+ );
945
+ filesToReview = (prDiffResult.stdout ?? '').split('\n').filter(f => f.trim());
946
+ break;
1033
947
  }
1034
-
1035
- // Calculate overall score
1036
- reviewResults.score = calculateReviewScore(reviewResults.issues, args.strictness);
1037
-
1038
- // Generate summary
1039
- reviewResults.summary = {
1040
- filesReviewed: filesToReview.length,
1041
- totalIssues: reviewResults.issues.length,
1042
- criticalIssues: reviewResults.issues.filter(i => i.severity === 'critical').length,
1043
- highIssues: reviewResults.issues.filter(i => i.severity === 'high').length,
1044
- mediumIssues: reviewResults.issues.filter(i => i.severity === 'medium').length,
1045
- lowIssues: reviewResults.issues.filter(i => i.severity === 'low').length,
1046
- overallScore: reviewResults.score
1047
- };
1048
-
1049
- return {
1050
- success: true,
1051
- reviewResults,
1052
- error: null
1053
- };
1054
- } catch (error) {
1055
- logger.error('Code review agent failed', { error });
1056
- return {
1057
- success: false,
1058
- reviewResults: null,
1059
- error: error instanceof Error ? error.message : String(error)
1060
- };
948
+
949
+ case 'files':
950
+ default:
951
+ if (!input.files || input.files.length === 0) {
952
+ throw ToolError.invalidInput(
953
+ 'files is required and must be non-empty when reviewType is "files".'
954
+ );
955
+ }
956
+ filesToReview = input.files;
957
+ break;
958
+ }
959
+
960
+ const issues: any[] = [];
961
+ const recommendations: any[] = [];
962
+
963
+ for (const file of filesToReview) {
964
+ const fileReview = await reviewFile(file, focusAreas, strictness, workingDir);
965
+ issues.push(...fileReview.issues);
966
+ recommendations.push(...fileReview.recommendations);
1061
967
  }
968
+
969
+ const score = calculateReviewScore(issues, strictness);
970
+
971
+ return {
972
+ summary: {
973
+ filesReviewed: filesToReview.length,
974
+ totalIssues: issues.length,
975
+ criticalIssues: issues.filter(i => i.severity === 'critical').length,
976
+ highIssues: issues.filter(i => i.severity === 'high').length,
977
+ mediumIssues: issues.filter(i => i.severity === 'medium').length,
978
+ lowIssues: issues.filter(i => i.severity === 'low').length,
979
+ overallScore: score
980
+ },
981
+ issues,
982
+ recommendations,
983
+ score
984
+ };
1062
985
  }
1063
- };
986
+ });
1064
987
 
1065
- // Export all safety tools
1066
988
  export const safetyTools = [
1067
989
  createFeatureBranchTool,
1068
990
  runFullTestSuiteTool,
@@ -1070,4 +992,4 @@ export const safetyTools = [
1070
992
  rollbackChangesTool,
1071
993
  createBackupTool,
1072
994
  codeReviewAgentTool
1073
- ];
995
+ ];