@akira-tl/forgerelay 0.1.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 (373) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +22 -0
  3. package/NOTICE.md +19 -0
  4. package/README.md +270 -0
  5. package/dist/apply-patch.js +402 -0
  6. package/dist/artifact-error.js +8 -0
  7. package/dist/artifact-tools.js +377 -0
  8. package/dist/cli.js +597 -0
  9. package/dist/config.js +224 -0
  10. package/dist/db/client.js +29 -0
  11. package/dist/db/migrations.js +189 -0
  12. package/dist/db/schema.js +85 -0
  13. package/dist/git-worktrees.js +255 -0
  14. package/dist/git.js +41 -0
  15. package/dist/incoming-artifacts.js +378 -0
  16. package/dist/local-agent-adapters.js +653 -0
  17. package/dist/local-agent-availability.js +119 -0
  18. package/dist/local-agent-path.js +25 -0
  19. package/dist/local-agent-profiles.js +136 -0
  20. package/dist/local-agent-runtime.js +48 -0
  21. package/dist/local-agent-store.js +145 -0
  22. package/dist/local-agent-targets.js +80 -0
  23. package/dist/logger.js +69 -0
  24. package/dist/mcp/server-instructions.js +78 -0
  25. package/dist/mcp-sessions.js +56 -0
  26. package/dist/oauth-provider.js +237 -0
  27. package/dist/oauth-store.js +138 -0
  28. package/dist/pi-tools.js +81 -0
  29. package/dist/process-platform.js +49 -0
  30. package/dist/process-sessions.js +337 -0
  31. package/dist/request-meta.js +9 -0
  32. package/dist/review-checkpoints.js +219 -0
  33. package/dist/roots.js +37 -0
  34. package/dist/server-shutdown.js +12 -0
  35. package/dist/server.js +1596 -0
  36. package/dist/skills.js +89 -0
  37. package/dist/ui/.vite/manifest.json +2521 -0
  38. package/dist/ui/assets/abap-CLvhMVsD.js +1 -0
  39. package/dist/ui/assets/actionscript-3--17pq3dv.js +1 -0
  40. package/dist/ui/assets/ada-C5qYipkI.js +1 -0
  41. package/dist/ui/assets/andromeeda-vGVdxbeo.js +1 -0
  42. package/dist/ui/assets/angular-html-CovppXt5.js +1 -0
  43. package/dist/ui/assets/angular-ts-0Y9l6YXu.js +1 -0
  44. package/dist/ui/assets/apache-U0d_L8uA.js +1 -0
  45. package/dist/ui/assets/apex-CGTLDQj6.js +1 -0
  46. package/dist/ui/assets/apl-BmBhiklA.js +1 -0
  47. package/dist/ui/assets/applescript-CCn79oCD.js +1 -0
  48. package/dist/ui/assets/ara-4CJ0cIlV.js +1 -0
  49. package/dist/ui/assets/asciidoc-DE70LPWp.js +1 -0
  50. package/dist/ui/assets/asm-Cmm7eHzH.js +1 -0
  51. package/dist/ui/assets/astro-BAmqcqyV.js +1 -0
  52. package/dist/ui/assets/aurora-x-CDeNXAV0.js +1 -0
  53. package/dist/ui/assets/awk-BWXHIvNe.js +1 -0
  54. package/dist/ui/assets/ayu-dark-DluEY0Gj.js +1 -0
  55. package/dist/ui/assets/ayu-light-C3h-C4tm.js +1 -0
  56. package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  57. package/dist/ui/assets/ballerina-B7ZEbQpA.js +1 -0
  58. package/dist/ui/assets/bat-Bo4NYOV-.js +1 -0
  59. package/dist/ui/assets/beancount-D-usSTwE.js +1 -0
  60. package/dist/ui/assets/berry-DKpUyyne.js +1 -0
  61. package/dist/ui/assets/bibtex-Ci_nEsc7.js +1 -0
  62. package/dist/ui/assets/bicep-CUHmPFLl.js +1 -0
  63. package/dist/ui/assets/bird2-C2hNVINV.js +1 -0
  64. package/dist/ui/assets/blade-BjFcmRJo.js +1 -0
  65. package/dist/ui/assets/bsl-BkkzgIyY.js +1 -0
  66. package/dist/ui/assets/c-CaOJn4x0.js +1 -0
  67. package/dist/ui/assets/c3-CnJL0r0V.js +1 -0
  68. package/dist/ui/assets/cadence-CQ2zXKGN.js +1 -0
  69. package/dist/ui/assets/cairo-DLTphjLi.js +1 -0
  70. package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +1 -0
  71. package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +1 -0
  72. package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +1 -0
  73. package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +1 -0
  74. package/dist/ui/assets/clarity-SemFz856.js +1 -0
  75. package/dist/ui/assets/clojure-DqKBuwfJ.js +1 -0
  76. package/dist/ui/assets/cmake-Bj61d0ZC.js +1 -0
  77. package/dist/ui/assets/cobol-DR4zWFMt.js +1 -0
  78. package/dist/ui/assets/codeowners-C8r90Shi.js +1 -0
  79. package/dist/ui/assets/codeql-oeQT6MSM.js +1 -0
  80. package/dist/ui/assets/coffee-ReGSThmM.js +1 -0
  81. package/dist/ui/assets/common-lisp-Cv5bFMCO.js +1 -0
  82. package/dist/ui/assets/coq-BrsZFFmf.js +1 -0
  83. package/dist/ui/assets/cpp-CnlQh6MO.js +1 -0
  84. package/dist/ui/assets/crystal-BrpPbH1U.js +1 -0
  85. package/dist/ui/assets/csharp-Ct8U2NOr.js +1 -0
  86. package/dist/ui/assets/css-DRBEvyPU.js +1 -0
  87. package/dist/ui/assets/csv-Dx-8-gkx.js +1 -0
  88. package/dist/ui/assets/cue-CE9AQfxI.js +1 -0
  89. package/dist/ui/assets/cypher-ClKdZ_lG.js +1 -0
  90. package/dist/ui/assets/d-qD-0Kul2.js +1 -0
  91. package/dist/ui/assets/dark-plus-Cs2F2srj.js +1 -0
  92. package/dist/ui/assets/dart-DkHntEIa.js +1 -0
  93. package/dist/ui/assets/dax-BkyTk9wS.js +1 -0
  94. package/dist/ui/assets/desktop-Dlh5hvp9.js +1 -0
  95. package/dist/ui/assets/diff-woXpYk--.js +1 -0
  96. package/dist/ui/assets/docker-IyjqRm3v.js +1 -0
  97. package/dist/ui/assets/dotenv-_5a1GRtc.js +1 -0
  98. package/dist/ui/assets/dracula-BHWKrbxM.js +1 -0
  99. package/dist/ui/assets/dracula-soft-5eyTD99u.js +1 -0
  100. package/dist/ui/assets/dream-maker-DW3nJb8Q.js +1 -0
  101. package/dist/ui/assets/edge-cfwpdvl9.js +1 -0
  102. package/dist/ui/assets/elixir-7owrJxb6.js +1 -0
  103. package/dist/ui/assets/elm-udiOjODk.js +1 -0
  104. package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +1 -0
  105. package/dist/ui/assets/erb-8ctXHMkS.js +1 -0
  106. package/dist/ui/assets/erlang-Cphh6RMH.js +1 -0
  107. package/dist/ui/assets/everforest-dark-sB-x3p7T.js +1 -0
  108. package/dist/ui/assets/everforest-light-Df2xbC6M.js +1 -0
  109. package/dist/ui/assets/fennel-DQxkIbk2.js +1 -0
  110. package/dist/ui/assets/fish-BJitypiv.js +1 -0
  111. package/dist/ui/assets/fluent-C03EYrpw.js +1 -0
  112. package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  113. package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +1 -0
  114. package/dist/ui/assets/fsharp-D13ZGOAj.js +1 -0
  115. package/dist/ui/assets/gdresource-C0sCabJj.js +1 -0
  116. package/dist/ui/assets/gdscript-Cp2uCuqX.js +1 -0
  117. package/dist/ui/assets/gdshader-CBce3t8t.js +1 -0
  118. package/dist/ui/assets/genie-CV2tkWYe.js +1 -0
  119. package/dist/ui/assets/gherkin-DExj1W_8.js +1 -0
  120. package/dist/ui/assets/git-commit-BSykSTBG.js +1 -0
  121. package/dist/ui/assets/git-rebase-P0V_qK1w.js +1 -0
  122. package/dist/ui/assets/github-dark-C-LZuMrd.js +1 -0
  123. package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
  124. package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  125. package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  126. package/dist/ui/assets/github-light-EUqPIrTm.js +1 -0
  127. package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
  128. package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  129. package/dist/ui/assets/gleam-CSRkHgEL.js +1 -0
  130. package/dist/ui/assets/glimmer-js-CZ14ED7U.js +1 -0
  131. package/dist/ui/assets/glimmer-ts-DtBSFP-o.js +1 -0
  132. package/dist/ui/assets/glsl-BX2BkZJK.js +1 -0
  133. package/dist/ui/assets/gn-ilITqXS6.js +1 -0
  134. package/dist/ui/assets/gnuplot-7GGW24-e.js +1 -0
  135. package/dist/ui/assets/go-BJwz_mda.js +1 -0
  136. package/dist/ui/assets/graphql-Dj_cPv4w.js +1 -0
  137. package/dist/ui/assets/groovy-CacY0gHj.js +1 -0
  138. package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  139. package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  140. package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  141. package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  142. package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  143. package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  144. package/dist/ui/assets/hack-DGRkd8xn.js +1 -0
  145. package/dist/ui/assets/haml-BAPSvujW.js +1 -0
  146. package/dist/ui/assets/handlebars-PYksdULG.js +1 -0
  147. package/dist/ui/assets/haskell-D8IpX4py.js +1 -0
  148. package/dist/ui/assets/haxe-OTjmBuCE.js +1 -0
  149. package/dist/ui/assets/hcl-Dh228itO.js +1 -0
  150. package/dist/ui/assets/heavy-payload-BjE0LSUi.js +4 -0
  151. package/dist/ui/assets/hjson-CxZEssPk.js +1 -0
  152. package/dist/ui/assets/hlsl-Cvrh5tZx.js +1 -0
  153. package/dist/ui/assets/horizon-CE9ld1lL.js +1 -0
  154. package/dist/ui/assets/horizon-bright-Br1oVSNq.js +1 -0
  155. package/dist/ui/assets/houston-CsvMBhTu.js +1 -0
  156. package/dist/ui/assets/html-DloAPGZ1.js +1 -0
  157. package/dist/ui/assets/html-derivative-C6q86ZrS.js +1 -0
  158. package/dist/ui/assets/http-fPvYDvHA.js +1 -0
  159. package/dist/ui/assets/hurl-Dbir4WvE.js +1 -0
  160. package/dist/ui/assets/hxml-B0Qn7Nwc.js +1 -0
  161. package/dist/ui/assets/hy-CZbG8q4J.js +1 -0
  162. package/dist/ui/assets/imba-DsUTQ-LC.js +1 -0
  163. package/dist/ui/assets/ini-B5eOa1yu.js +1 -0
  164. package/dist/ui/assets/java-DvkqXg3z.js +1 -0
  165. package/dist/ui/assets/javascript-TONTV6Ty.js +1 -0
  166. package/dist/ui/assets/jinja-OM1F5Gct.js +1 -0
  167. package/dist/ui/assets/jison-CBttugts.js +1 -0
  168. package/dist/ui/assets/json-Db5oY-7J.js +1 -0
  169. package/dist/ui/assets/json5-BR5RXkoi.js +1 -0
  170. package/dist/ui/assets/jsonc-CYpm1nAK.js +1 -0
  171. package/dist/ui/assets/jsonl-CmCQp5Yx.js +1 -0
  172. package/dist/ui/assets/jsonnet-CJTPZ8u_.js +1 -0
  173. package/dist/ui/assets/jssm-DXw9l8Rf.js +1 -0
  174. package/dist/ui/assets/jsx-DKeJCgsc.js +1 -0
  175. package/dist/ui/assets/julia-NlIPhXaL.js +1 -0
  176. package/dist/ui/assets/just-BXBJB-mX.js +1 -0
  177. package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +1 -0
  178. package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +1 -0
  179. package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +1 -0
  180. package/dist/ui/assets/kdl-CsD5j6eV.js +1 -0
  181. package/dist/ui/assets/kotlin-DhhofPvG.js +1 -0
  182. package/dist/ui/assets/kusto-BUv0MjJC.js +1 -0
  183. package/dist/ui/assets/laserwave-C_8bwKvT.js +1 -0
  184. package/dist/ui/assets/latex-CGK-6yDy.js +1 -0
  185. package/dist/ui/assets/lean-CewbzKMR.js +1 -0
  186. package/dist/ui/assets/less-DVTAwKKz.js +1 -0
  187. package/dist/ui/assets/light-plus-DVQuIRkW.js +1 -0
  188. package/dist/ui/assets/liquid-D7waQ0MY.js +1 -0
  189. package/dist/ui/assets/llvm-Cm23YOpf.js +1 -0
  190. package/dist/ui/assets/log-BNLmms1o.js +1 -0
  191. package/dist/ui/assets/logo-Cluzi2Zq.js +1 -0
  192. package/dist/ui/assets/lua-C_kkt4h1.js +1 -0
  193. package/dist/ui/assets/luau-CNKltnaQ.js +1 -0
  194. package/dist/ui/assets/make-Dixweg8N.js +1 -0
  195. package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
  196. package/dist/ui/assets/marko-BokXuCts.js +1 -0
  197. package/dist/ui/assets/material-theme-Bm3Qr25_.js +1 -0
  198. package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +1 -0
  199. package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +1 -0
  200. package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  201. package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  202. package/dist/ui/assets/matlab-D7qyCx1q.js +1 -0
  203. package/dist/ui/assets/mdc-Bk0nnUJA.js +1 -0
  204. package/dist/ui/assets/mdx-DQZ5AkYe.js +1 -0
  205. package/dist/ui/assets/mermaid-Bk4SNUv9.js +1 -0
  206. package/dist/ui/assets/min-dark-BSWPekZh.js +1 -0
  207. package/dist/ui/assets/min-light-DDpmG2fV.js +1 -0
  208. package/dist/ui/assets/mipsasm-BMqwQI7S.js +1 -0
  209. package/dist/ui/assets/mojo-BgCJLMeH.js +1 -0
  210. package/dist/ui/assets/monokai-CdkpiU2Y.js +1 -0
  211. package/dist/ui/assets/moonbit-CaWjb8XO.js +1 -0
  212. package/dist/ui/assets/move-B1IS1UjX.js +1 -0
  213. package/dist/ui/assets/narrat-_X_XdTYD.js +1 -0
  214. package/dist/ui/assets/nextflow-Bbiyy34d.js +1 -0
  215. package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +1 -0
  216. package/dist/ui/assets/nginx-jM6NF9TV.js +1 -0
  217. package/dist/ui/assets/night-owl-DhmEMT88.js +1 -0
  218. package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +1 -0
  219. package/dist/ui/assets/nim-hYsL-WKb.js +1 -0
  220. package/dist/ui/assets/nix-IvuFDN5E.js +1 -0
  221. package/dist/ui/assets/nord-Cb4Vim4T.js +1 -0
  222. package/dist/ui/assets/nushell-DcLAeLz5.js +1 -0
  223. package/dist/ui/assets/objective-c-D1A_Heim.js +1 -0
  224. package/dist/ui/assets/objective-cpp-BsSzOQcm.js +1 -0
  225. package/dist/ui/assets/ocaml-O90oeIOV.js +1 -0
  226. package/dist/ui/assets/odin-B1RWQWA5.js +1 -0
  227. package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +1 -0
  228. package/dist/ui/assets/one-light-D7Lr4KcI.js +1 -0
  229. package/dist/ui/assets/openscad-BUDT5pXO.js +1 -0
  230. package/dist/ui/assets/pascal-4ZHwLPI5.js +1 -0
  231. package/dist/ui/assets/perl-DBI_cUHJ.js +1 -0
  232. package/dist/ui/assets/php-BX_aQhFl.js +1 -0
  233. package/dist/ui/assets/pierre-dark-sU4Zdns8.js +1 -0
  234. package/dist/ui/assets/pierre-dark-soft-HXgun5vs.js +1 -0
  235. package/dist/ui/assets/pierre-light-DWBk51d8.js +1 -0
  236. package/dist/ui/assets/pierre-light-soft-CodEzPxf.js +1 -0
  237. package/dist/ui/assets/pkl-ot-7Btpt.js +1 -0
  238. package/dist/ui/assets/plastic-DQwYfKfQ.js +1 -0
  239. package/dist/ui/assets/plsql-DGHpHOYJ.js +1 -0
  240. package/dist/ui/assets/po-BiJDBrnU.js +1 -0
  241. package/dist/ui/assets/poimandres-DRFjx7u4.js +1 -0
  242. package/dist/ui/assets/polar-C7UOKdEL.js +1 -0
  243. package/dist/ui/assets/postcss-BXeXVLqQ.js +1 -0
  244. package/dist/ui/assets/powerquery-DNMTfnFr.js +1 -0
  245. package/dist/ui/assets/powershell-DshXNtvi.js +1 -0
  246. package/dist/ui/assets/prisma-BsRQq5mF.js +1 -0
  247. package/dist/ui/assets/prolog-iXnhIJG7.js +1 -0
  248. package/dist/ui/assets/proto-DB4EqR-F.js +1 -0
  249. package/dist/ui/assets/pug-DwFAYy1e.js +1 -0
  250. package/dist/ui/assets/puppet-CDv2pdJW.js +1 -0
  251. package/dist/ui/assets/purescript-9MfHhQsQ.js +1 -0
  252. package/dist/ui/assets/python-gzcpVVnB.js +1 -0
  253. package/dist/ui/assets/qml-eLSwQUKt.js +1 -0
  254. package/dist/ui/assets/qmldir-DCQb3MpD.js +1 -0
  255. package/dist/ui/assets/qss-Fe1Jh2GI.js +1 -0
  256. package/dist/ui/assets/r-BMnQsMLf.js +1 -0
  257. package/dist/ui/assets/racket-DcIDlBhZ.js +1 -0
  258. package/dist/ui/assets/raku-B3gFvitq.js +1 -0
  259. package/dist/ui/assets/razor-CgciiMyw.js +1 -0
  260. package/dist/ui/assets/red-CJ3rzSJv.js +1 -0
  261. package/dist/ui/assets/reg-CRGYupPL.js +1 -0
  262. package/dist/ui/assets/regexp-ZrURNrn5.js +1 -0
  263. package/dist/ui/assets/rel-BtDbiS_P.js +1 -0
  264. package/dist/ui/assets/review-payload-HGkpJjjm.js +1 -0
  265. package/dist/ui/assets/riscv-Ckw8ddFX.js +1 -0
  266. package/dist/ui/assets/ron-VUp2lXgN.js +1 -0
  267. package/dist/ui/assets/rose-pine-BthvhNj6.js +1 -0
  268. package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +1 -0
  269. package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +1 -0
  270. package/dist/ui/assets/rosmsg-CAekHB0j.js +1 -0
  271. package/dist/ui/assets/rst-EoAOr3M6.js +1 -0
  272. package/dist/ui/assets/ruby-Dqx5_Rqw.js +1 -0
  273. package/dist/ui/assets/rust-Cfkwpbl8.js +1 -0
  274. package/dist/ui/assets/sas-Bf6xPLSX.js +1 -0
  275. package/dist/ui/assets/sass-DXrisJhu.js +1 -0
  276. package/dist/ui/assets/scala-DKOlJaKm.js +1 -0
  277. package/dist/ui/assets/scheme-DQCgrYNe.js +1 -0
  278. package/dist/ui/assets/scrollbar-DX4Ujotb.js +287 -0
  279. package/dist/ui/assets/scss-DDpW3yTd.js +1 -0
  280. package/dist/ui/assets/sdbl-bTVj8UrX.js +1 -0
  281. package/dist/ui/assets/shaderlab-TOUzSsQk.js +1 -0
  282. package/dist/ui/assets/shellscript-DayAQ-W9.js +1 -0
  283. package/dist/ui/assets/shellsession-jPr_lSnA.js +1 -0
  284. package/dist/ui/assets/slack-dark-DnToyrRv.js +1 -0
  285. package/dist/ui/assets/slack-ochin-B2OO5cIa.js +1 -0
  286. package/dist/ui/assets/smalltalk-B16xEiuN.js +1 -0
  287. package/dist/ui/assets/snazzy-light-4G7pJPwS.js +1 -0
  288. package/dist/ui/assets/solarized-dark-DV17i1UV.js +1 -0
  289. package/dist/ui/assets/solarized-light-DSh2HLQt.js +1 -0
  290. package/dist/ui/assets/solidity-CKzVLygQ.js +1 -0
  291. package/dist/ui/assets/soy-X9adks8G.js +1 -0
  292. package/dist/ui/assets/sparql-D_iOobhT.js +1 -0
  293. package/dist/ui/assets/splunk-BC2Px7Mm.js +1 -0
  294. package/dist/ui/assets/sql-Bmt09AIR.js +1 -0
  295. package/dist/ui/assets/ssh-config-BgfXC-Er.js +1 -0
  296. package/dist/ui/assets/stata-DVkNpH8r.js +1 -0
  297. package/dist/ui/assets/stylus-B6D30XZt.js +1 -0
  298. package/dist/ui/assets/surrealql-eEXzMb8j.js +1 -0
  299. package/dist/ui/assets/svelte-CFoBxZF3.js +1 -0
  300. package/dist/ui/assets/swift-DonLKvLd.js +1 -0
  301. package/dist/ui/assets/synthwave-84-nFMaYfgc.js +1 -0
  302. package/dist/ui/assets/system-verilog-DJ5XKQeo.js +1 -0
  303. package/dist/ui/assets/systemd-BxMlprV5.js +1 -0
  304. package/dist/ui/assets/talonscript-CohzipZa.js +1 -0
  305. package/dist/ui/assets/tasl-DMoTqEGO.js +1 -0
  306. package/dist/ui/assets/tcl-CZd0xW_V.js +1 -0
  307. package/dist/ui/assets/templ-Da_324JF.js +1 -0
  308. package/dist/ui/assets/terraform-DswuEJGm.js +1 -0
  309. package/dist/ui/assets/tex-HAzlmKJ0.js +1 -0
  310. package/dist/ui/assets/tokyo-night-oM2G3aXe.js +1 -0
  311. package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
  312. package/dist/ui/assets/ts-tags-7iLrNH8p.js +1 -0
  313. package/dist/ui/assets/tsv-sltzmVWM.js +1 -0
  314. package/dist/ui/assets/tsx-gZaXf8PJ.js +1 -0
  315. package/dist/ui/assets/turtle-ByJddavk.js +1 -0
  316. package/dist/ui/assets/twig-CLpQ7p1N.js +1 -0
  317. package/dist/ui/assets/typescript-B2WWCRI2.js +1 -0
  318. package/dist/ui/assets/typespec-B88KGewJ.js +1 -0
  319. package/dist/ui/assets/typst-DI99ib-x.js +1 -0
  320. package/dist/ui/assets/v-DETTlOr0.js +1 -0
  321. package/dist/ui/assets/vala-zf12oZj6.js +1 -0
  322. package/dist/ui/assets/vb-Djn5o6TS.js +1 -0
  323. package/dist/ui/assets/verilog-CiiDBU1e.js +1 -0
  324. package/dist/ui/assets/vesper-D5bVUKB1.js +1 -0
  325. package/dist/ui/assets/vhdl-BroJfC0k.js +1 -0
  326. package/dist/ui/assets/viml-DvXPmvsu.js +1 -0
  327. package/dist/ui/assets/vitesse-black-fwtXNY1n.js +1 -0
  328. package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +1 -0
  329. package/dist/ui/assets/vitesse-light-VbXTXTou.js +1 -0
  330. package/dist/ui/assets/vue-No_8Jygd.js +1 -0
  331. package/dist/ui/assets/vue-html-kHJg98Kk.js +1 -0
  332. package/dist/ui/assets/vue-vine-Df4cEYaN.js +1 -0
  333. package/dist/ui/assets/vyper-CgoNMtux.js +1 -0
  334. package/dist/ui/assets/wasm-BnjxR4X6.js +1 -0
  335. package/dist/ui/assets/wasm-ByWQv1Qj.js +1 -0
  336. package/dist/ui/assets/wenyan-C8pVoKbM.js +1 -0
  337. package/dist/ui/assets/wgsl-BsKzXJz4.js +1 -0
  338. package/dist/ui/assets/wikitext-ClFFjSW2.js +1 -0
  339. package/dist/ui/assets/wit-DdvCle-K.js +1 -0
  340. package/dist/ui/assets/wolfram-DLL8P-h_.js +1 -0
  341. package/dist/ui/assets/workspace-app-C_wsC6VL.js +117 -0
  342. package/dist/ui/assets/workspace-app-DHvE2ojW.css +1 -0
  343. package/dist/ui/assets/xml-DUZ05dpG.js +1 -0
  344. package/dist/ui/assets/xsl-Bwp3w2OF.js +1 -0
  345. package/dist/ui/assets/yaml-IA_-TEM1.js +1 -0
  346. package/dist/ui/assets/zenscript-BnlCZFoB.js +1 -0
  347. package/dist/ui/assets/zig-CMLA9XwU.js +1 -0
  348. package/dist/ui/workspace-app.html +15 -0
  349. package/dist/user-config.js +97 -0
  350. package/dist/workspace-store.js +162 -0
  351. package/dist/workspaces.js +528 -0
  352. package/docs/agent-profile-schema.md +119 -0
  353. package/docs/artifact-exchange.md +63 -0
  354. package/docs/chatgpt-coding-workflow.md +183 -0
  355. package/docs/configuration.md +197 -0
  356. package/docs/gotchas.md +243 -0
  357. package/docs/roadmap.md +195 -0
  358. package/docs/security.md +142 -0
  359. package/docs/setup.md +132 -0
  360. package/docs/versioning.md +189 -0
  361. package/examples/agents/claude-implementer.md +30 -0
  362. package/examples/agents/codex-explorer.md +27 -0
  363. package/examples/agents/codex-qa-tester.md +29 -0
  364. package/examples/agents/codex-worker.md +25 -0
  365. package/examples/agents/copilot-reviewer.md +25 -0
  366. package/examples/agents/cursor-agent-worker.md +26 -0
  367. package/examples/agents/opencode-explorer.md +27 -0
  368. package/examples/agents/pi-reviewer.md +27 -0
  369. package/package.json +102 -0
  370. package/scripts/dev-server.mjs +120 -0
  371. package/scripts/fix-node-pty-permissions.mjs +22 -0
  372. package/scripts/release-version.mjs +207 -0
  373. package/skills/subagent-delegation/SKILL.md +132 -0
