terramend 0.2.0

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 (406) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +145 -0
  3. package/dist/agents/claude.d.ts +73 -0
  4. package/dist/agents/claudePretoolGate.d.ts +99 -0
  5. package/dist/agents/gateServer.d.ts +7 -0
  6. package/dist/agents/index.d.ts +6 -0
  7. package/dist/agents/nativeFsDenies.d.ts +28 -0
  8. package/dist/agents/opencode.d.ts +231 -0
  9. package/dist/agents/opencodePlugin.d.ts +85 -0
  10. package/dist/agents/opencodeShared.d.ts +40 -0
  11. package/dist/agents/postRun.d.ts +132 -0
  12. package/dist/agents/reviewer.d.ts +38 -0
  13. package/dist/agents/sessionLabeler.d.ts +97 -0
  14. package/dist/agents/shared.d.ts +189 -0
  15. package/dist/agents/subagentModels.d.ts +19 -0
  16. package/dist/agents/subagentToolGates.d.ts +55 -0
  17. package/dist/cli.mjs +197426 -0
  18. package/dist/external.d.ts +227 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +196783 -0
  21. package/dist/internal/index.d.ts +18 -0
  22. package/dist/internal.js +1714 -0
  23. package/dist/lifecycle.d.ts +2 -0
  24. package/dist/main.d.ts +8 -0
  25. package/dist/mcp/arkConfig.d.ts +1 -0
  26. package/dist/mcp/checkSuite.d.ts +25 -0
  27. package/dist/mcp/checkout.d.ts +77 -0
  28. package/dist/mcp/comment.d.ts +119 -0
  29. package/dist/mcp/commitInfo.d.ts +9 -0
  30. package/dist/mcp/crosswalk.d.ts +105 -0
  31. package/dist/mcp/dependencies.d.ts +8 -0
  32. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  33. package/dist/mcp/git.d.ts +46 -0
  34. package/dist/mcp/guardrails.d.ts +104 -0
  35. package/dist/mcp/issue.d.ts +18 -0
  36. package/dist/mcp/issueComments.d.ts +9 -0
  37. package/dist/mcp/issueEvents.d.ts +9 -0
  38. package/dist/mcp/issueInfo.d.ts +9 -0
  39. package/dist/mcp/labels.d.ts +12 -0
  40. package/dist/mcp/localContext.d.ts +19 -0
  41. package/dist/mcp/moduleExtraction.d.ts +71 -0
  42. package/dist/mcp/moduleTests.d.ts +104 -0
  43. package/dist/mcp/modules.d.ts +179 -0
  44. package/dist/mcp/output.d.ts +12 -0
  45. package/dist/mcp/pathSafety.d.ts +14 -0
  46. package/dist/mcp/policy.d.ts +48 -0
  47. package/dist/mcp/pr.d.ts +49 -0
  48. package/dist/mcp/prInfo.d.ts +9 -0
  49. package/dist/mcp/providerSchema.d.ts +50 -0
  50. package/dist/mcp/review.d.ts +199 -0
  51. package/dist/mcp/reviewComments.d.ts +178 -0
  52. package/dist/mcp/roots.d.ts +58 -0
  53. package/dist/mcp/scope.d.ts +15 -0
  54. package/dist/mcp/selectMode.d.ts +18 -0
  55. package/dist/mcp/server.d.ts +48 -0
  56. package/dist/mcp/shared.d.ts +47 -0
  57. package/dist/mcp/shell.d.ts +37 -0
  58. package/dist/mcp/staleFix.d.ts +51 -0
  59. package/dist/mcp/terraform/cost.d.ts +55 -0
  60. package/dist/mcp/terraform/currency.d.ts +94 -0
  61. package/dist/mcp/terraform/decisions.d.ts +178 -0
  62. package/dist/mcp/terraform/findings.d.ts +75 -0
  63. package/dist/mcp/terraform/plan.d.ts +157 -0
  64. package/dist/mcp/terraform/scanners.d.ts +131 -0
  65. package/dist/mcp/terraform/tools.d.ts +63 -0
  66. package/dist/mcp/terraform/types.d.ts +172 -0
  67. package/dist/mcp/terraform.d.ts +22 -0
  68. package/dist/mcp/terratest.d.ts +83 -0
  69. package/dist/mcp/upload.d.ts +6 -0
  70. package/dist/models.d.ts +171 -0
  71. package/dist/modes.d.ts +26 -0
  72. package/dist/prep/index.d.ts +7 -0
  73. package/dist/prep/installNodeDependencies.d.ts +2 -0
  74. package/dist/prep/installPythonDependencies.d.ts +2 -0
  75. package/dist/prep/types.d.ts +31 -0
  76. package/dist/reviewQuality.d.ts +64 -0
  77. package/dist/skills/terraform-best-practices/SKILL.md +369 -0
  78. package/dist/toolState.d.ts +135 -0
  79. package/dist/utils/activity.d.ts +40 -0
  80. package/dist/utils/agent.d.ts +20 -0
  81. package/dist/utils/agentHangReport.d.ts +38 -0
  82. package/dist/utils/apiFetch.d.ts +19 -0
  83. package/dist/utils/apiKeys.d.ts +41 -0
  84. package/dist/utils/apiUrl.d.ts +20 -0
  85. package/dist/utils/assets.d.ts +8 -0
  86. package/dist/utils/billingErrors.d.ts +85 -0
  87. package/dist/utils/body.d.ts +34 -0
  88. package/dist/utils/buildTerramendFooter.d.ts +25 -0
  89. package/dist/utils/byokFallback.d.ts +85 -0
  90. package/dist/utils/claudeSubscription.d.ts +30 -0
  91. package/dist/utils/cli.d.ts +10 -0
  92. package/dist/utils/codexHome.d.ts +29 -0
  93. package/dist/utils/codexOAuth.d.ts +60 -0
  94. package/dist/utils/diffCoverage.d.ts +63 -0
  95. package/dist/utils/errorReport.d.ts +17 -0
  96. package/dist/utils/exitHandler.d.ts +8 -0
  97. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  98. package/dist/utils/gitAuth.d.ts +84 -0
  99. package/dist/utils/gitAuthServer.d.ts +24 -0
  100. package/dist/utils/github.d.ts +78 -0
  101. package/dist/utils/globals.d.ts +3 -0
  102. package/dist/utils/install.d.ts +60 -0
  103. package/dist/utils/instructions.d.ts +48 -0
  104. package/dist/utils/leapingComment.d.ts +11 -0
  105. package/dist/utils/learnings.d.ts +62 -0
  106. package/dist/utils/learningsTruncate.d.ts +25 -0
  107. package/dist/utils/lifecycle.d.ts +57 -0
  108. package/dist/utils/log.d.ts +111 -0
  109. package/dist/utils/normalizeEnv.d.ts +30 -0
  110. package/dist/utils/openCodeModels.d.ts +11 -0
  111. package/dist/utils/overrides.d.ts +40 -0
  112. package/dist/utils/packageManager.d.ts +49 -0
  113. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  114. package/dist/utils/payload.d.ts +105 -0
  115. package/dist/utils/prSummary.d.ts +61 -0
  116. package/dist/utils/progressComment.d.ts +146 -0
  117. package/dist/utils/providerErrors.d.ts +31 -0
  118. package/dist/utils/rangeDiff.d.ts +51 -0
  119. package/dist/utils/remediationCommand.d.ts +55 -0
  120. package/dist/utils/retry.d.ts +13 -0
  121. package/dist/utils/reviewCleanup.d.ts +14 -0
  122. package/dist/utils/run.d.ts +9 -0
  123. package/dist/utils/runContext.d.ts +60 -0
  124. package/dist/utils/runContextData.d.ts +23 -0
  125. package/dist/utils/runErrorRenderer.d.ts +64 -0
  126. package/dist/utils/runLifecycle.d.ts +86 -0
  127. package/dist/utils/runStartupLog.d.ts +15 -0
  128. package/dist/utils/secrets.d.ts +22 -0
  129. package/dist/utils/setup.d.ts +90 -0
  130. package/dist/utils/shell.d.ts +32 -0
  131. package/dist/utils/skills.d.ts +10 -0
  132. package/dist/utils/subprocess.d.ts +80 -0
  133. package/dist/utils/terraformMcp.d.ts +42 -0
  134. package/dist/utils/time.d.ts +15 -0
  135. package/dist/utils/timer.d.ts +23 -0
  136. package/dist/utils/todoTracking.d.ts +16 -0
  137. package/dist/utils/token.d.ts +39 -0
  138. package/dist/utils/version.d.ts +2 -0
  139. package/dist/utils/versioning.d.ts +7 -0
  140. package/dist/utils/vertex.d.ts +16 -0
  141. package/dist/utils/workflow.d.ts +13 -0
  142. package/package.json +119 -0
  143. package/src/agents/claude.test.ts +1016 -0
  144. package/src/agents/claude.ts +1246 -0
  145. package/src/agents/claudePretoolGate.test.ts +28 -0
  146. package/src/agents/claudePretoolGate.ts +173 -0
  147. package/src/agents/gateServer.test.ts +204 -0
  148. package/src/agents/gateServer.ts +124 -0
  149. package/src/agents/index.ts +10 -0
  150. package/src/agents/nativeFsDenies.ts +82 -0
  151. package/src/agents/opencode.test.ts +1440 -0
  152. package/src/agents/opencode.ts +1312 -0
  153. package/src/agents/opencodePlugin.ts +222 -0
  154. package/src/agents/opencodeShared.test.ts +34 -0
  155. package/src/agents/opencodeShared.ts +121 -0
  156. package/src/agents/postRun.test.ts +549 -0
  157. package/src/agents/postRun.ts +535 -0
  158. package/src/agents/reviewer.ts +104 -0
  159. package/src/agents/sessionLabeler.test.ts +247 -0
  160. package/src/agents/sessionLabeler.ts +178 -0
  161. package/src/agents/shared.test.ts +76 -0
  162. package/src/agents/shared.ts +292 -0
  163. package/src/agents/subagentModels.test.ts +113 -0
  164. package/src/agents/subagentModels.ts +40 -0
  165. package/src/agents/subagentRegistration.test.ts +41 -0
  166. package/src/agents/subagentToolGates.ts +114 -0
  167. package/src/cli.test.ts +129 -0
  168. package/src/cli.ts +105 -0
  169. package/src/commands/gha.test.ts +192 -0
  170. package/src/commands/gha.ts +188 -0
  171. package/src/commands/mcp.ts +122 -0
  172. package/src/config.ts +1 -0
  173. package/src/entry.ts +7 -0
  174. package/src/entryPost.stdlibOnly.test.ts +109 -0
  175. package/src/entryPost.ts +99 -0
  176. package/src/external.test.ts +16 -0
  177. package/src/external.ts +302 -0
  178. package/src/index.ts +11 -0
  179. package/src/internal/index.ts +71 -0
  180. package/src/lifecycle.ts +2 -0
  181. package/src/main.test.ts +873 -0
  182. package/src/main.ts +712 -0
  183. package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
  184. package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
  185. package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
  186. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  187. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  188. package/src/mcp/arkConfig.ts +7 -0
  189. package/src/mcp/checkSuite.test.ts +245 -0
  190. package/src/mcp/checkSuite.ts +255 -0
  191. package/src/mcp/checkout.test.ts +752 -0
  192. package/src/mcp/checkout.ts +886 -0
  193. package/src/mcp/comment.test.ts +772 -0
  194. package/src/mcp/comment.ts +582 -0
  195. package/src/mcp/commitInfo.test.ts +127 -0
  196. package/src/mcp/commitInfo.ts +61 -0
  197. package/src/mcp/crosswalk.test.ts +106 -0
  198. package/src/mcp/crosswalk.ts +339 -0
  199. package/src/mcp/dependencies.test.ts +309 -0
  200. package/src/mcp/dependencies.ts +189 -0
  201. package/src/mcp/geminiSanitizer.test.ts +287 -0
  202. package/src/mcp/geminiSanitizer.ts +207 -0
  203. package/src/mcp/git.test.ts +1083 -0
  204. package/src/mcp/git.ts +890 -0
  205. package/src/mcp/guardrails.test.ts +705 -0
  206. package/src/mcp/guardrails.ts +465 -0
  207. package/src/mcp/issue.test.ts +113 -0
  208. package/src/mcp/issue.ts +73 -0
  209. package/src/mcp/issueComments.test.ts +69 -0
  210. package/src/mcp/issueComments.ts +48 -0
  211. package/src/mcp/issueEvents.test.ts +134 -0
  212. package/src/mcp/issueEvents.ts +100 -0
  213. package/src/mcp/issueInfo.test.ts +104 -0
  214. package/src/mcp/issueInfo.ts +72 -0
  215. package/src/mcp/labels.test.ts +52 -0
  216. package/src/mcp/labels.ts +34 -0
  217. package/src/mcp/localContext.ts +28 -0
  218. package/src/mcp/localServer.test.ts +75 -0
  219. package/src/mcp/localServer.ts +131 -0
  220. package/src/mcp/moduleExtraction.test.ts +261 -0
  221. package/src/mcp/moduleExtraction.ts +313 -0
  222. package/src/mcp/moduleTests.test.ts +269 -0
  223. package/src/mcp/moduleTests.ts +421 -0
  224. package/src/mcp/modules.test.ts +640 -0
  225. package/src/mcp/modules.ts +696 -0
  226. package/src/mcp/output.test.ts +96 -0
  227. package/src/mcp/output.ts +70 -0
  228. package/src/mcp/pathSafety.test.ts +44 -0
  229. package/src/mcp/pathSafety.ts +28 -0
  230. package/src/mcp/policy.test.ts +282 -0
  231. package/src/mcp/policy.ts +199 -0
  232. package/src/mcp/pr.test.ts +387 -0
  233. package/src/mcp/pr.ts +194 -0
  234. package/src/mcp/prInfo.test.ts +96 -0
  235. package/src/mcp/prInfo.ts +91 -0
  236. package/src/mcp/providerSchema.test.ts +85 -0
  237. package/src/mcp/providerSchema.ts +175 -0
  238. package/src/mcp/review.test.ts +936 -0
  239. package/src/mcp/review.ts +923 -0
  240. package/src/mcp/reviewComments.test.ts +549 -0
  241. package/src/mcp/reviewComments.ts +896 -0
  242. package/src/mcp/roots.test.ts +175 -0
  243. package/src/mcp/roots.ts +217 -0
  244. package/src/mcp/scope.test.ts +59 -0
  245. package/src/mcp/scope.ts +65 -0
  246. package/src/mcp/security.test.ts +720 -0
  247. package/src/mcp/selectMode.test.ts +210 -0
  248. package/src/mcp/selectMode.ts +181 -0
  249. package/src/mcp/server.test.ts +292 -0
  250. package/src/mcp/server.ts +403 -0
  251. package/src/mcp/shared.ts +100 -0
  252. package/src/mcp/shell.test.ts +520 -0
  253. package/src/mcp/shell.ts +505 -0
  254. package/src/mcp/staleFix.test.ts +237 -0
  255. package/src/mcp/staleFix.ts +277 -0
  256. package/src/mcp/terraform/cost.ts +163 -0
  257. package/src/mcp/terraform/currency.test.ts +338 -0
  258. package/src/mcp/terraform/currency.ts +336 -0
  259. package/src/mcp/terraform/decisions.ts +527 -0
  260. package/src/mcp/terraform/findings.ts +333 -0
  261. package/src/mcp/terraform/plan.ts +348 -0
  262. package/src/mcp/terraform/scanners.ts +809 -0
  263. package/src/mcp/terraform/tools.test.ts +1071 -0
  264. package/src/mcp/terraform/tools.ts +908 -0
  265. package/src/mcp/terraform/types.ts +305 -0
  266. package/src/mcp/terraform.test.ts +1957 -0
  267. package/src/mcp/terraform.ts +23 -0
  268. package/src/mcp/terratest.test.ts +105 -0
  269. package/src/mcp/terratest.ts +196 -0
  270. package/src/mcp/toolFiltering.test.ts +85 -0
  271. package/src/mcp/upload.test.ts +180 -0
  272. package/src/mcp/upload.ts +112 -0
  273. package/src/models.test.ts +300 -0
  274. package/src/models.ts +708 -0
  275. package/src/modes.test.ts +107 -0
  276. package/src/modes.ts +880 -0
  277. package/src/prep/index.ts +43 -0
  278. package/src/prep/installNodeDependencies.test.ts +298 -0
  279. package/src/prep/installNodeDependencies.ts +196 -0
  280. package/src/prep/installPythonDependencies.test.ts +268 -0
  281. package/src/prep/installPythonDependencies.ts +199 -0
  282. package/src/prep/types.ts +38 -0
  283. package/src/reviewQuality.test.ts +63 -0
  284. package/src/reviewQuality.ts +134 -0
  285. package/src/runCli.test.ts +214 -0
  286. package/src/runCli.ts +282 -0
  287. package/src/skills/terraform-best-practices/SKILL.md +369 -0
  288. package/src/toolState.test.ts +45 -0
  289. package/src/toolState.ts +252 -0
  290. package/src/utils/activity.test.ts +188 -0
  291. package/src/utils/activity.ts +210 -0
  292. package/src/utils/agent.test.ts +251 -0
  293. package/src/utils/agent.ts +139 -0
  294. package/src/utils/agentHangReport.test.ts +203 -0
  295. package/src/utils/agentHangReport.ts +170 -0
  296. package/src/utils/apiFetch.test.ts +115 -0
  297. package/src/utils/apiFetch.ts +62 -0
  298. package/src/utils/apiKeys.test.ts +344 -0
  299. package/src/utils/apiKeys.ts +206 -0
  300. package/src/utils/apiUrl.test.ts +30 -0
  301. package/src/utils/apiUrl.ts +59 -0
  302. package/src/utils/assets.test.ts +153 -0
  303. package/src/utils/assets.ts +107 -0
  304. package/src/utils/billingErrors.test.ts +121 -0
  305. package/src/utils/billingErrors.ts +189 -0
  306. package/src/utils/body.test.ts +217 -0
  307. package/src/utils/body.ts +168 -0
  308. package/src/utils/buildTerramendFooter.test.ts +38 -0
  309. package/src/utils/buildTerramendFooter.ts +82 -0
  310. package/src/utils/byokFallback.test.ts +205 -0
  311. package/src/utils/byokFallback.ts +128 -0
  312. package/src/utils/claudeSubscription.test.ts +179 -0
  313. package/src/utils/claudeSubscription.ts +93 -0
  314. package/src/utils/cli.ts +31 -0
  315. package/src/utils/codexHome.test.ts +190 -0
  316. package/src/utils/codexHome.ts +191 -0
  317. package/src/utils/codexOAuth.ts +147 -0
  318. package/src/utils/codexRefreshDetect.test.ts +85 -0
  319. package/src/utils/codexRefreshDetect.ts +35 -0
  320. package/src/utils/diffCoverage.test.ts +468 -0
  321. package/src/utils/diffCoverage.ts +404 -0
  322. package/src/utils/errorReport.test.ts +135 -0
  323. package/src/utils/errorReport.ts +83 -0
  324. package/src/utils/exitHandler.ts +35 -0
  325. package/src/utils/fixDoubleEscapedString.ts +9 -0
  326. package/src/utils/ghaCore.ts +13 -0
  327. package/src/utils/gitAuth.test.ts +322 -0
  328. package/src/utils/gitAuth.ts +263 -0
  329. package/src/utils/gitAuthServer.test.ts +260 -0
  330. package/src/utils/gitAuthServer.ts +182 -0
  331. package/src/utils/github.test.ts +615 -0
  332. package/src/utils/github.ts +538 -0
  333. package/src/utils/globals.ts +9 -0
  334. package/src/utils/humanEditCapture.test.ts +100 -0
  335. package/src/utils/humanEditCapture.ts +193 -0
  336. package/src/utils/install.test.ts +768 -0
  337. package/src/utils/install.ts +492 -0
  338. package/src/utils/instructions.test.ts +240 -0
  339. package/src/utils/instructions.ts +543 -0
  340. package/src/utils/leapingComment.test.ts +51 -0
  341. package/src/utils/leapingComment.ts +18 -0
  342. package/src/utils/learnings.test.ts +87 -0
  343. package/src/utils/learnings.ts +138 -0
  344. package/src/utils/learningsTocRender.test.ts +116 -0
  345. package/src/utils/learningsTruncate.test.ts +39 -0
  346. package/src/utils/learningsTruncate.ts +42 -0
  347. package/src/utils/lifecycle.test.ts +195 -0
  348. package/src/utils/lifecycle.ts +198 -0
  349. package/src/utils/log.test.ts +402 -0
  350. package/src/utils/log.ts +432 -0
  351. package/src/utils/normalizeEnv.test.ts +91 -0
  352. package/src/utils/normalizeEnv.ts +106 -0
  353. package/src/utils/openCodeModels.ts +82 -0
  354. package/src/utils/overrides.test.ts +89 -0
  355. package/src/utils/overrides.ts +98 -0
  356. package/src/utils/packageManager.test.ts +321 -0
  357. package/src/utils/packageManager.ts +257 -0
  358. package/src/utils/patchWorkflowRunFields.test.ts +92 -0
  359. package/src/utils/patchWorkflowRunFields.ts +150 -0
  360. package/src/utils/payload.test.ts +497 -0
  361. package/src/utils/payload.ts +371 -0
  362. package/src/utils/postApiFetch.ts +51 -0
  363. package/src/utils/prSummary.test.ts +224 -0
  364. package/src/utils/prSummary.ts +147 -0
  365. package/src/utils/progressComment.ts +261 -0
  366. package/src/utils/providerErrors.test.ts +315 -0
  367. package/src/utils/providerErrors.ts +172 -0
  368. package/src/utils/rangeDiff.test.ts +236 -0
  369. package/src/utils/rangeDiff.ts +182 -0
  370. package/src/utils/remediationCommand.test.ts +163 -0
  371. package/src/utils/remediationCommand.ts +119 -0
  372. package/src/utils/retry.test.ts +153 -0
  373. package/src/utils/retry.ts +58 -0
  374. package/src/utils/reviewCleanup.ts +106 -0
  375. package/src/utils/run.ts +99 -0
  376. package/src/utils/runContext.ts +145 -0
  377. package/src/utils/runContextData.ts +58 -0
  378. package/src/utils/runErrorRenderer.test.ts +95 -0
  379. package/src/utils/runErrorRenderer.ts +259 -0
  380. package/src/utils/runFixture.ts +76 -0
  381. package/src/utils/runLifecycle.ts +237 -0
  382. package/src/utils/runStartupLog.ts +60 -0
  383. package/src/utils/secrets.test.ts +103 -0
  384. package/src/utils/secrets.ts +177 -0
  385. package/src/utils/setup.test.ts +509 -0
  386. package/src/utils/setup.ts +352 -0
  387. package/src/utils/shell.ts +103 -0
  388. package/src/utils/skills.test.ts +46 -0
  389. package/src/utils/skills.ts +67 -0
  390. package/src/utils/subprocess.test.ts +170 -0
  391. package/src/utils/subprocess.ts +438 -0
  392. package/src/utils/terraformMcp.test.ts +63 -0
  393. package/src/utils/terraformMcp.ts +83 -0
  394. package/src/utils/time.test.ts +105 -0
  395. package/src/utils/time.ts +59 -0
  396. package/src/utils/timer.test.ts +91 -0
  397. package/src/utils/timer.ts +72 -0
  398. package/src/utils/todoTracking.test.ts +223 -0
  399. package/src/utils/todoTracking.ts +167 -0
  400. package/src/utils/token.test.ts +239 -0
  401. package/src/utils/token.ts +186 -0
  402. package/src/utils/version.ts +10 -0
  403. package/src/utils/versioning.test.ts +34 -0
  404. package/src/utils/versioning.ts +44 -0
  405. package/src/utils/vertex.ts +85 -0
  406. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { SetOutputTool } from "#app/mcp/output";
