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,217 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { PayloadEvent } from "#app/external";
3
+ import { resolveBody, resolveBodyAssets } from "#app/utils/body";
4
+ import type { OctokitWithPlugins } from "#app/utils/github";
5
+ import type { RunContextData } from "#app/utils/runContextData";
6
+
7
+ vi.mock("#app/utils/assets", () => ({
8
+ // echo the markdown back with a marker so call-through is observable
9
+ downloadAssetsInMarkdown: vi.fn(async (markdown: string) => `downloaded:${markdown}`),
10
+ }));
11
+
12
+ import { downloadAssetsInMarkdown } from "#app/utils/assets";
13
+
14
+ const downloadMock = vi.mocked(downloadAssetsInMarkdown);
15
+
16
+ const repo = { owner: "acme", name: "infra" } as RunContextData["repo"];
17
+
18
+ function makeOctokit() {
19
+ const issuesGetComment = vi.fn(async () => ({ data: { body_html: "<p>comment html</p>" } }));
20
+ const issuesGet = vi.fn(async () => ({ data: { body_html: "<p>issue html</p>" } }));
21
+ const pullsGetReview = vi.fn(async () => ({ data: { body_html: "<p>review html</p>" } }));
22
+ const pullsGetReviewComment = vi.fn(async () => ({
23
+ data: { body_html: "<p>review comment html</p>" },
24
+ }));
25
+ const octokit = {
26
+ rest: {
27
+ issues: { getComment: issuesGetComment, get: issuesGet },
28
+ pulls: { getReview: pullsGetReview, getReviewComment: pullsGetReviewComment },
29
+ },
30
+ } as unknown as OctokitWithPlugins;
31
+ return { octokit, issuesGetComment, issuesGet, pullsGetReview, pullsGetReviewComment };
32
+ }
33
+
34
+ function ctxFor(event: Record<string, unknown>, octokit: OctokitWithPlugins) {
35
+ return {
36
+ event: event as unknown as PayloadEvent,
37
+ octokit,
38
+ repo,
39
+ tmpdir: "/tmp/run",
40
+ githubToken: "tok",
41
+ };
42
+ }
43
+
44
+ const IMG_BODY = "look: ![shot](https://github.com/user-attachments/assets/abc)";
45
+
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ });
49
+
50
+ describe("resolveBodyAssets", () => {
51
+ it("returns null for a missing body", async () => {
52
+ await expect(
53
+ resolveBodyAssets({ body: null, bodyHtml: undefined, tmpdir: "/t", githubToken: "x" }),
54
+ ).resolves.toBeNull();
55
+ await expect(
56
+ resolveBodyAssets({ body: undefined, bodyHtml: "<p>hi</p>", tmpdir: "/t", githubToken: "x" }),
57
+ ).resolves.toBeNull();
58
+ expect(downloadMock).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it("downloads assets for a plain markdown body without conversion", async () => {
62
+ const result = await resolveBodyAssets({
63
+ body: "no images here",
64
+ bodyHtml: "<p>ignored</p>",
65
+ tmpdir: "/t",
66
+ githubToken: "x",
67
+ });
68
+ expect(result).toBe("downloaded:no images here");
69
+ expect(downloadMock).toHaveBeenCalledWith("no images here", "/t", "x");
70
+ });
71
+
72
+ it("converts body_html to markdown when the body has images", async () => {
73
+ const result = await resolveBodyAssets({
74
+ body: IMG_BODY,
75
+ bodyHtml: '<p>see <img src="https://signed.example/img.png" alt="shot"></p>',
76
+ tmpdir: "/t",
77
+ githubToken: "x",
78
+ });
79
+ expect(result).toContain("downloaded:");
80
+ expect(result).toContain("https://signed.example/img.png");
81
+ expect(result).not.toContain("user-attachments");
82
+ });
83
+
84
+ it("keeps the raw body when it has images but no rendered html", async () => {
85
+ const result = await resolveBodyAssets({
86
+ body: IMG_BODY,
87
+ bodyHtml: undefined,
88
+ tmpdir: "/t",
89
+ githubToken: "x",
90
+ });
91
+ expect(result).toBe(`downloaded:${IMG_BODY}`);
92
+ });
93
+ });
94
+
95
+ describe("resolveBody", () => {
96
+ it("does not fetch body_html when the body has no images", async () => {
97
+ const { octokit, issuesGet, issuesGetComment } = makeOctokit();
98
+ const result = await resolveBody(
99
+ ctxFor({ trigger: "issues_opened", issue_number: 3, body: "plain text" }, octokit),
100
+ );
101
+ expect(result).toBe("downloaded:plain text");
102
+ expect(issuesGet).not.toHaveBeenCalled();
103
+ expect(issuesGetComment).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it("fetches comment html for issue_comment_created", async () => {
107
+ const { octokit, issuesGetComment } = makeOctokit();
108
+ const result = await resolveBody(
109
+ ctxFor({ trigger: "issue_comment_created", comment_id: 11, body: IMG_BODY }, octokit),
110
+ );
111
+ expect(issuesGetComment).toHaveBeenCalledWith(
112
+ expect.objectContaining({
113
+ owner: "acme",
114
+ repo: "infra",
115
+ comment_id: 11,
116
+ headers: { accept: "application/vnd.github.full+json" },
117
+ }),
118
+ );
119
+ expect(result).toBe("downloaded:comment html");
120
+ });
121
+
122
+ it("returns undefined html when issue_comment_created lacks a comment id", async () => {
123
+ const { octokit, issuesGetComment } = makeOctokit();
124
+ const result = await resolveBody(
125
+ ctxFor({ trigger: "issue_comment_created", body: IMG_BODY }, octokit),
126
+ );
127
+ expect(issuesGetComment).not.toHaveBeenCalled();
128
+ expect(result).toBe(`downloaded:${IMG_BODY}`);
129
+ });
130
+
131
+ it.each([
132
+ "issues_opened",
133
+ "issues_assigned",
134
+ "issues_labeled",
135
+ ])("fetches the issue html for %s", async (trigger) => {
136
+ const { octokit, issuesGet } = makeOctokit();
137
+ const result = await resolveBody(ctxFor({ trigger, issue_number: 5, body: IMG_BODY }, octokit));
138
+ expect(issuesGet).toHaveBeenCalledWith(expect.objectContaining({ issue_number: 5 }));
139
+ expect(result).toBe("downloaded:issue html");
140
+ });
141
+
142
+ it.each([
143
+ "pull_request_opened",
144
+ "pull_request_ready_for_review",
145
+ "pull_request_synchronize",
146
+ "pull_request_review_requested",
147
+ "check_suite_completed",
148
+ ])("fetches the PR body html via issues.get for %s", async (trigger) => {
149
+ const { octokit, issuesGet } = makeOctokit();
150
+ const result = await resolveBody(ctxFor({ trigger, issue_number: 9, body: IMG_BODY }, octokit));
151
+ expect(issuesGet).toHaveBeenCalledWith(expect.objectContaining({ issue_number: 9 }));
152
+ expect(result).toBe("downloaded:issue html");
153
+ });
154
+
155
+ it("fetches the review html for pull_request_review_submitted", async () => {
156
+ const { octokit, pullsGetReview } = makeOctokit();
157
+ const result = await resolveBody(
158
+ ctxFor(
159
+ {
160
+ trigger: "pull_request_review_submitted",
161
+ issue_number: 9,
162
+ review_id: 21,
163
+ body: IMG_BODY,
164
+ },
165
+ octokit,
166
+ ),
167
+ );
168
+ expect(pullsGetReview).toHaveBeenCalledWith(
169
+ expect.objectContaining({ pull_number: 9, review_id: 21 }),
170
+ );
171
+ expect(result).toBe("downloaded:review html");
172
+ });
173
+
174
+ it("skips the fetch when pull_request_review_submitted lacks a review id", async () => {
175
+ const { octokit, pullsGetReview } = makeOctokit();
176
+ await resolveBody(
177
+ ctxFor(
178
+ { trigger: "pull_request_review_submitted", issue_number: 9, body: IMG_BODY },
179
+ octokit,
180
+ ),
181
+ );
182
+ expect(pullsGetReview).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it("fetches the review comment html for pull_request_review_comment_created", async () => {
186
+ const { octokit, pullsGetReviewComment } = makeOctokit();
187
+ const result = await resolveBody(
188
+ ctxFor(
189
+ { trigger: "pull_request_review_comment_created", comment_id: 31, body: IMG_BODY },
190
+ octokit,
191
+ ),
192
+ );
193
+ expect(pullsGetReviewComment).toHaveBeenCalledWith(expect.objectContaining({ comment_id: 31 }));
194
+ expect(result).toBe("downloaded:review comment html");
195
+ });
196
+
197
+ it("fetches the plan comment html for implement_plan", async () => {
198
+ const { octokit, issuesGetComment } = makeOctokit();
199
+ const result = await resolveBody(
200
+ ctxFor({ trigger: "implement_plan", plan_comment_id: 41, body: IMG_BODY }, octokit),
201
+ );
202
+ expect(issuesGetComment).toHaveBeenCalledWith(expect.objectContaining({ comment_id: 41 }));
203
+ expect(result).toBe("downloaded:comment html");
204
+ });
205
+
206
+ it.each([
207
+ "workflow_dispatch",
208
+ "fix_review",
209
+ "unknown",
210
+ ])("never fetches html for bodyless trigger %s", async (trigger) => {
211
+ const { octokit, issuesGet, issuesGetComment } = makeOctokit();
212
+ const result = await resolveBody(ctxFor({ trigger, body: IMG_BODY }, octokit));
213
+ expect(issuesGet).not.toHaveBeenCalled();
214
+ expect(issuesGetComment).not.toHaveBeenCalled();
215
+ expect(result).toBe(`downloaded:${IMG_BODY}`);
216
+ });
217
+ });
@@ -0,0 +1,168 @@
1
+ import TurndownService from "turndown";
2
+ import type { PayloadEvent } from "#app/external";
3
+ import { downloadAssetsInMarkdown } from "#app/utils/assets";
4
+ import type { OctokitWithPlugins } from "#app/utils/github";
5
+ import type { RunContextData } from "#app/utils/runContextData";
6
+
7
+ const turndown = new TurndownService();
8
+
9
+ function hasImages(body: string | null | undefined): boolean {
10
+ if (!body) return false;
11
+ return body.includes("<img") || body.includes("![");
12
+ }
13
+
14
+ interface ResolveBodyContext {
15
+ event: PayloadEvent;
16
+ octokit: OctokitWithPlugins;
17
+ repo: RunContextData["repo"];
18
+ tmpdir: string;
19
+ githubToken: string;
20
+ }
21
+
22
+ /**
23
+ * resolves the body of an event by fetching body_html and converting to markdown.
24
+ * only fetches body_html if the body contains images (to avoid unnecessary API calls).
25
+ * this ensures agents receive markdown with working signed image URLs instead of
26
+ * broken user-attachments URLs.
27
+ */
28
+ export async function resolveBody(ctx: ResolveBodyContext): Promise<string | null> {
29
+ const body = ctx.event.body;
30
+ const bodyHtml = hasImages(body) ? await fetchBodyHtml(ctx) : undefined;
31
+ return resolveBodyAssets({
32
+ body,
33
+ bodyHtml,
34
+ tmpdir: ctx.tmpdir,
35
+ githubToken: ctx.githubToken,
36
+ });
37
+ }
38
+
39
+ interface ResolveBodyAssetsContext {
40
+ body: string | null | undefined;
41
+ bodyHtml: string | null | undefined;
42
+ tmpdir: string;
43
+ githubToken: string;
44
+ }
45
+
46
+ /**
47
+ * downloads github-hosted image assets in a body to disk and rewrites the urls to local
48
+ * paths so the agent can read them. when the body has images and a rendered `bodyHtml`
49
+ * is supplied, the html is turndowned first: github only exposes attachments as signed,
50
+ * self-authenticating `*.githubusercontent.com` urls through body_html — the raw
51
+ * `github.com/user-attachments/...` urls in unrendered markdown 404 for the installation
52
+ * token. callers that fetch a body should request it with the `application/vnd.github.full+json`
53
+ * media type and pass `body_html` here.
54
+ */
55
+ export async function resolveBodyAssets(ctx: ResolveBodyAssetsContext): Promise<string | null> {
56
+ let body = ctx.body ?? null;
57
+ if (body && hasImages(body) && ctx.bodyHtml) {
58
+ body = turndown.turndown(ctx.bodyHtml);
59
+ }
60
+ if (!body) return ctx.body ?? null;
61
+ return downloadAssetsInMarkdown(body, ctx.tmpdir, ctx.githubToken);
62
+ }
63
+
64
+ async function fetchBodyHtml(ctx: ResolveBodyContext): Promise<string | undefined> {
65
+ const event = ctx.event;
66
+ const headers = { accept: "application/vnd.github.full+json" };
67
+ const owner = ctx.repo.owner;
68
+ const repo = ctx.repo.name;
69
+
70
+ switch (event.trigger) {
71
+ case "issue_comment_created":
72
+ if (!event.comment_id) return;
73
+ return (
74
+ await ctx.octokit.rest.issues.getComment({
75
+ owner,
76
+ repo,
77
+ comment_id: event.comment_id,
78
+ headers,
79
+ })
80
+ ).data.body_html;
81
+
82
+ case "issues_opened":
83
+ case "issues_assigned":
84
+ case "issues_labeled":
85
+ if (!event.issue_number) return;
86
+ return (
87
+ await ctx.octokit.rest.issues.get({
88
+ owner,
89
+ repo,
90
+ issue_number: event.issue_number,
91
+ headers,
92
+ })
93
+ ).data.body_html;
94
+
95
+ case "pull_request_opened":
96
+ case "pull_request_ready_for_review":
97
+ case "pull_request_synchronize":
98
+ case "pull_request_review_requested":
99
+ // PRs are also issues - use issues.get which returns body_html
100
+ if (!event.issue_number) return;
101
+ return (
102
+ await ctx.octokit.rest.issues.get({
103
+ owner,
104
+ repo,
105
+ issue_number: event.issue_number,
106
+ headers,
107
+ })
108
+ ).data.body_html;
109
+
110
+ case "pull_request_review_submitted":
111
+ if (!event.issue_number || !event.review_id) return;
112
+ return (
113
+ await ctx.octokit.rest.pulls.getReview({
114
+ owner,
115
+ repo,
116
+ pull_number: event.issue_number,
117
+ review_id: event.review_id,
118
+ headers,
119
+ })
120
+ ).data.body_html;
121
+
122
+ case "pull_request_review_comment_created":
123
+ if (!event.comment_id) return;
124
+ return (
125
+ await ctx.octokit.rest.pulls.getReviewComment({
126
+ owner,
127
+ repo,
128
+ comment_id: event.comment_id,
129
+ headers,
130
+ })
131
+ ).data.body_html;
132
+
133
+ case "check_suite_completed":
134
+ // body is the PR body
135
+ if (!event.issue_number) return;
136
+ return (
137
+ await ctx.octokit.rest.issues.get({
138
+ owner,
139
+ repo,
140
+ issue_number: event.issue_number,
141
+ headers,
142
+ })
143
+ ).data.body_html;
144
+
145
+ case "implement_plan":
146
+ // body is the plan content from an issue comment
147
+ if (!event.plan_comment_id) return;
148
+ return (
149
+ await ctx.octokit.rest.issues.getComment({
150
+ owner,
151
+ repo,
152
+ comment_id: event.plan_comment_id,
153
+ headers,
154
+ })
155
+ ).data.body_html;
156
+
157
+ // triggers without a body field that needs resolution
158
+ case "workflow_dispatch":
159
+ case "fix_review":
160
+ case "unknown":
161
+ return undefined;
162
+
163
+ default:
164
+ // exhaustiveness check - TypeScript will error if a trigger is missing
165
+ event satisfies never;
166
+ return undefined;
167
+ }
168
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildTerramendFooter } from "#app/utils/buildTerramendFooter";
3
+
4
+ describe("buildTerramendFooter — fallbackFrom annotation", () => {
5
+ it("renders the provider display name when fallbackFrom is set", () => {
6
+ const footer = buildTerramendFooter({
7
+ model: "opencode/big-pickle",
8
+ fallbackFrom: "anthropic/claude-opus",
9
+ });
10
+ expect(footer).toContain(
11
+ "Using `Big Pickle` (free) (credentials for Anthropic not configured)",
12
+ );
13
+ });
14
+
15
+ it("works for OpenAI's display name too", () => {
16
+ const footer = buildTerramendFooter({
17
+ model: "opencode/big-pickle",
18
+ fallbackFrom: "openai/gpt",
19
+ });
20
+ expect(footer).toContain("(credentials for OpenAI not configured)");
21
+ });
22
+
23
+ it("falls back to the raw provider key when the slug provider is unknown to the catalog", () => {
24
+ const footer = buildTerramendFooter({
25
+ model: "opencode/big-pickle",
26
+ fallbackFrom: "some-unknown/model",
27
+ });
28
+ expect(footer).toContain("(credentials for some-unknown not configured)");
29
+ });
30
+
31
+ it("omits the annotation when fallbackFrom is not set", () => {
32
+ const footer = buildTerramendFooter({
33
+ model: "anthropic/claude-opus",
34
+ });
35
+ expect(footer).toContain("Using `Claude Opus`");
36
+ expect(footer).not.toContain("not configured");
37
+ });
38
+ });
@@ -0,0 +1,82 @@
1
+ import { getModelProvider, modelAliases, providers, resolveDisplayAlias } from "#app/models";
2
+
3
+ export const TERRAMEND_DIVIDER = "<!-- TERRAMEND_DIVIDER_DO_NOT_REMOVE_PLZ -->";
4
+
5
+ export interface BuildTerramendFooterParams {
6
+ /** add "via Terramend" link */
7
+ triggeredBy?: boolean;
8
+ /** arbitrary custom parts (e.g., action links) */
9
+ customParts?: string[] | undefined;
10
+ /** model slug from payload (e.g., "anthropic/claude-opus"). shown in footer as "Using `Model Name`" */
11
+ model?: string | undefined;
12
+ /**
13
+ * When the action engaged the BYOK fallback, this is the slug the user
14
+ * had configured (e.g. "anthropic/claude-opus") — the footer renders
15
+ * `Using <free model> (credentials for <configured> not configured)`
16
+ * so the substitution is visible in PR comments + reviews.
17
+ */
18
+ fallbackFrom?: string | undefined;
19
+ }
20
+
21
+ /** Provider display name (e.g. "Anthropic") for the slug, or the raw provider segment as a fallback. */
22
+ function providerDisplayName(slug: string): string {
23
+ try {
24
+ const key = getModelProvider(slug);
25
+ const meta = providers[key as keyof typeof providers];
26
+ return meta?.displayName ?? key;
27
+ } catch {
28
+ // raw IDs without a `/` (Bedrock model IDs) — never reach this function
29
+ // in practice because the BYOK fallback skips Bedrock, but defensively
30
+ // return the slug itself rather than throw if it ever does.
31
+ return slug;
32
+ }
33
+ }
34
+
35
+ function formatModelLabel(params: { model: string; fallbackFrom?: string | undefined }): string {
36
+ const alias =
37
+ resolveDisplayAlias(params.model) ??
38
+ // reverse-lookup: when the caller passes an effective model (proxy or
39
+ // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
40
+ // a stored alias slug, find the alias whose resolve target matches so we
41
+ // still render a friendly display name.
42
+ modelAliases.find((a) => a.resolve === params.model || a.openRouterResolve === params.model);
43
+ const displayName = alias?.displayName ?? params.model;
44
+ const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
45
+ if (!params.fallbackFrom) return base;
46
+ return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} not configured)`;
47
+ }
48
+
49
+ /**
50
+ * build a terramend footer with configurable parts
51
+ * order: action links (customParts) > model > attribution
52
+ */
53
+ export function buildTerramendFooter(params: BuildTerramendFooterParams): string {
54
+ const parts: string[] = [];
55
+
56
+ if (params.customParts) {
57
+ parts.push(...params.customParts);
58
+ }
59
+
60
+ if (params.triggeredBy) {
61
+ parts.push("via Terramend");
62
+ }
63
+
64
+ if (params.model) {
65
+ parts.push(
66
+ `Using ${formatModelLabel({ model: params.model, fallbackFrom: params.fallbackFrom })}`,
67
+ );
68
+ }
69
+
70
+ return `\n\n${TERRAMEND_DIVIDER}\n<sup>${parts.join(" | ")}</sup>`;
71
+ }
72
+
73
+ /**
74
+ * strip any existing terramend footer from a comment body
75
+ */
76
+ export function stripExistingFooter(body: string): string {
77
+ const dividerIndex = body.indexOf(TERRAMEND_DIVIDER);
78
+ if (dividerIndex === -1) {
79
+ return body;
80
+ }
81
+ return body.substring(0, dividerIndex).trimEnd();
82
+ }