package/dist/server.js ADDED
@@ -0,0 +1,1596 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { access, realpath } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
7
+ import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
8
+ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
11
+ import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
12
+ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
13
+ import express from "express";
14
+ import * as z from "zod/v4";
15
+ import { applyPatch } from "./apply-patch.js";
16
+ import { isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
17
+ import { loadConfig } from "./config.js";
18
+ import { buildServerInstructions, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
19
+ import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
20
+ import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, } from "./logger.js";
21
+ import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, runShellTool, writeFileTool, } from "./pi-tools.js";
22
+ import { SingleUserOAuthProvider } from "./oauth-provider.js";
23
+ import { McpSessionRegistry, } from "./mcp-sessions.js";
24
+ import { ProcessSessionManager } from "./process-sessions.js";
25
+ import { createReviewCheckpointManager } from "./review-checkpoints.js";
26
+ import { openAiConversationScopeId } from "./request-meta.js";
27
+ import { shutdownHttpServer } from "./server-shutdown.js";
28
+ import { formatPathForPrompt } from "./skills.js";
29
+ import { createWorkspaceStore } from "./workspace-store.js";
30
+ import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
31
+ import { summarizeLocalAgentProfile } from "./local-agent-profiles.js";
32
+ import { formatLocalAgentProviderAvailabilitySummary, getLocalAgentProviderAvailabilitySnapshot, } from "./local-agent-availability.js";
33
+ // MCP clients can reconnect without closing the previous transport. Bound stale
34
+ // session retention so abandoned MCP servers do not accumulate for the life of the process.
35
+ const MCP_SESSION_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
36
+ const MCP_SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1_000;
37
+ const WORKSPACE_APP_URI = "ui://forgerelay/workspace-app.html";
38
+ const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html";
39
+ const WRITE_TOOL_ANNOTATIONS = {
40
+ readOnlyHint: false,
41
+ destructiveHint: true,
42
+ idempotentHint: false,
43
+ openWorldHint: false,
44
+ };
45
+ const EDIT_TOOL_ANNOTATIONS = {
46
+ readOnlyHint: false,
47
+ destructiveHint: true,
48
+ idempotentHint: false,
49
+ openWorldHint: false,
50
+ };
51
+ const SHELL_TOOL_ANNOTATIONS = {
52
+ readOnlyHint: false,
53
+ destructiveHint: true,
54
+ idempotentHint: false,
55
+ openWorldHint: true,
56
+ };
57
+ function shouldAttachWidget(mode, kind) {
58
+ switch (mode) {
59
+ case "off":
60
+ return false;
61
+ case "changes":
62
+ return kind === "workspace" || kind === "show_changes";
63
+ case "full":
64
+ return true;
65
+ }
66
+ }
67
+ function toolWidgetDescriptorMeta(config, kind) {
68
+ if (!shouldAttachWidget(config.widgets, kind))
69
+ return { _meta: {} };
70
+ return {
71
+ _meta: {
72
+ ui: {
73
+ resourceUri: WORKSPACE_APP_URI,
74
+ visibility: ["model"],
75
+ },
76
+ },
77
+ };
78
+ }
79
+ function formatVisibleAgent(agent) {
80
+ const model = agent.model ? `, model ${agent.model}` : "";
81
+ const thinking = agent.thinking ? `, thinking ${agent.thinking}` : "";
82
+ const availability = agent.providerAvailable === false
83
+ ? `, unavailable: ${agent.providerUnavailableReason ?? "provider unavailable"}`
84
+ : "";
85
+ return `${agent.name} (${agent.provider}${model}${thinking}${availability})`;
86
+ }
87
+ function formatUnavailableAgentProvider(provider) {
88
+ return `${provider.name} (${provider.reason ?? "unavailable"})`;
89
+ }
90
+ function resultOutputSchema(extra = {}) {
91
+ return {
92
+ result: z
93
+ .string()
94
+ .describe("Model-readable result text for follow-up reasoning and plain MCP hosts."),
95
+ ...extra,
96
+ };
97
+ }
98
+ const workspaceSkillOutputSchema = z.object({
99
+ name: z.string(),
100
+ description: z.string(),
101
+ path: z.string(),
102
+ });
103
+ const workspaceAgentsFileOutputSchema = z.object({
104
+ path: z.string(),
105
+ content: z.string(),
106
+ });
107
+ const workspaceLocalAgentOutputSchema = z.object({
108
+ name: z.string(),
109
+ description: z.string(),
110
+ provider: z.string(),
111
+ model: z.string().optional(),
112
+ thinking: z.string().optional(),
113
+ providerAvailable: z.boolean().optional(),
114
+ providerUnavailableReason: z.string().optional(),
115
+ });
116
+ const workspaceLocalAgentProviderOutputSchema = z.object({
117
+ name: z.string(),
118
+ available: z.boolean(),
119
+ reason: z.string().optional(),
120
+ });
121
+ const workspaceAvailableAgentsFileOutputSchema = z.object({
122
+ path: z.string(),
123
+ });
124
+ const reviewFileOutputSchema = z.object({
125
+ path: z.string(),
126
+ previousPath: z.string().optional(),
127
+ type: z.enum(["change", "rename-pure", "rename-changed", "new", "deleted"]),
128
+ additions: z.number(),
129
+ removals: z.number(),
130
+ });
131
+ const reviewSummaryOutputSchema = z.object({
132
+ files: z.number(),
133
+ additions: z.number(),
134
+ removals: z.number(),
135
+ });
136
+ function sendJsonRpcError(res, status, code, message) {
137
+ res.status(status).json({
138
+ jsonrpc: "2.0",
139
+ error: { code, message },
140
+ id: null,
141
+ });
142
+ }
143
+ function requestLogFields(req, config) {
144
+ return {
145
+ ip: requestIp(req, config.logging.trustProxy),
146
+ host: req.header("host"),
147
+ userAgent: req.header("user-agent"),
148
+ origin: req.header("origin"),
149
+ referer: req.header("referer"),
150
+ contentLength: req.header("content-length"),
151
+ };
152
+ }
153
+ function logToolCall(config, fields) {
154
+ if (!config.logging.toolCalls)
155
+ return;
156
+ const { command, ...safeFields } = fields;
157
+ logEvent(config.logging, fields.success ? "info" : "warn", "tool_call", {
158
+ ...safeFields,
159
+ commandPreview: config.logging.shellCommands && command ? commandPreview(command) : undefined,
160
+ });
161
+ }
162
+ function contentText(content) {
163
+ return content
164
+ .filter((item) => item.type === "text")
165
+ .map((item) => item.text)
166
+ .join("\n");
167
+ }
168
+ function toolErrorPreview(content) {
169
+ const text = contentText(content).replace(/\s+/g, " ").trim();
170
+ if (!text)
171
+ return undefined;
172
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
173
+ }
174
+ function logFailedToolResponse(config, fields, content, startedAt) {
175
+ logToolCall(config, {
176
+ ...fields,
177
+ success: false,
178
+ durationMs: Math.round(performance.now() - startedAt),
179
+ error: toolErrorPreview(content),
180
+ });
181
+ }
182
+ function textBlock(text) {
183
+ return { type: "text", text };
184
+ }
185
+ function textSummary(content) {
186
+ const text = contentText(content);
187
+ return {
188
+ lines: text.length === 0 ? 0 : text.split("\n").length,
189
+ characters: text.length,
190
+ };
191
+ }
192
+ function contentLineCount(content) {
193
+ if (content.length === 0)
194
+ return 0;
195
+ return content.endsWith("\n")
196
+ ? content.slice(0, -1).split("\n").length
197
+ : content.split("\n").length;
198
+ }
199
+ function countDiffStats(diff) {
200
+ if (!diff)
201
+ return { additions: 0, removals: 0 };
202
+ let additions = 0;
203
+ let removals = 0;
204
+ for (const line of diff.split("\n")) {
205
+ if (line.startsWith("+") && !line.startsWith("+++"))
206
+ additions++;
207
+ if (line.startsWith("-") && !line.startsWith("---"))
208
+ removals++;
209
+ }
210
+ return { additions, removals };
211
+ }
212
+ function newFilePatch(path, content) {
213
+ const lines = content.length === 0
214
+ ? []
215
+ : content.endsWith("\n")
216
+ ? content.slice(0, -1).split("\n")
217
+ : content.split("\n");
218
+ const hunkLength = lines.length;
219
+ const hunkRange = hunkLength === 0 ? "+0,0" : `+1,${hunkLength}`;
220
+ const body = lines.map((line) => `+${line}`).join("\n");
221
+ return [
222
+ `diff --git a/${path} b/${path}`,
223
+ "new file mode 100644",
224
+ "index 0000000..0000000",
225
+ "--- /dev/null",
226
+ `+++ b/${path}`,
227
+ `@@ -0,0 ${hunkRange} @@`,
228
+ body,
229
+ ]
230
+ .filter((line) => line.length > 0)
231
+ .join("\n");
232
+ }
233
+ function assetBaseUrl(config) {
234
+ return `${config.publicBaseUrl.replace(/\/+$/, "")}/mcp-app-assets`;
235
+ }
236
+ function uiManifestUrl() {
237
+ return new URL("../dist/ui/.vite/manifest.json", import.meta.url);
238
+ }
239
+ function readWorkspaceAppManifest() {
240
+ return JSON.parse(readFileSync(uiManifestUrl(), "utf8"));
241
+ }
242
+ function getWorkspaceAppManifestEntry() {
243
+ const manifest = readWorkspaceAppManifest();
244
+ const entry = manifest[WORKSPACE_APP_MANIFEST_ENTRY];
245
+ if (!entry?.file) {
246
+ throw new Error(`Missing ${WORKSPACE_APP_MANIFEST_ENTRY} in UI manifest.`);
247
+ }
248
+ return entry;
249
+ }
250
+ function assetUrl(baseUrl, assetPath) {
251
+ return `${baseUrl}/${assetPath.replace(/^\/+/, "")}`;
252
+ }
253
+ function workspaceAppHtml(config) {
254
+ const baseUrl = assetBaseUrl(config);
255
+ const entry = getWorkspaceAppManifestEntry();
256
+ const stylesheets = (entry.css ?? [])
257
+ .map((stylesheet) => ` <link rel="stylesheet" crossorigin href="${assetUrl(baseUrl, stylesheet)}" />`)
258
+ .join("\n");
259
+ return `<!doctype html>
260
+ <html lang="en">
261
+ <head>
262
+ <meta charset="UTF-8" />
263
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
264
+ <title>ForgeRelay Workspace</title>
265
+ <script type="module" crossorigin src="${assetUrl(baseUrl, entry.file)}"></script>
266
+ ${stylesheets}
267
+ </head>
268
+ <body>
269
+ <main id="app" class="shell">
270
+ <section class="empty">Waiting for a tool result.</section>
271
+ </main>
272
+ </body>
273
+ </html>`;
274
+ }
275
+ function appCsp(config) {
276
+ const publicBaseUrl = config.publicBaseUrl.replace(/\/+$/, "");
277
+ return {
278
+ resourceDomains: [publicBaseUrl],
279
+ connectDomains: [publicBaseUrl],
280
+ };
281
+ }
282
+ function uiBuildDirectory() {
283
+ return fileURLToPath(new URL("../dist/ui", import.meta.url));
284
+ }
285
+ function setAssetHeaders(res) {
286
+ res.setHeader("Access-Control-Allow-Origin", "*");
287
+ res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
288
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Range");
289
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
290
+ }
291
+ async function assertWorkspaceAppAssets() {
292
+ const entry = getWorkspaceAppManifestEntry();
293
+ const candidates = [entry.file, ...(entry.css ?? [])].map((assetPath) => new URL(`../dist/ui/${assetPath}`, import.meta.url));
294
+ for (const candidate of candidates) {
295
+ await access(candidate);
296
+ }
297
+ }
298
+ function processResult(snapshot) {
299
+ const status = snapshot.running
300
+ ? `Process running with session ID ${snapshot.sessionId}.`
301
+ : snapshot.signal
302
+ ? `Process exited after signal ${snapshot.signal}.`
303
+ : `Process exited with code ${snapshot.exitCode ?? "unknown"}.`;
304
+ return snapshot.output ? `${snapshot.output.replace(/\n$/, "")}\n${status}` : status;
305
+ }
306
+ function processOutputSchema() {
307
+ return resultOutputSchema({
308
+ sessionId: z.number().optional(),
309
+ running: z.boolean(),
310
+ exitCode: z.number().int().optional(),
311
+ signal: z.string().optional(),
312
+ wallTimeMs: z.number().nonnegative(),
313
+ outputTruncated: z.boolean(),
314
+ });
315
+ }
316
+ function processToolResponse(tool, workspaceId, snapshot, summary) {
317
+ const result = processResult(snapshot);
318
+ const content = [textBlock(result)];
319
+ const outputSummary = textSummary(snapshot.output ? [textBlock(snapshot.output)] : []);
320
+ return {
321
+ content,
322
+ _meta: {
323
+ tool,
324
+ card: {
325
+ workspaceId,
326
+ summary: { ...summary, ...outputSummary },
327
+ payload: { content },
328
+ },
329
+ },
330
+ structuredContent: {
331
+ result,
332
+ sessionId: snapshot.sessionId,
333
+ running: snapshot.running,
334
+ exitCode: snapshot.exitCode,
335
+ signal: snapshot.signal,
336
+ wallTimeMs: snapshot.wallTimeMs,
337
+ outputTruncated: snapshot.outputTruncated,
338
+ },
339
+ };
340
+ }
341
+ function registerCodexProcessTools(server, config, workspaces, processSessions) {
342
+ registerAppTool(server, "exec_command", {
343
+ title: "Execute command",
344
+ description: "Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a sessionId for write_stdin. Use this for file inspection, tests, builds, package scripts, and long-running processes. Call open_workspace first and pass workspaceId.",
345
+ inputSchema: {
346
+ workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
347
+ cmd: z.string().min(1).describe("Shell command to execute."),
348
+ tty: z
349
+ .boolean()
350
+ .optional()
351
+ .describe("Allocate a pseudo-terminal for interactive commands. Defaults to false."),
352
+ columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width. Defaults to 80."),
353
+ rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height. Defaults to 24."),
354
+ workingDirectory: z
355
+ .string()
356
+ .optional()
357
+ .describe("Working directory relative to the workspace root. Defaults to the workspace root."),
358
+ yieldTimeMs: z
359
+ .number()
360
+ .int()
361
+ .min(0)
362
+ .max(30_000)
363
+ .optional()
364
+ .describe("Milliseconds to wait before returning a running session. Defaults to 10000."),
365
+ maxOutputTokens: z
366
+ .number()
367
+ .int()
368
+ .positive()
369
+ .max(100_000)
370
+ .optional()
371
+ .describe("Approximate output token budget. Defaults to 10000."),
372
+ },
373
+ outputSchema: processOutputSchema(),
374
+ ...toolWidgetDescriptorMeta(config, "shell"),
375
+ annotations: SHELL_TOOL_ANNOTATIONS,
376
+ }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }) => {
377
+ const startedAt = performance.now();
378
+ const workspace = workspaces.getWorkspace(workspaceId);
379
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
380
+ const snapshot = await processSessions.start({
381
+ workspaceId,
382
+ command: cmd,
383
+ cwd,
384
+ workspaceRoot: workspace.root,
385
+ tty,
386
+ columns,
387
+ rows,
388
+ yieldTimeMs,
389
+ maxOutputTokens,
390
+ });
391
+ logToolCall(config, {
392
+ tool: "exec_command",
393
+ workspaceId,
394
+ workingDirectory: workingDirectory ?? ".",
395
+ command: cmd,
396
+ commandLength: cmd.length,
397
+ success: true,
398
+ durationMs: Math.round(performance.now() - startedAt),
399
+ });
400
+ return processToolResponse("exec_command", workspaceId, snapshot, {
401
+ command: cmd,
402
+ workingDirectory: workingDirectory ?? ".",
403
+ running: snapshot.running,
404
+ exitCode: snapshot.exitCode,
405
+ wallTimeMs: snapshot.wallTimeMs,
406
+ });
407
+ });
408
+ registerAppTool(server, "write_stdin", {
409
+ title: "Write to process",
410
+ description: "Poll or write characters to a process returned by exec_command. Omit chars or pass an empty string to poll. Pass \\u0003 to send Ctrl-C.",
411
+ inputSchema: {
412
+ workspaceId: z.string().describe("Workspace identifier used to start the process."),
413
+ sessionId: z.number().describe("Process session identifier returned by exec_command."),
414
+ chars: z.string().optional().describe("Characters to write. Omit or pass an empty string to poll."),
415
+ columns: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this width."),
416
+ rows: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this height."),
417
+ yieldTimeMs: z
418
+ .number()
419
+ .int()
420
+ .min(0)
421
+ .max(30_000)
422
+ .optional()
423
+ .describe("Milliseconds to wait for process output or completion. Defaults to 10000."),
424
+ maxOutputTokens: z
425
+ .number()
426
+ .int()
427
+ .positive()
428
+ .max(100_000)
429
+ .optional()
430
+ .describe("Approximate output token budget. Defaults to 10000."),
431
+ },
432
+ outputSchema: processOutputSchema(),
433
+ ...toolWidgetDescriptorMeta(config, "shell"),
434
+ annotations: SHELL_TOOL_ANNOTATIONS,
435
+ }, async ({ workspaceId, sessionId, chars, columns, rows, yieldTimeMs, maxOutputTokens }) => {
436
+ const startedAt = performance.now();
437
+ workspaces.getWorkspace(workspaceId);
438
+ const snapshot = await processSessions.write({
439
+ workspaceId,
440
+ sessionId,
441
+ chars,
442
+ columns,
443
+ rows,
444
+ yieldTimeMs,
445
+ maxOutputTokens,
446
+ });
447
+ logToolCall(config, {
448
+ tool: "write_stdin",
449
+ workspaceId,
450
+ success: true,
451
+ durationMs: Math.round(performance.now() - startedAt),
452
+ });
453
+ return processToolResponse("write_stdin", workspaceId, snapshot, {
454
+ sessionId,
455
+ charactersWritten: chars?.length ?? 0,
456
+ running: snapshot.running,
457
+ exitCode: snapshot.exitCode,
458
+ wallTimeMs: snapshot.wallTimeMs,
459
+ });
460
+ });
461
+ }
462
+ export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters) {
463
+ const toolDescriptions = buildToolDescriptions(config);
464
+ const server = new McpServer({
465
+ name: "forgerelay",
466
+ title: "ForgeRelay",
467
+ version: "0.1.0",
468
+ description: "Secure local coding workspace for MCP clients. Provides workspace-scoped file, search, edit, write, and shell tools.",
469
+ }, {
470
+ instructions: buildServerInstructions(config, {
471
+ artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
472
+ }),
473
+ });
474
+ registerAppResource(server, "ForgeRelay Diff Card", WORKSPACE_APP_URI, {
475
+ description: "Interactive card for viewing ForgeRelay file diffs.",
476
+ _meta: {
477
+ ui: {
478
+ csp: appCsp(config),
479
+ },
480
+ },
481
+ }, async () => {
482
+ await assertWorkspaceAppAssets();
483
+ return {
484
+ contents: [
485
+ {
486
+ uri: WORKSPACE_APP_URI,
487
+ mimeType: RESOURCE_MIME_TYPE,
488
+ text: workspaceAppHtml(config),
489
+ _meta: {
490
+ ui: {
491
+ csp: appCsp(config),
492
+ },
493
+ },
494
+ },
495
+ ],
496
+ };
497
+ });
498
+ registerAppTool(server, "open_workspace", {
499
+ title: "Open workspace",
500
+ description: "Open a local project directory as a coding workspace. The same directory reuses the same active workspaceId across requests. Default to checkout mode and only use mode=\"worktree\" when the user explicitly asks for isolated or parallel work. Managed worktrees use dedicated forgerelay/* branches and can later be safely closed into their original target branch with close_worktree. Existing managed worktree paths can also be reopened directly.",
501
+ inputSchema: {
502
+ path: z
503
+ .string()
504
+ .describe("Absolute path, or a leading-tilde home path such as ~/project, to a local project directory inside an allowed root. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
505
+ mode: z
506
+ .enum(["checkout", "worktree"])
507
+ .optional()
508
+ .describe("Defaults to checkout, which works in the actual directory. Use worktree only when the user explicitly requests isolated or parallel Git work."),
509
+ baseRef: z
510
+ .string()
511
+ .optional()
512
+ .describe("Local branch to base a managed worktree on and eventually merge back into. Only used with mode=\"worktree\". Defaults to the source checkout's current branch."),
513
+ newWorktree: z
514
+ .boolean()
515
+ .optional()
516
+ .describe("When true, create another isolated managed worktree instead of reusing the existing worktree for this project and baseRef. Use only when the user explicitly requests a separate worktree."),
517
+ },
518
+ outputSchema: {
519
+ workspaceId: z.string(),
520
+ root: z.string(),
521
+ mode: z.enum(["checkout", "worktree"]),
522
+ sourceRoot: z.string().optional(),
523
+ worktree: z
524
+ .object({
525
+ path: z.string(),
526
+ baseRef: z.string(),
527
+ baseSha: z.string(),
528
+ branch: z.string().optional(),
529
+ targetBranch: z.string().optional(),
530
+ dirtySource: z.boolean(),
531
+ detached: z.boolean(),
532
+ managed: z.boolean(),
533
+ })
534
+ .optional(),
535
+ worktrees: z.array(z.object({
536
+ workspaceId: z.string(),
537
+ path: z.string(),
538
+ baseRef: z.string(),
539
+ baseSha: z.string(),
540
+ branch: z.string().optional(),
541
+ targetBranch: z.string().optional(),
542
+ managed: z.boolean(),
543
+ current: z.boolean(),
544
+ })),
545
+ agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
546
+ availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
547
+ skills: z.array(workspaceSkillOutputSchema).optional(),
548
+ agentProviders: z.array(workspaceLocalAgentProviderOutputSchema).optional(),
549
+ agents: z.array(workspaceLocalAgentOutputSchema).optional(),
550
+ skillDiagnostics: z.array(z.unknown()).optional(),
551
+ instruction: z.string(),
552
+ },
553
+ ...toolWidgetDescriptorMeta(config, "workspace"),
554
+ annotations: {
555
+ readOnlyHint: false,
556
+ destructiveHint: false,
557
+ idempotentHint: false,
558
+ openWorldHint: false,
559
+ },
560
+ }, async ({ path, mode, baseRef, newWorktree }, { _meta }) => {
561
+ const startedAt = performance.now();
562
+ const { workspace, agentsFiles, availableAgentsFiles, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, mode, baseRef, newWorktree }, { conversationScopeId: openAiConversationScopeId(_meta) });
563
+ const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
564
+ if (config.widgets === "changes") {
565
+ await reviewCheckpoints.initializeWorkspace({
566
+ workspaceId: workspace.id,
567
+ root: workspace.root,
568
+ });
569
+ }
570
+ const cardSkills = workspace.skills
571
+ .filter((skill) => !skill.disableModelInvocation)
572
+ .map((skill) => ({
573
+ name: skill.name,
574
+ description: skill.description,
575
+ path: formatPathForPrompt(skill.filePath),
576
+ }));
577
+ const cardAgentProviders = config.subagents ? localAgentProviders : [];
578
+ const cardAgents = workspace.agentProfiles.map((profile) => {
579
+ const summary = summarizeLocalAgentProfile(profile);
580
+ const availability = cardAgentProviders.find((provider) => provider.name === summary.provider);
581
+ return {
582
+ ...summary,
583
+ providerAvailable: availability?.available,
584
+ providerUnavailableReason: availability?.reason,
585
+ };
586
+ });
587
+ const cardAgentsFiles = agentsFiles.map((file) => ({
588
+ path: formatAgentsPath(file.path, workspace.root),
589
+ content: file.content,
590
+ }));
591
+ const cardAvailableAgentsFiles = availableAgentsFiles.map((file) => ({
592
+ path: formatAgentsPath(file.path, workspace.root),
593
+ }));
594
+ const visibleSkills = includeBootstrapContext ? cardSkills : [];
595
+ const visibleAgentProviders = includeBootstrapContext ? cardAgentProviders : [];
596
+ const visibleAgents = includeBootstrapContext ? cardAgents : [];
597
+ const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
598
+ const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
599
+ const cardInstruction = config.skillsEnabled
600
+ ? "Use this workspaceId in all subsequent tool calls for this project. Default to the user's checkout; only create a worktree when the user explicitly requests isolated or parallel work. Managed worktrees are branch-backed. When a managed worktree task is complete and verified, close it with close_worktree so ForgeRelay can commit, fast-forward the target branch when safe, and clean up the worktree. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file. When a task matches an available skill in skills, read its path before proceeding."
601
+ : "Use this workspaceId in all subsequent tool calls for this project. Default to the user's checkout; only create a worktree when the user explicitly requests isolated or parallel work. Managed worktrees are branch-backed. When a managed worktree task is complete and verified, close it with close_worktree so ForgeRelay can commit, fast-forward the target branch when safe, and clean up the worktree. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file.";
602
+ const instruction = workspaceReused
603
+ ? includeBootstrapContext
604
+ ? [
605
+ `Workspace already exists as ${workspace.id} for this directory.`,
606
+ "Reuse this workspaceId for subsequent tool calls.",
607
+ "The complete project context is included because it has not yet been provided in this conversation or host context.",
608
+ ].join("\n\n")
609
+ : [
610
+ `Workspace already open as ${workspace.id}.`,
611
+ "Reuse this workspaceId for subsequent tool calls. This is the same directory previously opened in this conversation.",
612
+ "Continue following the project instructions, nested instruction files, skills, agent profiles, and diagnostics previously provided for this workspace. They remain active and are not repeated here.",
613
+ ].join("\n\n")
614
+ : workspace.mode === "worktree"
615
+ ? "Use this workspaceId for subsequent tool calls. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for this isolated worktree."
616
+ : cardInstruction;
617
+ const resultContent = [
618
+ {
619
+ type: "text",
620
+ text: [
621
+ workspaceReused
622
+ ? `Workspace already open as ${workspace.id}.`
623
+ : workspace.mode === "worktree"
624
+ ? `Opened isolated worktree workspace ${workspace.id}.`
625
+ : `Opened workspace ${workspace.id}.`,
626
+ `Root: ${workspace.root}`,
627
+ `Mode: ${workspace.mode}`,
628
+ loadedAgentsFiles.length > 0
629
+ ? `Loaded project instructions: ${loadedAgentsFiles.map((file) => file.path).join(", ")}`
630
+ : undefined,
631
+ availableAgentsFileOutputs.length > 0
632
+ ? `Available nested instructions: ${availableAgentsFileOutputs.map((file) => file.path).join(", ")}`
633
+ : undefined,
634
+ visibleSkills.length > 0
635
+ ? `Available skills: ${visibleSkills.map((skill) => skill.name).join(", ")}`
636
+ : undefined,
637
+ visibleAgentProviders.some((provider) => provider.available)
638
+ ? `Available subagent providers: ${visibleAgentProviders.filter((provider) => provider.available).map((provider) => provider.name).join(", ")}`
639
+ : undefined,
640
+ visibleAgentProviders.some((provider) => !provider.available)
641
+ ? `Unavailable subagent providers: ${visibleAgentProviders.filter((provider) => !provider.available).map(formatUnavailableAgentProvider).join(", ")}`
642
+ : undefined,
643
+ visibleAgents.length > 0
644
+ ? `Available subagent profiles: ${visibleAgents.map(formatVisibleAgent).join(", ")}`
645
+ : undefined,
646
+ knownWorktrees.length > 0
647
+ ? `Known worktrees: ${knownWorktrees.map((worktree) => `${worktree.path} [${worktree.workspaceId}]${worktree.branch ? ` branch=${worktree.branch}` : ""}${worktree.targetBranch ? ` target=${worktree.targetBranch}` : ""}${worktree.current ? " (current)" : ""}`).join(", ")}`
648
+ : undefined,
649
+ instruction,
650
+ ].filter(Boolean).join("\n"),
651
+ },
652
+ ];
653
+ logToolCall(config, {
654
+ tool: "open_workspace",
655
+ workspaceId: workspace.id,
656
+ path: workspace.root,
657
+ success: true,
658
+ durationMs: Math.round(performance.now() - startedAt),
659
+ });
660
+ return {
661
+ content: resultContent,
662
+ _meta: {
663
+ tool: "open_workspace",
664
+ card: {
665
+ workspaceId: workspace.id,
666
+ root: workspace.root,
667
+ path: workspace.root,
668
+ mode: workspace.mode,
669
+ workspaceReused,
670
+ includeBootstrapContext,
671
+ sourceRoot: workspace.sourceRoot,
672
+ worktree: workspace.worktree,
673
+ worktrees: knownWorktrees,
674
+ agentsFiles: cardAgentsFiles,
675
+ availableAgentsFiles: cardAvailableAgentsFiles,
676
+ skills: cardSkills,
677
+ agentProviders: cardAgentProviders,
678
+ agents: cardAgents,
679
+ instruction: cardInstruction,
680
+ summary: {
681
+ mode: workspace.mode,
682
+ agentsFiles: cardAgentsFiles.length,
683
+ availableAgentsFiles: cardAvailableAgentsFiles.length,
684
+ skills: cardSkills.length,
685
+ agentProviders: cardAgentProviders.length,
686
+ agents: cardAgents.length,
687
+ },
688
+ },
689
+ },
690
+ structuredContent: {
691
+ workspaceId: workspace.id,
692
+ root: workspace.root,
693
+ mode: workspace.mode,
694
+ sourceRoot: workspace.sourceRoot,
695
+ worktree: workspace.worktree,
696
+ worktrees: knownWorktrees,
697
+ ...(includeBootstrapContext
698
+ ? {
699
+ agentsFiles: loadedAgentsFiles,
700
+ availableAgentsFiles: availableAgentsFileOutputs,
701
+ skills: visibleSkills,
702
+ agentProviders: visibleAgentProviders,
703
+ agents: visibleAgents,
704
+ skillDiagnostics: workspace.skillDiagnostics,
705
+ }
706
+ : {}),
707
+ instruction,
708
+ },
709
+ };
710
+ });
711
+ registerAppTool(server, toolNames.closeWorktree, {
712
+ title: "Close worktree",
713
+ description: "Finish a managed ForgeRelay worktree after its task has been completed and verified. ForgeRelay commits any remaining worktree changes, fast-forwards the original target branch only when the source checkout is clean and the histories have not diverged, then removes the worktree and its forgerelay/* branch. If safe fast-forward is not possible, the source checkout is left out of a merge-conflict state and the worktree is preserved.",
714
+ inputSchema: {
715
+ workspaceId: z
716
+ .string()
717
+ .describe("Managed worktree workspace identifier returned by open_workspace."),
718
+ commitMessage: z
719
+ .string()
720
+ .min(1)
721
+ .describe("Concise Git commit message describing the completed worktree changes."),
722
+ },
723
+ outputSchema: resultOutputSchema({
724
+ workspaceId: z.string(),
725
+ sourceRoot: z.string(),
726
+ branch: z.string(),
727
+ targetBranch: z.string(),
728
+ commitSha: z.string(),
729
+ mergedSha: z.string(),
730
+ committed: z.boolean(),
731
+ cleanupWarning: z.string().optional(),
732
+ }),
733
+ _meta: {},
734
+ annotations: WRITE_TOOL_ANNOTATIONS,
735
+ }, async ({ workspaceId, commitMessage }) => {
736
+ const startedAt = performance.now();
737
+ const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
738
+ const result = [
739
+ `Closed managed worktree ${workspaceId}.`,
740
+ `Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
741
+ `Source checkout: ${closed.sourceRoot}`,
742
+ `Commit: ${closed.commitSha}`,
743
+ closed.cleanupWarning
744
+ ? `Cleanup warning: ${closed.cleanupWarning}`
745
+ : "The worktree directory and managed branch were removed.",
746
+ ].join("\n");
747
+ logToolCall(config, {
748
+ tool: toolNames.closeWorktree,
749
+ workspaceId,
750
+ path: closed.sourceRoot,
751
+ success: true,
752
+ durationMs: Math.round(performance.now() - startedAt),
753
+ });
754
+ return {
755
+ content: [{ type: "text", text: result }],
756
+ structuredContent: {
757
+ result,
758
+ workspaceId,
759
+ sourceRoot: closed.sourceRoot,
760
+ branch: closed.branch,
761
+ targetBranch: closed.targetBranch,
762
+ commitSha: closed.commitSha,
763
+ mergedSha: closed.mergedSha,
764
+ committed: closed.committed,
765
+ cleanupWarning: closed.cleanupWarning,
766
+ },
767
+ };
768
+ });
769
+ registerAppTool(server, toolNames.read, {
770
+ title: "Read file",
771
+ description: toolDescriptions.read,
772
+ inputSchema: {
773
+ workspaceId: z
774
+ .string()
775
+ .describe("Workspace identifier returned by open_workspace."),
776
+ path: z
777
+ .string()
778
+ .describe(config.skillsEnabled
779
+ ? "File path to read, relative to the workspace root. May also be an advertised skill path from open_workspace skills."
780
+ : "File path to read, relative to the workspace root."),
781
+ offset: z
782
+ .number()
783
+ .int()
784
+ .positive()
785
+ .optional()
786
+ .describe("1-indexed line number to start reading from."),
787
+ limit: z
788
+ .number()
789
+ .int()
790
+ .positive()
791
+ .optional()
792
+ .describe("Maximum number of lines to read."),
793
+ },
794
+ outputSchema: resultOutputSchema(),
795
+ ...toolWidgetDescriptorMeta(config, "read"),
796
+ annotations: { readOnlyHint: true },
797
+ }, async ({ workspaceId, ...input }) => {
798
+ const startedAt = performance.now();
799
+ const workspace = workspaces.getWorkspace(workspaceId);
800
+ const readPath = workspaces.resolveReadPath(workspace, input.path);
801
+ const response = await readFileTool({ ...input, path: readPath.absolutePath }, {
802
+ cwd: workspace.root,
803
+ root: workspace.root,
804
+ readRoots: readPath.readRoots,
805
+ });
806
+ if (response.isError) {
807
+ logFailedToolResponse(config, {
808
+ tool: toolNames.read,
809
+ workspaceId,
810
+ path: input.path,
811
+ }, response.content, startedAt);
812
+ return response;
813
+ }
814
+ workspaces.markReadPathLoaded(workspace, readPath);
815
+ const summary = {
816
+ ...textSummary(response.content),
817
+ offset: input.offset ?? 1,
818
+ limited: input.limit !== undefined,
819
+ };
820
+ logToolCall(config, {
821
+ tool: toolNames.read,
822
+ workspaceId,
823
+ path: input.path,
824
+ success: true,
825
+ durationMs: Math.round(performance.now() - startedAt),
826
+ });
827
+ return {
828
+ ...response,
829
+ _meta: {
830
+ tool: toolNames.read,
831
+ card: {
832
+ workspaceId,
833
+ path: input.path,
834
+ summary,
835
+ payload: { content: response.content },
836
+ },
837
+ },
838
+ structuredContent: {
839
+ result: contentText(response.content),
840
+ },
841
+ };
842
+ });
843
+ if (config.toolMode !== "codex") {
844
+ registerAppTool(server, toolNames.write, {
845
+ title: "Write file",
846
+ description: toolDescriptions.write,
847
+ inputSchema: {
848
+ workspaceId: z
849
+ .string()
850
+ .describe("Workspace identifier returned by open_workspace."),
851
+ path: z
852
+ .string()
853
+ .describe("File path to write, relative to the workspace root."),
854
+ content: z.string().describe("Complete new file content."),
855
+ },
856
+ outputSchema: resultOutputSchema(),
857
+ ...toolWidgetDescriptorMeta(config, "write"),
858
+ annotations: WRITE_TOOL_ANNOTATIONS,
859
+ }, async ({ workspaceId, ...input }) => {
860
+ const startedAt = performance.now();
861
+ const workspace = workspaces.getWorkspace(workspaceId);
862
+ workspaces.resolvePath(workspace, input.path);
863
+ const response = await writeFileTool(input, {
864
+ cwd: workspace.root,
865
+ root: workspace.root,
866
+ });
867
+ if (response.isError) {
868
+ logFailedToolResponse(config, {
869
+ tool: toolNames.write,
870
+ workspaceId,
871
+ path: input.path,
872
+ }, response.content, startedAt);
873
+ return response;
874
+ }
875
+ const patch = newFilePatch(input.path, input.content);
876
+ const stats = countDiffStats(patch);
877
+ const summary = {
878
+ ...stats,
879
+ lines: contentLineCount(input.content),
880
+ characters: input.content.length,
881
+ };
882
+ logToolCall(config, {
883
+ tool: toolNames.write,
884
+ workspaceId,
885
+ path: input.path,
886
+ success: true,
887
+ durationMs: Math.round(performance.now() - startedAt),
888
+ });
889
+ return {
890
+ ...response,
891
+ _meta: {
892
+ tool: toolNames.write,
893
+ card: {
894
+ workspaceId,
895
+ path: input.path,
896
+ summary,
897
+ payload: {
898
+ content: response.content,
899
+ patch,
900
+ },
901
+ },
902
+ },
903
+ structuredContent: {
904
+ result: contentText(response.content),
905
+ },
906
+ };
907
+ });
908
+ registerAppTool(server, toolNames.edit, {
909
+ title: "Edit file",
910
+ description: toolDescriptions.edit,
911
+ inputSchema: {
912
+ workspaceId: z
913
+ .string()
914
+ .describe("Workspace identifier returned by open_workspace."),
915
+ path: z
916
+ .string()
917
+ .describe("File path to edit, relative to the workspace root."),
918
+ edits: z
919
+ .array(z.object({
920
+ oldText: z
921
+ .string()
922
+ .describe("Exact text to replace. Must match uniquely in the original file."),
923
+ newText: z.string().describe("Replacement text."),
924
+ }))
925
+ .min(1),
926
+ },
927
+ outputSchema: resultOutputSchema({
928
+ status: z.literal("applied"),
929
+ }),
930
+ ...toolWidgetDescriptorMeta(config, "edit"),
931
+ annotations: EDIT_TOOL_ANNOTATIONS,
932
+ }, async ({ workspaceId, ...input }) => {
933
+ const startedAt = performance.now();
934
+ const workspace = workspaces.getWorkspace(workspaceId);
935
+ workspaces.resolvePath(workspace, input.path);
936
+ const response = await editFileTool(input, {
937
+ cwd: workspace.root,
938
+ root: workspace.root,
939
+ });
940
+ if (response.isError) {
941
+ logFailedToolResponse(config, {
942
+ tool: toolNames.edit,
943
+ workspaceId,
944
+ path: input.path,
945
+ }, response.content, startedAt);
946
+ return response;
947
+ }
948
+ const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
949
+ const summary = {
950
+ ...stats,
951
+ editCount: input.edits.length,
952
+ };
953
+ const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
954
+ const editContent = [textBlock(editResultText)];
955
+ logToolCall(config, {
956
+ tool: toolNames.edit,
957
+ workspaceId,
958
+ path: input.path,
959
+ success: true,
960
+ durationMs: Math.round(performance.now() - startedAt),
961
+ });
962
+ return {
963
+ content: editContent,
964
+ _meta: {
965
+ tool: toolNames.edit,
966
+ card: {
967
+ workspaceId,
968
+ path: input.path,
969
+ summary,
970
+ payload: {
971
+ diff: response.details?.diff,
972
+ patch: response.details?.patch,
973
+ },
974
+ },
975
+ },
976
+ structuredContent: {
977
+ status: "applied",
978
+ result: contentText(editContent),
979
+ },
980
+ };
981
+ });
982
+ }
983
+ if (config.toolMode === "codex") {
984
+ registerAppTool(server, "apply_patch", {
985
+ title: "Apply patch",
986
+ description: toolDescriptions.applyPatch,
987
+ inputSchema: {
988
+ workspaceId: z
989
+ .string()
990
+ .describe("Workspace identifier returned by open_workspace."),
991
+ patch: z
992
+ .string()
993
+ .describe("Patch text enclosed by *** Begin Patch and *** End Patch markers."),
994
+ },
995
+ outputSchema: resultOutputSchema({
996
+ additions: z.number(),
997
+ removals: z.number(),
998
+ files: z.array(z.object({
999
+ path: z.string(),
1000
+ previousPath: z.string().optional(),
1001
+ operation: z.enum(["add", "update", "delete", "move"]),
1002
+ })),
1003
+ }),
1004
+ ...toolWidgetDescriptorMeta(config, "edit"),
1005
+ annotations: EDIT_TOOL_ANNOTATIONS,
1006
+ }, async ({ workspaceId, patch }) => {
1007
+ const startedAt = performance.now();
1008
+ const workspace = workspaces.getWorkspace(workspaceId);
1009
+ const applied = await applyPatch(workspace.root, patch);
1010
+ const paths = applied.files.map((file) => file.path).join(", ");
1011
+ const result = `Applied patch to ${applied.files.length} file(s): ${paths}`;
1012
+ const content = [textBlock(result)];
1013
+ const displayPath = applied.files.length === 1
1014
+ ? applied.files[0]?.path
1015
+ : `${applied.files.length} files`;
1016
+ logToolCall(config, {
1017
+ tool: "apply_patch",
1018
+ workspaceId,
1019
+ success: true,
1020
+ durationMs: Math.round(performance.now() - startedAt),
1021
+ });
1022
+ return {
1023
+ content,
1024
+ _meta: {
1025
+ tool: "apply_patch",
1026
+ card: {
1027
+ workspaceId,
1028
+ path: displayPath,
1029
+ summary: {
1030
+ files: applied.files.length,
1031
+ additions: applied.additions,
1032
+ removals: applied.removals,
1033
+ },
1034
+ files: applied.files,
1035
+ payload: { patch: applied.patch },
1036
+ },
1037
+ },
1038
+ structuredContent: {
1039
+ result,
1040
+ additions: applied.additions,
1041
+ removals: applied.removals,
1042
+ files: applied.files,
1043
+ },
1044
+ };
1045
+ });
1046
+ }
1047
+ if (config.widgets === "changes") {
1048
+ registerAppTool(server, "show_changes", {
1049
+ title: "Show changes",
1050
+ description: "Show the changes made in this turn for an open workspace. Call this once after the final related file change and before your final response so the user can review the combined diff. Do not call it after each individual file change.",
1051
+ inputSchema: {
1052
+ workspaceId: z
1053
+ .string()
1054
+ .describe("Workspace identifier returned by open_workspace."),
1055
+ },
1056
+ outputSchema: resultOutputSchema(),
1057
+ ...toolWidgetDescriptorMeta(config, "show_changes"),
1058
+ annotations: { readOnlyHint: true },
1059
+ }, async ({ workspaceId }) => {
1060
+ const startedAt = performance.now();
1061
+ const workspace = workspaces.getWorkspace(workspaceId);
1062
+ const review = await reviewCheckpoints.reviewChanges({
1063
+ workspaceId,
1064
+ root: workspace.root,
1065
+ markReviewed: true,
1066
+ });
1067
+ const content = [textBlock(review.result)];
1068
+ logToolCall(config, {
1069
+ tool: "show_changes",
1070
+ workspaceId,
1071
+ success: true,
1072
+ durationMs: Math.round(performance.now() - startedAt),
1073
+ });
1074
+ return {
1075
+ content,
1076
+ _meta: {
1077
+ tool: "show_changes",
1078
+ card: {
1079
+ workspaceId,
1080
+ summary: review.summary,
1081
+ files: review.files,
1082
+ payload: {
1083
+ patch: review.patch,
1084
+ },
1085
+ },
1086
+ },
1087
+ structuredContent: {
1088
+ result: contentText(content),
1089
+ },
1090
+ };
1091
+ });
1092
+ }
1093
+ if (config.toolMode === "full") {
1094
+ registerAppTool(server, toolNames.grep, {
1095
+ title: "Grep",
1096
+ description: "Search file contents inside an open workspace. Use this before broad reads when looking for symbols, text, or usage sites. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1097
+ inputSchema: {
1098
+ workspaceId: z
1099
+ .string()
1100
+ .describe("Workspace identifier returned by open_workspace."),
1101
+ pattern: z.string().describe("Search pattern."),
1102
+ path: z
1103
+ .string()
1104
+ .optional()
1105
+ .describe("Optional path or glob scope relative to the workspace root."),
1106
+ include: z.string().optional().describe("Optional include glob."),
1107
+ },
1108
+ outputSchema: resultOutputSchema(),
1109
+ ...toolWidgetDescriptorMeta(config, "search"),
1110
+ annotations: { readOnlyHint: true },
1111
+ }, async ({ workspaceId, ...input }) => {
1112
+ const startedAt = performance.now();
1113
+ const workspace = workspaces.getWorkspace(workspaceId);
1114
+ if (input.path)
1115
+ workspaces.resolvePath(workspace, input.path);
1116
+ const response = await grepFilesTool(input, {
1117
+ cwd: workspace.root,
1118
+ root: workspace.root,
1119
+ });
1120
+ if (response.isError) {
1121
+ logFailedToolResponse(config, {
1122
+ tool: toolNames.grep,
1123
+ workspaceId,
1124
+ path: input.path,
1125
+ }, response.content, startedAt);
1126
+ return response;
1127
+ }
1128
+ const summary = {
1129
+ pattern: input.pattern,
1130
+ scope: input.path ?? ".",
1131
+ ...textSummary(response.content),
1132
+ };
1133
+ logToolCall(config, {
1134
+ tool: toolNames.grep,
1135
+ workspaceId,
1136
+ path: input.path,
1137
+ success: true,
1138
+ durationMs: Math.round(performance.now() - startedAt),
1139
+ });
1140
+ return {
1141
+ ...response,
1142
+ _meta: {
1143
+ tool: toolNames.grep,
1144
+ card: {
1145
+ workspaceId,
1146
+ path: input.path,
1147
+ summary,
1148
+ payload: { content: response.content },
1149
+ },
1150
+ },
1151
+ structuredContent: {
1152
+ result: contentText(response.content),
1153
+ },
1154
+ };
1155
+ });
1156
+ registerAppTool(server, toolNames.glob, {
1157
+ title: "Glob",
1158
+ description: "Find files by glob pattern inside an open workspace. Use this to discover filenames or narrow file sets before reading. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1159
+ inputSchema: {
1160
+ workspaceId: z
1161
+ .string()
1162
+ .describe("Workspace identifier returned by open_workspace."),
1163
+ pattern: z.string().describe("File glob pattern."),
1164
+ path: z
1165
+ .string()
1166
+ .optional()
1167
+ .describe("Optional path scope relative to the workspace root."),
1168
+ },
1169
+ outputSchema: resultOutputSchema(),
1170
+ ...toolWidgetDescriptorMeta(config, "search"),
1171
+ annotations: { readOnlyHint: true },
1172
+ }, async ({ workspaceId, ...input }) => {
1173
+ const startedAt = performance.now();
1174
+ const workspace = workspaces.getWorkspace(workspaceId);
1175
+ if (input.path)
1176
+ workspaces.resolvePath(workspace, input.path);
1177
+ const response = await findFilesTool(input, {
1178
+ cwd: workspace.root,
1179
+ root: workspace.root,
1180
+ });
1181
+ if (response.isError) {
1182
+ logFailedToolResponse(config, {
1183
+ tool: toolNames.glob,
1184
+ workspaceId,
1185
+ path: input.path,
1186
+ }, response.content, startedAt);
1187
+ return response;
1188
+ }
1189
+ const summary = {
1190
+ pattern: input.pattern,
1191
+ scope: input.path ?? ".",
1192
+ ...textSummary(response.content),
1193
+ };
1194
+ logToolCall(config, {
1195
+ tool: toolNames.glob,
1196
+ workspaceId,
1197
+ path: input.path,
1198
+ success: true,
1199
+ durationMs: Math.round(performance.now() - startedAt),
1200
+ });
1201
+ return {
1202
+ ...response,
1203
+ _meta: {
1204
+ tool: toolNames.glob,
1205
+ card: {
1206
+ workspaceId,
1207
+ path: input.path,
1208
+ summary,
1209
+ payload: { content: response.content },
1210
+ },
1211
+ },
1212
+ structuredContent: {
1213
+ result: contentText(response.content),
1214
+ },
1215
+ };
1216
+ });
1217
+ registerAppTool(server, toolNames.ls, {
1218
+ title: "Ls",
1219
+ description: "List a directory inside an open workspace. Use this for directory inspection before reading files. Call open_workspace first and pass workspaceId.",
1220
+ inputSchema: {
1221
+ workspaceId: z
1222
+ .string()
1223
+ .describe("Workspace identifier returned by open_workspace."),
1224
+ path: z
1225
+ .string()
1226
+ .describe("Directory path to list, relative to the workspace root."),
1227
+ },
1228
+ outputSchema: resultOutputSchema(),
1229
+ ...toolWidgetDescriptorMeta(config, "directory"),
1230
+ annotations: { readOnlyHint: true },
1231
+ }, async ({ workspaceId, ...input }) => {
1232
+ const startedAt = performance.now();
1233
+ const workspace = workspaces.getWorkspace(workspaceId);
1234
+ workspaces.resolvePath(workspace, input.path);
1235
+ const response = await listDirectoryTool(input, {
1236
+ cwd: workspace.root,
1237
+ root: workspace.root,
1238
+ });
1239
+ if (response.isError) {
1240
+ logFailedToolResponse(config, {
1241
+ tool: toolNames.ls,
1242
+ workspaceId,
1243
+ path: input.path,
1244
+ }, response.content, startedAt);
1245
+ return response;
1246
+ }
1247
+ const summary = textSummary(response.content);
1248
+ logToolCall(config, {
1249
+ tool: toolNames.ls,
1250
+ workspaceId,
1251
+ path: input.path,
1252
+ success: true,
1253
+ durationMs: Math.round(performance.now() - startedAt),
1254
+ });
1255
+ return {
1256
+ ...response,
1257
+ _meta: {
1258
+ tool: toolNames.ls,
1259
+ card: {
1260
+ workspaceId,
1261
+ path: input.path,
1262
+ summary,
1263
+ payload: { content: response.content },
1264
+ },
1265
+ },
1266
+ structuredContent: {
1267
+ result: contentText(response.content),
1268
+ },
1269
+ };
1270
+ });
1271
+ }
1272
+ if (config.toolMode !== "codex") {
1273
+ registerAppTool(server, toolNames.shell, {
1274
+ title: "Bash",
1275
+ description: toolDescriptions.shell,
1276
+ inputSchema: {
1277
+ workspaceId: z
1278
+ .string()
1279
+ .describe("Workspace identifier returned by open_workspace."),
1280
+ command: z
1281
+ .string()
1282
+ .describe(toolDescriptions.shellCommand),
1283
+ workingDirectory: z
1284
+ .string()
1285
+ .optional()
1286
+ .describe("Optional working directory relative to the workspace root. Defaults to the workspace root."),
1287
+ timeout: z
1288
+ .number()
1289
+ .positive()
1290
+ .max(300)
1291
+ .optional()
1292
+ .describe("Timeout in seconds. Defaults to 30, max 300."),
1293
+ },
1294
+ outputSchema: resultOutputSchema(),
1295
+ ...toolWidgetDescriptorMeta(config, "shell"),
1296
+ annotations: SHELL_TOOL_ANNOTATIONS,
1297
+ }, async ({ workspaceId, workingDirectory, ...input }) => {
1298
+ const startedAt = performance.now();
1299
+ const workspace = workspaces.getWorkspace(workspaceId);
1300
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1301
+ const response = await runShellTool(input, {
1302
+ cwd,
1303
+ root: workspace.root,
1304
+ });
1305
+ if (response.isError) {
1306
+ logFailedToolResponse(config, {
1307
+ tool: toolNames.shell,
1308
+ workspaceId,
1309
+ workingDirectory: workingDirectory ?? ".",
1310
+ command: input.command,
1311
+ commandLength: input.command.length,
1312
+ }, response.content, startedAt);
1313
+ return response;
1314
+ }
1315
+ const summary = {
1316
+ command: input.command,
1317
+ workingDirectory: workingDirectory ?? ".",
1318
+ ...textSummary(response.content),
1319
+ };
1320
+ logToolCall(config, {
1321
+ tool: toolNames.shell,
1322
+ workspaceId,
1323
+ workingDirectory: workingDirectory ?? ".",
1324
+ command: input.command,
1325
+ commandLength: input.command.length,
1326
+ success: true,
1327
+ durationMs: Math.round(performance.now() - startedAt),
1328
+ });
1329
+ return {
1330
+ ...response,
1331
+ _meta: {
1332
+ tool: toolNames.shell,
1333
+ card: {
1334
+ workspaceId,
1335
+ path: workingDirectory,
1336
+ summary,
1337
+ payload: { content: response.content },
1338
+ },
1339
+ },
1340
+ structuredContent: {
1341
+ result: contentText(response.content),
1342
+ },
1343
+ };
1344
+ });
1345
+ }
1346
+ if (config.toolMode === "codex") {
1347
+ registerCodexProcessTools(server, config, workspaces, processSessions);
1348
+ }
1349
+ if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
1350
+ registerArtifactTools(server, {
1351
+ config,
1352
+ workspaces,
1353
+ incomingArtifactAdapters,
1354
+ });
1355
+ }
1356
+ return server;
1357
+ }
1358
+ export function createServer(config = loadConfig(), options = {}) {
1359
+ const incomingArtifactAdapters = options.incomingArtifactAdapters
1360
+ ?? [createOpenAIIncomingArtifactAdapter()];
1361
+ const allowedHosts = config.allowedHosts.includes("*")
1362
+ ? undefined
1363
+ : Array.from(new Set([config.host, ...config.allowedHosts]));
1364
+ const app = createMcpExpressApp({
1365
+ host: config.host,
1366
+ ...(allowedHosts ? { allowedHosts } : {}),
1367
+ });
1368
+ const transports = new McpSessionRegistry();
1369
+ const mcpUrl = new URL("/mcp", config.publicBaseUrl);
1370
+ const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
1371
+ const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir);
1372
+ const bearerAuth = requireBearerAuth({
1373
+ verifier: oauthProvider,
1374
+ requiredScopes: [config.oauth.scopes[0] ?? "devspace"],
1375
+ resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(resourceServerUrl),
1376
+ });
1377
+ const workspaceStore = createWorkspaceStore(config.stateDir);
1378
+ const workspaces = new WorkspaceRegistry(config, workspaceStore);
1379
+ const reviewCheckpoints = createReviewCheckpointManager();
1380
+ const processSessions = new ProcessSessionManager();
1381
+ const localAgentProviders = config.subagents
1382
+ ? getLocalAgentProviderAvailabilitySnapshot()
1383
+ : [];
1384
+ const logSessionCloseResults = (reason, results) => {
1385
+ for (const result of results) {
1386
+ if (result.error) {
1387
+ logEvent(config.logging, "warn", "mcp_session_close_failed", {
1388
+ reason,
1389
+ sessionIdPrefix: sessionIdPrefix(result.sessionId),
1390
+ error: result.error instanceof Error
1391
+ ? result.error.message
1392
+ : String(result.error),
1393
+ });
1394
+ continue;
1395
+ }
1396
+ logEvent(config.logging, "info", "mcp_session_closed", {
1397
+ reason,
1398
+ sessionIdPrefix: sessionIdPrefix(result.sessionId),
1399
+ });
1400
+ }
1401
+ };
1402
+ const sessionCleanupTimer = setInterval(() => {
1403
+ void transports
1404
+ .closeIdle(MCP_SESSION_IDLE_TIMEOUT_MS)
1405
+ .then((results) => logSessionCloseResults("idle_timeout", results));
1406
+ }, MCP_SESSION_CLEANUP_INTERVAL_MS);
1407
+ sessionCleanupTimer.unref();
1408
+ if (config.logging.trustProxy) {
1409
+ app.set("trust proxy", true);
1410
+ }
1411
+ app.use((req, res, next) => {
1412
+ const requestId = randomUUID();
1413
+ const startedAt = performance.now();
1414
+ res.locals.requestId = requestId;
1415
+ res.on("finish", () => {
1416
+ const path = requestPath(req);
1417
+ if (!config.logging.requests)
1418
+ return;
1419
+ if (!config.logging.assets && path.startsWith("/mcp-app-assets"))
1420
+ return;
1421
+ logEvent(config.logging, "info", "http_request", {
1422
+ requestId,
1423
+ method: req.method,
1424
+ path,
1425
+ status: res.statusCode,
1426
+ durationMs: Math.round(performance.now() - startedAt),
1427
+ ...requestLogFields(req, config),
1428
+ });
1429
+ });
1430
+ next();
1431
+ });
1432
+ app.use(mcpAuthRouter({
1433
+ provider: oauthProvider,
1434
+ issuerUrl: new URL(config.publicBaseUrl),
1435
+ baseUrl: new URL(config.publicBaseUrl),
1436
+ resourceServerUrl,
1437
+ scopesSupported: config.oauth.scopes,
1438
+ resourceName: "ForgeRelay",
1439
+ }));
1440
+ app.options("/mcp-app-assets/{*asset}", (_req, res) => {
1441
+ setAssetHeaders(res);
1442
+ res.sendStatus(204);
1443
+ });
1444
+ app.use("/mcp-app-assets", express.static(uiBuildDirectory(), {
1445
+ immutable: true,
1446
+ maxAge: "1y",
1447
+ fallthrough: false,
1448
+ setHeaders: setAssetHeaders,
1449
+ }));
1450
+ app.get("/healthz", (_req, res) => {
1451
+ res.json({ ok: true, name: "forgerelay" });
1452
+ });
1453
+ app.all("/mcp", async (req, res) => {
1454
+ const requestId = res.locals.requestId;
1455
+ const sessionId = req.header("mcp-session-id");
1456
+ const initializeRequest = req.method === "POST" && isInitializeRequest(req.body);
1457
+ await new Promise((resolve, reject) => {
1458
+ bearerAuth(req, res, (error) => {
1459
+ if (error)
1460
+ reject(error);
1461
+ else
1462
+ resolve();
1463
+ });
1464
+ });
1465
+ if (res.headersSent)
1466
+ return;
1467
+ if (!req.auth?.resource || !checkResourceAllowed({ requestedResource: req.auth.resource, configuredResource: resourceServerUrl })) {
1468
+ logEvent(config.logging, "warn", "auth_denied", {
1469
+ requestId,
1470
+ method: req.method,
1471
+ path: requestPath(req),
1472
+ reason: "invalid_oauth_resource",
1473
+ ...requestLogFields(req, config),
1474
+ });
1475
+ sendJsonRpcError(res, 401, -32001, "Unauthorized");
1476
+ return;
1477
+ }
1478
+ logEvent(config.logging, "debug", "mcp_request", {
1479
+ requestId,
1480
+ method: req.method,
1481
+ sessionIdPresent: Boolean(sessionId),
1482
+ sessionIdPrefix: sessionIdPrefix(sessionId),
1483
+ isInitialize: initializeRequest,
1484
+ });
1485
+ try {
1486
+ let transport;
1487
+ if (sessionId) {
1488
+ transport = transports.get(sessionId);
1489
+ if (!transport) {
1490
+ sendJsonRpcError(res, 404, -32000, "Unknown MCP session");
1491
+ return;
1492
+ }
1493
+ }
1494
+ else if (initializeRequest) {
1495
+ transport = new StreamableHTTPServerTransport({
1496
+ sessionIdGenerator: () => randomUUID(),
1497
+ onsessioninitialized: (newSessionId) => {
1498
+ if (transport)
1499
+ transports.register(newSessionId, transport);
1500
+ logEvent(config.logging, "info", "mcp_session_created", {
1501
+ requestId,
1502
+ sessionIdPrefix: sessionIdPrefix(newSessionId),
1503
+ ...requestLogFields(req, config),
1504
+ });
1505
+ },
1506
+ });
1507
+ transport.onclose = () => {
1508
+ const closedSessionId = transport?.sessionId;
1509
+ if (closedSessionId && transports.remove(closedSessionId)) {
1510
+ logEvent(config.logging, "info", "mcp_session_closed", {
1511
+ reason: "transport_close",
1512
+ sessionIdPrefix: sessionIdPrefix(closedSessionId),
1513
+ });
1514
+ }
1515
+ };
1516
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters);
1517
+ await server.connect(transport);
1518
+ }
1519
+ else {
1520
+ sendJsonRpcError(res, 400, -32000, "No valid MCP session");
1521
+ return;
1522
+ }
1523
+ await transport.handleRequest(req, res, req.body);
1524
+ }
1525
+ catch (error) {
1526
+ logEvent(config.logging, "error", "mcp_request_error", {
1527
+ requestId,
1528
+ error: error instanceof Error ? error.message : String(error),
1529
+ });
1530
+ if (!res.headersSent) {
1531
+ sendJsonRpcError(res, 500, -32603, "Internal server error");
1532
+ }
1533
+ }
1534
+ });
1535
+ let closePromise;
1536
+ return {
1537
+ app,
1538
+ config,
1539
+ localAgentProviders,
1540
+ close: () => {
1541
+ closePromise ??= (async () => {
1542
+ clearInterval(sessionCleanupTimer);
1543
+ const results = await transports.closeAll();
1544
+ logSessionCloseResults("server_shutdown", results);
1545
+ processSessions.shutdown();
1546
+ oauthProvider.close();
1547
+ workspaceStore.close?.();
1548
+ })();
1549
+ return closePromise;
1550
+ },
1551
+ };
1552
+ }
1553
+ async function isMainModule() {
1554
+ if (!process.argv[1])
1555
+ return false;
1556
+ const modulePath = await realpath(fileURLToPath(import.meta.url));
1557
+ const entrypointPath = await realpath(process.argv[1]);
1558
+ return modulePath === entrypointPath;
1559
+ }
1560
+ if (await isMainModule()) {
1561
+ const { app, config, close, localAgentProviders } = createServer();
1562
+ const httpServer = app.listen(config.port, config.host, () => {
1563
+ console.log(`forgerelay listening on http://${config.host}:${config.port}/mcp`);
1564
+ console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
1565
+ console.log("auth: oauth owner-token flow required");
1566
+ console.log(`logging: ${config.logging.level} ${config.logging.format}`);
1567
+ console.log(`request logging: ${config.logging.requests ? "enabled" : "disabled"}`);
1568
+ console.log(`asset logging: ${config.logging.assets ? "enabled" : "disabled"}`);
1569
+ console.log(`trust proxy: ${config.logging.trustProxy ? "enabled" : "disabled"}`);
1570
+ const artifactDownloadStatus = !config.artifactsEnabled
1571
+ ? "disabled"
1572
+ : isArtifactDownloadSupportedPlatform()
1573
+ ? "enabled"
1574
+ : `unsupported on ${process.platform}`;
1575
+ console.log(`native artifact download: ${artifactDownloadStatus}`);
1576
+ if (config.subagents) {
1577
+ console.log(`subagent providers: ${formatLocalAgentProviderAvailabilitySummary(localAgentProviders)}`);
1578
+ }
1579
+ });
1580
+ let shuttingDown = false;
1581
+ const shutdown = async () => {
1582
+ if (shuttingDown)
1583
+ return;
1584
+ shuttingDown = true;
1585
+ await shutdownHttpServer(httpServer, close);
1586
+ process.exit(0);
1587
+ };
1588
+ const handleShutdown = () => {
1589
+ void shutdown().catch((error) => {
1590
+ console.error("forgerelay shutdown failed", error);
1591
+ process.exit(1);
1592
+ });
1593
+ };
1594
+ process.once("SIGINT", handleShutdown);
1595
+ process.once("SIGTERM", handleShutdown);
1596
+ }