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,257 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import { delimiter, join } from "node:path";
4
+ import semver from "semver";
5
+ import { log } from "#app/utils/cli";
6
+ import { spawn } from "#app/utils/subprocess";
7
+
8
+ export type SupportedPackageManager = "npm" | "pnpm" | "yarn" | "bun";
9
+
10
+ const SUPPORTED_NAMES: readonly SupportedPackageManager[] = ["npm", "pnpm", "yarn", "bun"];
11
+
12
+ // corepack ships pnpm + yarn shims out of the box. it does not ship bun or
13
+ // npm (npm comes with node). callers fall back to the legacy npm-install-g
14
+ // path for managers outside this set.
15
+ const COREPACK_MANAGED: readonly SupportedPackageManager[] = ["pnpm", "yarn"];
16
+
17
+ export interface PackageManagerSpec {
18
+ name: SupportedPackageManager;
19
+ /**
20
+ * either a concrete semver (e.g. "11.1.1") or a range (e.g. "^11.0.0").
21
+ * `concrete` distinguishes — corepack only accepts concrete versions.
22
+ */
23
+ version: string;
24
+ concrete: boolean;
25
+ /** which package.json field this came from */
26
+ source: "devEngines" | "packageManager";
27
+ }
28
+
29
+ interface PackageJson {
30
+ packageManager?: string;
31
+ devEngines?: {
32
+ packageManager?: {
33
+ name?: string;
34
+ version?: string;
35
+ onFail?: string;
36
+ };
37
+ };
38
+ }
39
+
40
+ function isSupported(name: string): name is SupportedPackageManager {
41
+ return (SUPPORTED_NAMES as readonly string[]).includes(name);
42
+ }
43
+
44
+ function parsePackageManagerField(value: string): PackageManagerSpec | null {
45
+ // npm spec form is "name@version[+integrity]" — corepack adds the integrity
46
+ // suffix; we strip it because it's not a semver.
47
+ const withoutHash = value.split("+")[0]!;
48
+ const at = withoutHash.lastIndexOf("@");
49
+ if (at <= 0) return null;
50
+ const name = withoutHash.slice(0, at);
51
+ const version = withoutHash.slice(at + 1);
52
+ if (!isSupported(name)) {
53
+ log.warning(`» unknown packageManager in package.json: ${value}`);
54
+ return null;
55
+ }
56
+ return {
57
+ name,
58
+ version,
59
+ concrete: semver.valid(version) !== null,
60
+ source: "packageManager",
61
+ };
62
+ }
63
+
64
+ function parseDevEnginesField(
65
+ field: NonNullable<NonNullable<PackageJson["devEngines"]>["packageManager"]>,
66
+ ): PackageManagerSpec | null {
67
+ if (!field.name || !field.version) return null;
68
+ if (!isSupported(field.name)) {
69
+ log.warning(`» unknown devEngines.packageManager.name in package.json: ${field.name}`);
70
+ return null;
71
+ }
72
+ const version = field.version.trim();
73
+ return {
74
+ name: field.name,
75
+ version,
76
+ concrete: semver.valid(version) !== null,
77
+ source: "devEngines",
78
+ };
79
+ }
80
+
81
+ /**
82
+ * resolve the project's intended package manager from package.json. precedence
83
+ * matches pnpm 11+: `devEngines.packageManager` wins over `packageManager`.
84
+ * when both are present, a concrete `packageManager` that satisfies a
85
+ * `devEngines` range is preferred (we can pin it via corepack); otherwise
86
+ * we warn on disagreement and stick with `devEngines`.
87
+ */
88
+ export async function resolvePackageManagerSpec(cwd: string): Promise<PackageManagerSpec | null> {
89
+ const pkgPath = join(cwd, "package.json");
90
+ if (!existsSync(pkgPath)) return null;
91
+
92
+ let pkg: PackageJson;
93
+ try {
94
+ pkg = JSON.parse(await readFile(pkgPath, "utf-8")) as PackageJson;
95
+ } catch (err) {
96
+ log.warning(
97
+ `» failed to parse package.json for package manager resolution: ${err instanceof Error ? err.message : String(err)}`,
98
+ );
99
+ return null;
100
+ }
101
+
102
+ const devSpec = pkg.devEngines?.packageManager
103
+ ? parseDevEnginesField(pkg.devEngines.packageManager)
104
+ : null;
105
+ const pmSpec = pkg.packageManager?.trim()
106
+ ? parsePackageManagerField(pkg.packageManager.trim())
107
+ : null;
108
+
109
+ if (!devSpec) return pmSpec;
110
+ if (!pmSpec) return devSpec;
111
+
112
+ if (devSpec.name !== pmSpec.name) {
113
+ log.warning(
114
+ `» devEngines.packageManager (${devSpec.name}) disagrees with packageManager (${pmSpec.name}); using devEngines per pnpm 11 precedence`,
115
+ );
116
+ return devSpec;
117
+ }
118
+
119
+ // same manager — try to land on a concrete version we can pin via corepack.
120
+ if (devSpec.concrete) {
121
+ if (pmSpec.concrete && devSpec.version !== pmSpec.version) {
122
+ log.warning(
123
+ `» devEngines.packageManager (${devSpec.version}) disagrees with packageManager (${pmSpec.version}); using devEngines per pnpm 11 precedence`,
124
+ );
125
+ }
126
+ return devSpec;
127
+ }
128
+
129
+ if (pmSpec.concrete && semver.satisfies(pmSpec.version, devSpec.version)) {
130
+ return pmSpec;
131
+ }
132
+
133
+ if (pmSpec.concrete) {
134
+ log.warning(
135
+ `» packageManager (${pmSpec.version}) does not satisfy devEngines range (${devSpec.version}); using devEngines`,
136
+ );
137
+ }
138
+ return devSpec;
139
+ }
140
+
141
+ interface CorepackResult {
142
+ exitCode: number;
143
+ stderr: string;
144
+ }
145
+
146
+ async function runCorepack(args: string[]): Promise<CorepackResult> {
147
+ const result = await spawn({
148
+ cmd: "corepack",
149
+ args,
150
+ env: {
151
+ PATH: process.env.PATH || "",
152
+ HOME: process.env.HOME || "",
153
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
154
+ },
155
+ onStdout: (chunk) => process.stdout.write(chunk),
156
+ onStderr: (chunk) => process.stderr.write(chunk),
157
+ });
158
+ return { exitCode: result.exitCode, stderr: result.stderr };
159
+ }
160
+
161
+ async function currentVersion(name: SupportedPackageManager): Promise<string | null> {
162
+ const result = await spawn({
163
+ cmd: name,
164
+ args: ["--version"],
165
+ env: { PATH: process.env.PATH || "" },
166
+ });
167
+ if (result.exitCode !== 0) return null;
168
+ return result.stdout.trim();
169
+ }
170
+
171
+ /** the per-run directory the corepack shim is installed into. deliberately
172
+ * NOT the node bin dir: that's npm's `-g` target, and a corepack shim sitting
173
+ * there makes a customer setup script's `npm i -g pnpm` abort with EEXIST
174
+ * (npm refuses to clobber a binary it doesn't own). lives under the run
175
+ * tmpdir so it's cleaned up with everything else. */
176
+ export function packageManagerBinDir(tmpdir: string): string {
177
+ return join(tmpdir, "pm-bin");
178
+ }
179
+
180
+ export interface EnsurePackageManagerParams {
181
+ spec: PackageManagerSpec;
182
+ /** directory to install the corepack shim into (see `packageManagerBinDir`).
183
+ * prepended to PATH so the pinned binary resolves by name. */
184
+ binDir: string;
185
+ }
186
+
187
+ /**
188
+ * ensure the requested package manager is on PATH at the declared version,
189
+ * provisioning via corepack when applicable. returns true if PATH now
190
+ * resolves to that version, false if we couldn't pin it (in which case
191
+ * the caller should treat PATH as untrusted and may fall back to its
192
+ * legacy install path).
193
+ *
194
+ * the corepack shim is installed into `params.binDir` (prepended to PATH),
195
+ * not the node bin dir, so a later `npm i -g pnpm` in a setup hook can't
196
+ * collide with it. our dir wins the PATH lookup, so the pinned version is
197
+ * also what resolves even if that `npm i -g` succeeds into the node bin dir.
198
+ *
199
+ * never throws: network failure, missing corepack, range-only versions —
200
+ * all degrade to "log warning, return false". the existing PATH binary
201
+ * still works; we just don't get our version guarantee.
202
+ */
203
+ export async function ensurePackageManager(params: EnsurePackageManagerParams): Promise<boolean> {
204
+ const spec = params.spec;
205
+ if (spec.name === "npm") return true;
206
+
207
+ if (!(COREPACK_MANAGED as readonly string[]).includes(spec.name)) {
208
+ return false;
209
+ }
210
+
211
+ if (!spec.concrete) {
212
+ log.warning(
213
+ `» ${spec.name} ${spec.source} version is a range (${spec.version}); corepack requires a concrete pin. leaving PATH unchanged.`,
214
+ );
215
+ return false;
216
+ }
217
+
218
+ const existing = await currentVersion(spec.name);
219
+ if (existing === spec.version) {
220
+ log.info(`» ${spec.name}@${spec.version} already active`);
221
+ return true;
222
+ }
223
+
224
+ log.info(
225
+ `» corepack prepare ${spec.name}@${spec.version} --activate (shim dir: ${params.binDir})`,
226
+ );
227
+
228
+ await mkdir(params.binDir, { recursive: true });
229
+ const enable = await runCorepack(["enable", "--install-directory", params.binDir, spec.name]);
230
+ if (enable.exitCode !== 0) {
231
+ log.warning(
232
+ `» corepack enable failed (exit ${enable.exitCode}); leaving ${spec.name} from PATH. stderr: ${enable.stderr.trim() || "(empty)"}`,
233
+ );
234
+ return false;
235
+ }
236
+
237
+ // shim dir first so `pnpm` resolves to the pinned binary and shadows any
238
+ // `pnpm` a later `npm i -g pnpm` drops into the node bin dir.
239
+ process.env.PATH = `${params.binDir}${delimiter}${process.env.PATH ?? ""}`;
240
+
241
+ const prepare = await runCorepack(["prepare", `${spec.name}@${spec.version}`, "--activate"]);
242
+ if (prepare.exitCode !== 0) {
243
+ log.warning(
244
+ `» corepack prepare ${spec.name}@${spec.version} failed (exit ${prepare.exitCode}); leaving ${spec.name} from PATH. stderr: ${prepare.stderr.trim() || "(empty)"}`,
245
+ );
246
+ return false;
247
+ }
248
+
249
+ const after = await currentVersion(spec.name);
250
+ if (after !== spec.version) {
251
+ log.warning(
252
+ `» corepack activated ${spec.name}@${spec.version} but PATH still resolves to ${after ?? "(missing)"}; continuing anyway`,
253
+ );
254
+ }
255
+
256
+ return true;
257
+ }
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AgentUsage } from "#app/agents/shared";
3
+ import { aggregateUsage } from "#app/utils/patchWorkflowRunFields";
4
+
5
+ const entry = (overrides: Partial<AgentUsage>): AgentUsage => ({
6
+ agent: "terramend",
7
+ inputTokens: 0,
8
+ outputTokens: 0,
9
+ ...overrides,
10
+ });
11
+
12
+ describe("aggregateUsage", () => {
13
+ it("returns empty object for empty input", () => {
14
+ expect(aggregateUsage([])).toEqual({});
15
+ });
16
+
17
+ it("drops fields that sum to zero so NULL stays 'not reported'", () => {
18
+ // a run that only recorded input tokens shouldn't write zero into output/cache/cost —
19
+ // those columns stay NULL so dashboards can tell 'zero' from 'never reported'.
20
+ expect(aggregateUsage([entry({ inputTokens: 42 })])).toEqual({ inputTokens: 42 });
21
+ });
22
+
23
+ it("sums a single entry with all fields present", () => {
24
+ expect(
25
+ aggregateUsage([
26
+ entry({
27
+ inputTokens: 100,
28
+ outputTokens: 50,
29
+ cacheReadTokens: 1000,
30
+ cacheWriteTokens: 200,
31
+ costUsd: 0.12,
32
+ }),
33
+ ]),
34
+ ).toEqual({
35
+ inputTokens: 100,
36
+ outputTokens: 50,
37
+ cacheReadTokens: 1000,
38
+ cacheWriteTokens: 200,
39
+ costUsd: 0.12,
40
+ });
41
+ });
42
+
43
+ it("sums multiple entries across agents", () => {
44
+ expect(
45
+ aggregateUsage([
46
+ entry({
47
+ agent: "claude",
48
+ inputTokens: 100,
49
+ outputTokens: 50,
50
+ cacheReadTokens: 1000,
51
+ costUsd: 0.1,
52
+ }),
53
+ entry({
54
+ agent: "terramend",
55
+ inputTokens: 200,
56
+ outputTokens: 80,
57
+ cacheReadTokens: 2000,
58
+ cacheWriteTokens: 300,
59
+ costUsd: 0.25,
60
+ }),
61
+ ]),
62
+ ).toEqual({
63
+ inputTokens: 300,
64
+ outputTokens: 130,
65
+ cacheReadTokens: 3000,
66
+ cacheWriteTokens: 300,
67
+ // floating-point sum — specifying exact value documents expected precision
68
+ costUsd: 0.35,
69
+ });
70
+ });
71
+
72
+ it("treats undefined cache/cost as zero and drops when the sum is still zero", () => {
73
+ expect(
74
+ aggregateUsage([
75
+ entry({ inputTokens: 10, outputTokens: 5 }),
76
+ entry({ inputTokens: 20, outputTokens: 15 }),
77
+ ]),
78
+ ).toEqual({ inputTokens: 30, outputTokens: 20 });
79
+ });
80
+
81
+ it("clamps individual INT fields at INT4_MAX so partial-persist cannot happen", () => {
82
+ // server-side per-field rejection would silently drop the huge column and
83
+ // keep the small ones, producing a row with a NULL for the missing metric.
84
+ // clamping client-side guarantees the wire payload is self-consistent.
85
+ const result = aggregateUsage([
86
+ entry({ inputTokens: 3_000_000_000, outputTokens: 42, cacheReadTokens: 5 }),
87
+ ]);
88
+ expect(result.inputTokens).toBe(2_147_483_647);
89
+ expect(result.outputTokens).toBe(42);
90
+ expect(result.cacheReadTokens).toBe(5);
91
+ });
92
+ });
@@ -0,0 +1,150 @@
1
+ import type { AgentUsage } from "#app/agents/shared";
2
+ import type { ToolContext } from "#app/mcp/server";
3
+ import { apiFetch } from "#app/utils/apiFetch";
4
+ import { log } from "#app/utils/cli";
5
+ import { retry } from "#app/utils/retry";
6
+
7
+ /**
8
+ * Artifact tracking fields — one-off PATCHes from MCP tools as GitHub entities
9
+ * are created during the run. Strings only (GraphQL node IDs).
10
+ * Keep in sync with `STRING_FIELDS` in `app/api/workflow-run/[runId]/route.ts`.
11
+ */
12
+ export type WorkflowRunArtifactPatchKey =
13
+ | "prNodeId"
14
+ | "issueNodeId"
15
+ | "reviewNodeId"
16
+ | "planCommentNodeId"
17
+ | "summarySnapshot";
18
+
19
+ /**
20
+ * Usage fields — aggregated across all agent calls and PATCHed once at
21
+ * end-of-run. Token counts are Int4 on the DB side (ample for any realistic
22
+ * run); `costUsd` is a Decimal populated by provider-reported dollar amounts.
23
+ * Keep in sync with `INT_FIELDS` + `DECIMAL_FIELDS` in the server route.
24
+ */
25
+ export type WorkflowRunUsagePatchKey =
26
+ | "inputTokens"
27
+ | "outputTokens"
28
+ | "cacheReadTokens"
29
+ | "cacheWriteTokens"
30
+ | "costUsd";
31
+
32
+ export type WorkflowRunPatch = Partial<Record<WorkflowRunArtifactPatchKey, string>> &
33
+ Partial<Record<WorkflowRunUsagePatchKey, number>>;
34
+
35
+ const STRING_KEYS: WorkflowRunArtifactPatchKey[] = [
36
+ "prNodeId",
37
+ "issueNodeId",
38
+ "reviewNodeId",
39
+ "planCommentNodeId",
40
+ "summarySnapshot",
41
+ ];
42
+
43
+ const NUMBER_KEYS: WorkflowRunUsagePatchKey[] = [
44
+ "inputTokens",
45
+ "outputTokens",
46
+ "cacheReadTokens",
47
+ "cacheWriteTokens",
48
+ "costUsd",
49
+ ];
50
+
51
+ /** PATCH workflow-run fields (Terramend JWT, not GitHub). */
52
+ export async function patchWorkflowRunFields(
53
+ ctx: ToolContext,
54
+ fields: WorkflowRunPatch,
55
+ ): Promise<void> {
56
+ if (ctx.runId === undefined || !ctx.apiToken) return;
57
+ const body: Record<string, string | number> = {};
58
+ for (const key of STRING_KEYS) {
59
+ const value = fields[key];
60
+ if (typeof value === "string" && value.length > 0) {
61
+ body[key] = value;
62
+ }
63
+ }
64
+ for (const key of NUMBER_KEYS) {
65
+ const value = fields[key];
66
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
67
+ body[key] = value;
68
+ }
69
+ }
70
+ if (Object.keys(body).length === 0) return;
71
+ try {
72
+ await retry(
73
+ async () => {
74
+ const response = await apiFetch({
75
+ path: `/api/workflow-run/${ctx.runId}`,
76
+ method: "PATCH",
77
+ headers: {
78
+ authorization: `Bearer ${ctx.apiToken}`,
79
+ "content-type": "application/json",
80
+ },
81
+ body: JSON.stringify(body),
82
+ signal: AbortSignal.timeout(10_000),
83
+ });
84
+ if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
85
+ },
86
+ {
87
+ maxAttempts: 3,
88
+ delayMs: 2000,
89
+ label: "patchWorkflowRunFields",
90
+ },
91
+ );
92
+ } catch (error) {
93
+ log.warning(`patchWorkflowRunFields exhausted retries: ${error}`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Postgres INTEGER / Prisma Int4 is signed 32-bit. Aggregated usage won't
99
+ * realistically hit this in a single run (2.1B tokens ≈ $6000+ of input on
100
+ * Claude Opus), but clamping here keeps the wire payload self-consistent:
101
+ * the server rejects out-of-range INT fields individually, so without a
102
+ * client-side clamp a single overflow would write a partial row where
103
+ * some columns land and others silently don't.
104
+ */
105
+ const INT4_MAX = 2_147_483_647;
106
+
107
+ function clampInt(value: number, field: WorkflowRunUsagePatchKey): number {
108
+ if (value > INT4_MAX) {
109
+ log.warning(
110
+ `aggregateUsage: ${field}=${value} exceeds INT4_MAX (${INT4_MAX}) — clamping so the rest of the usage row still persists.`,
111
+ );
112
+ return INT4_MAX;
113
+ }
114
+ return value;
115
+ }
116
+
117
+ /**
118
+ * Sum per-agent usage entries into a single WorkflowRunPatch payload.
119
+ * Returns an empty object when there's nothing to report, which causes
120
+ * `patchWorkflowRunFields` to no-op — safe to call unconditionally from
121
+ * end-of-run paths. Zero-valued fields are dropped so the DB only stores
122
+ * positive sums (and NULL means "not reported").
123
+ *
124
+ * Token sums are clamped to INT4_MAX to guarantee the payload the server
125
+ * sees is always self-consistent across all numeric columns.
126
+ */
127
+ export function aggregateUsage(entries: AgentUsage[]): WorkflowRunPatch {
128
+ if (entries.length === 0) return {};
129
+
130
+ const sum = entries.reduce(
131
+ (acc, e) => ({
132
+ inputTokens: acc.inputTokens + e.inputTokens,
133
+ outputTokens: acc.outputTokens + e.outputTokens,
134
+ cacheReadTokens: acc.cacheReadTokens + (e.cacheReadTokens ?? 0),
135
+ cacheWriteTokens: acc.cacheWriteTokens + (e.cacheWriteTokens ?? 0),
136
+ costUsd: acc.costUsd + (e.costUsd ?? 0),
137
+ }),
138
+ { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: 0 },
139
+ );
140
+
141
+ const out: WorkflowRunPatch = {};
142
+ if (sum.inputTokens > 0) out.inputTokens = clampInt(sum.inputTokens, "inputTokens");
143
+ if (sum.outputTokens > 0) out.outputTokens = clampInt(sum.outputTokens, "outputTokens");
144
+ if (sum.cacheReadTokens > 0)
145
+ out.cacheReadTokens = clampInt(sum.cacheReadTokens, "cacheReadTokens");
146
+ if (sum.cacheWriteTokens > 0)
147
+ out.cacheWriteTokens = clampInt(sum.cacheWriteTokens, "cacheWriteTokens");
148
+ if (sum.costUsd > 0) out.costUsd = sum.costUsd;
149
+ return out;
150
+ }