3
+ import type { ToolContext } from "#app/mcp/server";
4
+ import { initToolState, type ToolState } from "#app/toolState";
5
+
6
+ type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
7
+
8
+ async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
9
+ const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
10
+ return exec(params, {});
11
+ }
12
+
13
+ function makeCtx(): { ctx: ToolContext; toolState: ToolState } {
14
+ const toolState = initToolState({ progressComment: undefined });
15
+ const ctx = { toolState } as unknown as ToolContext;
16
+ return { ctx, toolState };
17
+ }
18
+
19
+ type StandardValidate = {
20
+ "~standard": {
21
+ vendor: string;
22
+ jsonSchema: { input: () => Record<string, unknown>; output: () => Record<string, unknown> };
23
+ validate: (input: unknown) => {
24
+ value?: unknown;
25
+ issues?: { message: string; path: string[] }[];
26
+ };
27
+ };
28
+ };
29
+
30
+ const objectSchema = {
31
+ $schema: "http://json-schema.org/draft-07/schema#",
32
+ type: "object",
33
+ properties: { foo: { type: "string" } },
34
+ required: ["foo"],
35
+ additionalProperties: false,
36
+ };
37
+
38
+ describe("SetOutputTool (string output)", () => {
39
+ it("stores the raw value on tool state", async () => {
40
+ const { ctx, toolState } = makeCtx();
41
+ const result = await runTool(SetOutputTool(ctx), { value: "hello world" });
42
+
43
+ expect(result.isError).toBeUndefined();
44
+ expect(result.content[0].text).toContain("success: true");
45
+ expect(toolState.output).toBe("hello world");
46
+ });
47
+ });
48
+
49
+ describe("SetOutputTool (structured output schema)", () => {
50
+ it("stores the params serialized as JSON", async () => {
51
+ const { ctx, toolState } = makeCtx();
52
+ const result = await runTool(SetOutputTool(ctx, objectSchema), { foo: "bar" });
53
+
54
+ expect(result.isError).toBeUndefined();
55
+ expect(toolState.output).toBe(JSON.stringify({ foo: "bar" }));
56
+ });
57
+
58
+ it("exposes the JSON schema (minus $schema) through the standard-schema surface", () => {
59
+ const { ctx } = makeCtx();
60
+ const tool = SetOutputTool(ctx, objectSchema);
61
+ const std = (tool.parameters as unknown as StandardValidate)["~standard"];
62
+
63
+ expect(std.vendor).toBe("json-schema");
64
+ expect(std.jsonSchema.input()).not.toHaveProperty("$schema");
65
+ expect(std.jsonSchema.output()).toMatchObject({ type: "object" });
66
+ });
67
+
68
+ it("validates conforming input", () => {
69
+ const { ctx } = makeCtx();
70
+ const tool = SetOutputTool(ctx, objectSchema);
71
+ const std = (tool.parameters as unknown as StandardValidate)["~standard"];
72
+
73
+ expect(std.validate({ foo: "ok" })).toEqual({ value: { foo: "ok" } });
74
+ });
75
+
76
+ it("reports root-level validation failures with a '/' path", () => {
77
+ const { ctx } = makeCtx();
78
+ const tool = SetOutputTool(ctx, objectSchema);
79
+ const std = (tool.parameters as unknown as StandardValidate)["~standard"];
80
+
81
+ const result = std.validate(42);
82
+ expect(result.value).toBeUndefined();
83
+ expect(result.issues?.[0]?.message).toMatch(/^\/: must be object/);
84
+ expect(result.issues?.[0]?.path).toEqual([]);
85
+ });
86
+
87
+ it("reports nested validation failures with the instance path", () => {
88
+ const { ctx } = makeCtx();
89
+ const tool = SetOutputTool(ctx, objectSchema);
90
+ const std = (tool.parameters as unknown as StandardValidate)["~standard"];
91
+
92
+ const result = std.validate({ foo: 42 });
93
+ expect(result.issues?.[0]?.message).toMatch(/\/foo: must be string/);
94
+ expect(result.issues?.[0]?.path).toEqual(["foo"]);
95
+ });
96
+ });
@@ -0,0 +1,70 @@
1
+ import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { Ajv } from "ajv";
3
+ import { type } from "arktype";
4
+ import type { ToolContext } from "#app/mcp/server";
5
+ import { execute, tool } from "#app/mcp/shared";
6
+
7
+ export const SetOutputParams = type({
8
+ value: type.string.describe("the output value to expose as a GitHub Action output"),
9
+ });
10
+
11
+ type JsonSchema = Record<string, unknown>;
12
+
13
+ function jsonSchemaToStandardSchema({
14
+ $schema: _,
15
+ ...jsonSchema
16
+ }: JsonSchema): StandardJSONSchemaV1<any> & StandardSchemaV1<any> {
17
+ const ajv = new Ajv();
18
+ const validate = ajv.compile(jsonSchema);
19
+
20
+ return {
21
+ "~standard": {
22
+ version: 1,
23
+ vendor: "json-schema",
24
+ jsonSchema: {
25
+ input: () => jsonSchema,
26
+ output: () => jsonSchema,
27
+ },
28
+ validate(input: unknown) {
29
+ if (validate(input)) {
30
+ return { value: input };
31
+ }
32
+ return {
33
+ issues: (validate.errors ?? []).map((err) => ({
34
+ message: `${err.instancePath || "/"}: ${err.message ?? "validation error"}`,
35
+ path: err.instancePath ? err.instancePath.split("/").filter(Boolean) : [],
36
+ })),
37
+ };
38
+ },
39
+ },
40
+ };
41
+ }
42
+
43
+ function storeOutput(ctx: ToolContext, value: string) {
44
+ ctx.toolState.output = value;
45
+ return { success: true };
46
+ }
47
+
48
+ export function SetOutputTool(ctx: ToolContext, outputSchema?: JsonSchema) {
49
+ if (outputSchema) {
50
+ return tool({
51
+ name: "set_output",
52
+ description:
53
+ "Set the structured action output. You MUST call this tool before finishing — the output is required. Pass the output object directly as the tool arguments (no wrapping needed).",
54
+ parameters: jsonSchemaToStandardSchema(outputSchema),
55
+ execute: execute(async (params) => {
56
+ return storeOutput(ctx, JSON.stringify(params));
57
+ }),
58
+ });
59
+ }
60
+
61
+ return tool({
62
+ name: "set_output",
63
+ description:
64
+ "Set the action output. Exposes the value as the 'result' GitHub Action output for downstream workflow steps. Do NOT use this for progress reporting — use report_progress instead.",
65
+ parameters: SetOutputParams,
66
+ execute: execute(async (params) => {
67
+ return storeOutput(ctx, params.value);
68
+ }),
69
+ });
70
+ }
@@ -0,0 +1,44 @@
1
+ import { resolve } from "node:path";
2
+ import { describe, expect, it } from "vitest";
3
+ import { resolveWithinCwd } from "#app/mcp/pathSafety";
4
+
5
+ // `resolve("workspace-root")` yields an absolute path under the test cwd, so the
6
+ // assertions are drive-correct on Windows and POSIX alike.
7
+ const base = resolve("workspace-root");
8
+
9
+ describe("resolveWithinCwd", () => {
10
+ it("allows a relative child path", () => {
11
+ expect(resolveWithinCwd(base, "findings.json")).toBe(resolve(base, "findings.json"));
12
+ });
13
+
14
+ it("allows a nested relative child path", () => {
15
+ expect(resolveWithinCwd(base, "reports/out.sarif")).toBe(resolve(base, "reports/out.sarif"));
16
+ });
17
+
18
+ it("allows the workspace root itself", () => {
19
+ expect(resolveWithinCwd(base, ".")).toBe(base);
20
+ });
21
+
22
+ it("allows traversal that resolves back inside the workspace", () => {
23
+ expect(resolveWithinCwd(base, "a/../b.json")).toBe(resolve(base, "b.json"));
24
+ });
25
+
26
+ it("allows an absolute path that is inside the workspace", () => {
27
+ const inside = resolve(base, "deep/x.json");
28
+ expect(resolveWithinCwd(base, inside)).toBe(inside);
29
+ });
30
+
31
+ it("rejects parent traversal", () => {
32
+ expect(() => resolveWithinCwd(base, "../escape")).toThrow(/escapes the workspace/);
33
+ });
34
+
35
+ it("rejects deep traversal that resolves outside", () => {
36
+ expect(() => resolveWithinCwd(base, "a/../../escape")).toThrow(/escapes the workspace/);
37
+ });
38
+
39
+ it("rejects an absolute path outside the workspace", () => {
40
+ expect(() => resolveWithinCwd(base, resolve(base, "../sibling/x"))).toThrow(
41
+ /escapes the workspace/,
42
+ );
43
+ });
44
+ });
@@ -0,0 +1,28 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+
3
+ /**
4
+ * Resolve an agent-supplied path against `cwd` and confine it to that workspace.
5
+ *
6
+ * Returns the absolute path when it stays inside `cwd`; throws otherwise. Blocks
7
+ * both `..` traversal and absolute paths that point outside `cwd` (including a
8
+ * different drive on Windows, where `relative` returns an absolute path).
9
+ *
10
+ * Terramend treats the agent as semi-trusted (attacker-controlled PR content can
11
+ * prompt-inject it), so any tool that reads or writes a file path the agent
12
+ * controls — read_findings, terraform_emit_sarif, terraform_module_interface —
13
+ * must confine that path to the workspace. Without it the agent has an arbitrary
14
+ * file read (findings) or write (SARIF) primitive on the runner.
15
+ */
16
+ export function resolveWithinCwd(cwd: string, userPath: string): string {
17
+ const base = resolve(cwd);
18
+ const target = resolve(base, userPath);
19
+ const rel = relative(base, target);
20
+ // rel === "" means target IS the workspace root; a child path neither starts
21
+ // with ".." nor is absolute. Anything else escaped the workspace.
22
+ if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))) {
23
+ return target;
24
+ }
25
+ throw new Error(
26
+ `path '${userPath}' escapes the workspace; only paths inside the working directory are allowed.`,
27
+ );
28
+ }
@@ -0,0 +1,282 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { PolicyCheckTool, parseConftestOutput } from "#app/mcp/policy";
6
+ import type { ToolContext } from "#app/mcp/server";
7
+
8
+ vi.mock("node:fs", async (importOriginal) => {
9
+ const actual = await importOriginal<typeof import("node:fs")>();
10
+ return { ...actual, default: actual, existsSync: vi.fn(() => false) };
11
+ });
12
+
13
+ vi.mock("node:child_process", async (importOriginal) => {
14
+ const actual = await importOriginal<typeof import("node:child_process")>();
15
+ return { ...actual, default: actual, spawnSync: vi.fn() };
16
+ });
17
+
18
+ vi.mock("#app/utils/secrets", () => ({
19
+ resolveEnv: vi.fn(() => ({})),
20
+ }));
21
+
22
+ const existsSyncMock = vi.mocked(existsSync);
23
+ const spawnSyncMock = vi.mocked(spawnSync);
24
+
25
+ describe("parseConftestOutput", () => {
26
+ it("returns a clean pass for empty/malformed input", () => {
27
+ expect(parseConftestOutput("")).toEqual({
28
+ passed: true,
29
+ failures: [],
30
+ warnings: [],
31
+ tested: 0,
32
+ });
33
+ expect(parseConftestOutput("not json")).toEqual({
34
+ passed: true,
35
+ failures: [],
36
+ warnings: [],
37
+ tested: 0,
38
+ });
39
+ expect(parseConftestOutput("{}")).toEqual({
40
+ passed: true,
41
+ failures: [],
42
+ warnings: [],
43
+ tested: 0,
44
+ });
45
+ });
46
+
47
+ it("counts successes and reports zero failures as passed", () => {
48
+ const out = parseConftestOutput(
49
+ JSON.stringify([
50
+ { filename: "plan.json", namespace: "main", successes: 3, failures: [], warnings: [] },
51
+ ]),
52
+ );
53
+ expect(out.passed).toBe(true);
54
+ expect(out.tested).toBe(3);
55
+ expect(out.failures).toHaveLength(0);
56
+ });
57
+
58
+ it("captures failures with file + level and fails the gate", () => {
59
+ const out = parseConftestOutput(
60
+ JSON.stringify([
61
+ {
62
+ filename: "plan.json",
63
+ successes: 1,
64
+ failures: [{ msg: "S3 bucket must be encrypted" }],
65
+ warnings: [{ msg: "consider tagging" }],
66
+ },
67
+ ]),
68
+ );
69
+ expect(out.passed).toBe(false);
70
+ expect(out.failures).toEqual([
71
+ { msg: "S3 bucket must be encrypted", file: "plan.json", level: "failure" },
72
+ ]);
73
+ expect(out.warnings).toEqual([
74
+ { msg: "consider tagging", file: "plan.json", level: "warning" },
75
+ ]);
76
+ // 1 success + 1 failure + 1 warning
77
+ expect(out.tested).toBe(3);
78
+ });
79
+
80
+ it("warnings alone do not fail the gate", () => {
81
+ const out = parseConftestOutput(
82
+ JSON.stringify([{ filename: "plan.json", warnings: [{ msg: "w" }] }]),
83
+ );
84
+ expect(out.passed).toBe(true);
85
+ expect(out.warnings).toHaveLength(1);
86
+ });
87
+
88
+ it("aggregates failures across multiple files", () => {
89
+ const out = parseConftestOutput(
90
+ JSON.stringify([
91
+ { filename: "a.json", failures: [{ msg: "x" }] },
92
+ { filename: "b.json", failures: [{ msg: "y" }] },
93
+ ]),
94
+ );
95
+ expect(out.passed).toBe(false);
96
+ expect(out.failures.map((f) => f.file)).toEqual(["a.json", "b.json"]);
97
+ });
98
+
99
+ it("defaults a missing failure message and filename", () => {
100
+ const out = parseConftestOutput(JSON.stringify([{ failures: [{}] }]));
101
+ expect(out.failures[0]).toEqual({ msg: "policy violation", file: "(plan)", level: "failure" });
102
+ });
103
+
104
+ it("defaults a missing warning message", () => {
105
+ const out = parseConftestOutput(JSON.stringify([{ warnings: [{}] }]));
106
+ expect(out.warnings[0]).toEqual({ msg: "policy warning", file: "(plan)", level: "warning" });
107
+ });
108
+ });
109
+
110
+ // ── PolicyCheckTool (the shell-out wrapper around the pure parser) ────────────
111
+
112
+ type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
113
+
114
+ async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
115
+ const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
116
+ return exec(params, {});
117
+ }
118
+
119
+ const CWD = join(path("ws"), "repo");
120
+
121
+ function path(...segs: string[]): string {
122
+ return join("/", ...segs);
123
+ }
124
+
125
+ function makeCtx(cwd: string | undefined = CWD): ToolContext {
126
+ return { payload: { cwd } } as unknown as ToolContext;
127
+ }
128
+
129
+ /** existsSync only returns true for the given absolute paths. */
130
+ function filesExist(...paths: string[]): void {
131
+ existsSyncMock.mockImplementation((p) => paths.includes(String(p)));
132
+ }
133
+
134
+ function spawnResult(over: Partial<ReturnType<typeof spawnSync>>): ReturnType<typeof spawnSync> {
135
+ return {
136
+ pid: 1,
137
+ output: [],
138
+ stdout: "[]",
139
+ stderr: "",
140
+ status: 0,
141
+ signal: null,
142
+ error: undefined,
143
+ ...over,
144
+ } as unknown as ReturnType<typeof spawnSync>;
145
+ }
146
+
147
+ describe("PolicyCheckTool", () => {
148
+ beforeEach(() => {
149
+ vi.clearAllMocks();
150
+ existsSyncMock.mockReturnValue(false);
151
+ });
152
+
153
+ it("skips with no_policy_dir when no default policy dir exists", async () => {
154
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
155
+
156
+ expect(result.isError).toBeUndefined();
157
+ expect(result.content[0].text).toContain("ok: false");
158
+ expect(result.content[0].text).toContain("code: no_policy_dir");
159
+ expect(spawnSyncMock).not.toHaveBeenCalled();
160
+ });
161
+
162
+ it("skips with no_policy_dir when the explicit policy_dir is missing", async () => {
163
+ const result = await runTool(PolicyCheckTool(makeCtx()), { policy_dir: "rego" });
164
+
165
+ expect(result.content[0].text).toContain("code: no_policy_dir");
166
+ expect(existsSyncMock).toHaveBeenCalledWith(join(CWD, "rego"));
167
+ });
168
+
169
+ it("skips with target_not_found when the explicit target does not exist", async () => {
170
+ filesExist(join(CWD, "policy"));
171
+ const result = await runTool(PolicyCheckTool(makeCtx()), { target: "missing.json" });
172
+
173
+ expect(result.content[0].text).toContain("code: target_not_found");
174
+ expect(spawnSyncMock).not.toHaveBeenCalled();
175
+ });
176
+
177
+ it("skips with no_target when no conventional plan JSON exists", async () => {
178
+ filesExist(join(CWD, "policies"));
179
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
180
+
181
+ expect(result.content[0].text).toContain("code: no_target");
182
+ expect(spawnSyncMock).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it("skips with conftest_not_installed on a spawn ENOENT", async () => {
186
+ filesExist(join(CWD, "policy"), join(CWD, "plan.json"));
187
+ const enoent = Object.assign(new Error("spawn conftest ENOENT"), { code: "ENOENT" });
188
+ spawnSyncMock.mockReturnValue(spawnResult({ error: enoent }));
189
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
190
+
191
+ expect(result.content[0].text).toContain("code: conftest_not_installed");
192
+ });
193
+
194
+ it("skips with conftest_failed when a non-zero exit evaluated nothing", async () => {
195
+ filesExist(join(CWD, "policy"), join(CWD, "plan.json"));
196
+ spawnSyncMock.mockReturnValue(
197
+ spawnResult({ status: 1, stdout: "", stderr: "rego_parse_error: unexpected token" }),
198
+ );
199
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
200
+
201
+ expect(result.content[0].text).toContain("code: conftest_failed");
202
+ expect(result.content[0].text).toContain("rego_parse_error");
203
+ });
204
+
205
+ it("reports an unknown error when conftest fails without stderr", async () => {
206
+ filesExist(join(CWD, "policy"), join(CWD, "plan.json"));
207
+ spawnSyncMock.mockReturnValue(spawnResult({ status: 2, stdout: "", stderr: " " }));
208
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
209
+
210
+ expect(result.content[0].text).toContain("unknown error");
211
+ });
212
+
213
+ it("passes a clean evaluation and reports the resolved dir + target", async () => {
214
+ filesExist(join(CWD, "policy"), join(CWD, "plan.json"));
215
+ spawnSyncMock.mockReturnValue(
216
+ spawnResult({ stdout: JSON.stringify([{ filename: "plan.json", successes: 4 }]) }),
217
+ );
218
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
219
+
220
+ const text = result.content[0].text;
221
+ expect(text).toContain("ok: true");
222
+ expect(text).toContain("passed: true");
223
+ expect(text).toContain("tested: 4");
224
+ expect(spawnSyncMock).toHaveBeenCalledWith(
225
+ "conftest",
226
+ ["test", "--output", "json", "-p", join(CWD, "policy"), join(CWD, "plan.json")],
227
+ expect.objectContaining({ cwd: CWD }),
228
+ );
229
+ });
230
+
231
+ it("flows a genuine denial (non-zero exit WITH failures) through to passed: false", async () => {
232
+ filesExist(join(CWD, "policy"), join(CWD, "plan.json"));
233
+ spawnSyncMock.mockReturnValue(
234
+ spawnResult({
235
+ status: 1,
236
+ stdout: JSON.stringify([
237
+ { filename: "plan.json", failures: [{ msg: "bucket must be encrypted" }] },
238
+ ]),
239
+ }),
240
+ );
241
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
242
+
243
+ const text = result.content[0].text;
244
+ expect(text).toContain("ok: true");
245
+ expect(text).toContain("passed: false");
246
+ expect(text).toContain("failure_count: 1");
247
+ expect(text).toContain("bucket must be encrypted");
248
+ });
249
+
250
+ it("falls back through the default policy dirs in order", async () => {
251
+ filesExist(join(CWD, ".conftest"), join(CWD, "tfplan.json"));
252
+ spawnSyncMock.mockReturnValue(spawnResult({ stdout: "[]" }));
253
+ const result = await runTool(PolicyCheckTool(makeCtx()), {});
254
+
255
+ expect(result.content[0].text).toContain("ok: true");
256
+ expect(spawnSyncMock).toHaveBeenCalledWith(
257
+ "conftest",
258
+ expect.arrayContaining([join(CWD, ".conftest"), join(CWD, "tfplan.json")]),
259
+ expect.anything(),
260
+ );
261
+ });
262
+
263
+ it("accepts absolute policy_dir and target paths and a default cwd", async () => {
264
+ const absPolicy = join(path("abs"), "rego");
265
+ const absTarget = join(path("abs"), "plan.json");
266
+ expect(isAbsolute(absPolicy)).toBe(true);
267
+ filesExist(absPolicy, absTarget);
268
+ spawnSyncMock.mockReturnValue(spawnResult({ stdout: "[]" }));
269
+ const noCwdCtx = { payload: {} } as unknown as ToolContext;
270
+ const result = await runTool(PolicyCheckTool(noCwdCtx), {
271
+ policy_dir: absPolicy,
272
+ target: absTarget,
273
+ });
274
+
275
+ expect(result.content[0].text).toContain("ok: true");
276
+ expect(spawnSyncMock).toHaveBeenCalledWith(
277
+ "conftest",
278
+ ["test", "--output", "json", "-p", absPolicy, absTarget],
279
+ expect.objectContaining({ cwd: process.cwd() }),
280
+ );
281
+ });
282
+ });