@dev-anywhere/proxy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/assets/fonts/sarasa-fixed-sc/00c59ca448ea49756112128d6a43b127.woff2 +0 -0
  4. package/assets/fonts/sarasa-fixed-sc/011decfe3e67ab099af86e86a15e4f12.woff2 +0 -0
  5. package/assets/fonts/sarasa-fixed-sc/0133a2c4604dd809764dc749af72dc79.woff2 +0 -0
  6. package/assets/fonts/sarasa-fixed-sc/02928e8a3c2bf5ae6677bdf552664108.woff2 +0 -0
  7. package/assets/fonts/sarasa-fixed-sc/02eb3185bd0fd343bb2ae5a13260e805.woff2 +0 -0
  8. package/assets/fonts/sarasa-fixed-sc/04248a8ea5cd1204f875bc7661f1096c.woff2 +0 -0
  9. package/assets/fonts/sarasa-fixed-sc/059276218fbb9297f1668b1e37611796.woff2 +0 -0
  10. package/assets/fonts/sarasa-fixed-sc/05c6ae1e7580a1eff4be47341a155474.woff2 +0 -0
  11. package/assets/fonts/sarasa-fixed-sc/0681e7debe2dd44962f142b5c454676e.woff2 +0 -0
  12. package/assets/fonts/sarasa-fixed-sc/06ec078ae766d56e900c2837556eaa21.woff2 +0 -0
  13. package/assets/fonts/sarasa-fixed-sc/0765562d298aed694539cfcc4e26537c.woff2 +0 -0
  14. package/assets/fonts/sarasa-fixed-sc/090128a865f81baab82dfa2776ef9d39.woff2 +0 -0
  15. package/assets/fonts/sarasa-fixed-sc/098ff8830b5d779d8656e7ca93253c12.woff2 +0 -0
  16. package/assets/fonts/sarasa-fixed-sc/09b4eba4363eb15092534724123a78f2.woff2 +0 -0
  17. package/assets/fonts/sarasa-fixed-sc/0a32f2f6ed1a99ffa5e833e6891d1f82.woff2 +0 -0
  18. package/assets/fonts/sarasa-fixed-sc/0c1c080e1ac07d6a49dca531de9725a4.woff2 +0 -0
  19. package/assets/fonts/sarasa-fixed-sc/0d022c7c63996b8e5dc70b7fd7b0689d.woff2 +0 -0
  20. package/assets/fonts/sarasa-fixed-sc/0e7c7848d16895e68f79a55fd7b8c29d.woff2 +0 -0
  21. package/assets/fonts/sarasa-fixed-sc/1021bfe59ce4f905808dd5c3ea3fecc4.woff2 +0 -0
  22. package/assets/fonts/sarasa-fixed-sc/103c3dd7a5ee0d3be6cab3d04bd40c96.woff2 +0 -0
  23. package/assets/fonts/sarasa-fixed-sc/104a138cdd04cdbf3f786d2abeeed70a.woff2 +0 -0
  24. package/assets/fonts/sarasa-fixed-sc/105c39ea073d59b2e2f5e16ca68b2d71.woff2 +0 -0
  25. package/assets/fonts/sarasa-fixed-sc/1113c82b3ef6588e32cf20d7bd723355.woff2 +0 -0
  26. package/assets/fonts/sarasa-fixed-sc/1284d569e10290971bf3200bf4ddf94f.woff2 +0 -0
  27. package/assets/fonts/sarasa-fixed-sc/13401a392cffb9f2f7254e91bf44a106.woff2 +0 -0
  28. package/assets/fonts/sarasa-fixed-sc/1364ce313965ebf440ff2f0311cd4c83.woff2 +0 -0
  29. package/assets/fonts/sarasa-fixed-sc/1641d3bae07fd8d369349f78dd6c9c40.woff2 +0 -0
  30. package/assets/fonts/sarasa-fixed-sc/187edd679db5112bd2699bf1adad4a32.woff2 +0 -0
  31. package/assets/fonts/sarasa-fixed-sc/1918eaee7987c39db5198d637623a538.woff2 +0 -0
  32. package/assets/fonts/sarasa-fixed-sc/1aa2711726a364e3ea6fb7ca8a95d950.woff2 +0 -0
  33. package/assets/fonts/sarasa-fixed-sc/1ab01fa7bd43e3242f3c9d5b6f73dc90.woff2 +0 -0
  34. package/assets/fonts/sarasa-fixed-sc/1ae20f7adc1aa93e95c1842ce3b0c329.woff2 +0 -0
  35. package/assets/fonts/sarasa-fixed-sc/1c16e221d67a71a270f700e31e406230.woff2 +0 -0
  36. package/assets/fonts/sarasa-fixed-sc/1c40fcc59ed13b4b861844a8feb8205b.woff2 +0 -0
  37. package/assets/fonts/sarasa-fixed-sc/1d0e76f87701ab6daecb38b5aff5e35c.woff2 +0 -0
  38. package/assets/fonts/sarasa-fixed-sc/1d18a292fa9adaae7261de62ab3d5b83.woff2 +0 -0
  39. package/assets/fonts/sarasa-fixed-sc/1f99a7c91c71e6303282aa66c99c6670.woff2 +0 -0
  40. package/assets/fonts/sarasa-fixed-sc/20e0dab161488105b7fa7852470503b1.woff2 +0 -0
  41. package/assets/fonts/sarasa-fixed-sc/210b0efe54084a98b88028a6f3f35b0d.woff2 +0 -0
  42. package/assets/fonts/sarasa-fixed-sc/2116632ed1a3bdf3bf0106e1703c0781.woff2 +0 -0
  43. package/assets/fonts/sarasa-fixed-sc/22b3a91f654e334e2e7fbe3780f67764.woff2 +0 -0
  44. package/assets/fonts/sarasa-fixed-sc/24c5d226f02d96f03a7d050b0fdfd1d1.woff2 +0 -0
  45. package/assets/fonts/sarasa-fixed-sc/254d3a725f55d7111abbafa05858f2d6.woff2 +0 -0
  46. package/assets/fonts/sarasa-fixed-sc/26506a3ce2dbbfbfde40d11d65230272.woff2 +0 -0
  47. package/assets/fonts/sarasa-fixed-sc/268f0d094899c94260e840ac55320e20.woff2 +0 -0
  48. package/assets/fonts/sarasa-fixed-sc/277fc44cecacaf16cefce4681495bdce.woff2 +0 -0
  49. package/assets/fonts/sarasa-fixed-sc/27b773ac20ca5fa1bdd09d2109dfd5dc.woff2 +0 -0
  50. package/assets/fonts/sarasa-fixed-sc/2803a14ad17e6753704504009dad3333.woff2 +0 -0
  51. package/assets/fonts/sarasa-fixed-sc/2a0d78f9037bc2b5846bb4bcb42bfedc.woff2 +0 -0
  52. package/assets/fonts/sarasa-fixed-sc/2ac0bf1c4f9ad6e69bc01259f6e17058.woff2 +0 -0
  53. package/assets/fonts/sarasa-fixed-sc/2b01deb59e13f60e307d370b674b7b3b.woff2 +0 -0
  54. package/assets/fonts/sarasa-fixed-sc/2b74162a48979b6282e4893199f1346d.woff2 +0 -0
  55. package/assets/fonts/sarasa-fixed-sc/2d11bafb03690fc87494426325d90414.woff2 +0 -0
  56. package/assets/fonts/sarasa-fixed-sc/2dbf9030d7fa41019c78d5a717726261.woff2 +0 -0
  57. package/assets/fonts/sarasa-fixed-sc/30ce0c911f54ecd538247018d5289bb6.woff2 +0 -0
  58. package/assets/fonts/sarasa-fixed-sc/31bc5b0f3eb1e0f0845f37ae39f1a43e.woff2 +0 -0
  59. package/assets/fonts/sarasa-fixed-sc/31cdb9be17f46537a2958ba142c9765f.woff2 +0 -0
  60. package/assets/fonts/sarasa-fixed-sc/31d182ea6704bc50689becec8f417674.woff2 +0 -0
  61. package/assets/fonts/sarasa-fixed-sc/31f6abd51bf73062085ad82253fc302d.woff2 +0 -0
  62. package/assets/fonts/sarasa-fixed-sc/32d1abf03444175b162eadfef3a8e937.woff2 +0 -0
  63. package/assets/fonts/sarasa-fixed-sc/3397f69dc93262cf856a7c836fb33cb1.woff2 +0 -0
  64. package/assets/fonts/sarasa-fixed-sc/349035f271962ecacc5fa5cd56930c44.woff2 +0 -0
  65. package/assets/fonts/sarasa-fixed-sc/3525d0f8fb4e24d09024d35992e9095d.woff2 +0 -0
  66. package/assets/fonts/sarasa-fixed-sc/393876ac692b10f5ab7190590b062745.woff2 +0 -0
  67. package/assets/fonts/sarasa-fixed-sc/3bf37030a116e7377dcaf47356f08ef1.woff2 +0 -0
  68. package/assets/fonts/sarasa-fixed-sc/3c953d2e03b93436ee5c522ce3a27e1a.woff2 +0 -0
  69. package/assets/fonts/sarasa-fixed-sc/3fe795fd56b36a734a31613b1fed5441.woff2 +0 -0
  70. package/assets/fonts/sarasa-fixed-sc/401e69f55dc74b2e6fb774dcbba811da.woff2 +0 -0
  71. package/assets/fonts/sarasa-fixed-sc/404518f4b01a2fe38462bd04bfd76d35.woff2 +0 -0
  72. package/assets/fonts/sarasa-fixed-sc/404fe750213a60cbd07016ab43aacb71.woff2 +0 -0
  73. package/assets/fonts/sarasa-fixed-sc/41491053defe7688d8d53bfc313c6974.woff2 +0 -0
  74. package/assets/fonts/sarasa-fixed-sc/420121d03c1887873aa0eaa37a534e0c.woff2 +0 -0
  75. package/assets/fonts/sarasa-fixed-sc/42dffba83a406a85b208fba09e465c4d.woff2 +0 -0
  76. package/assets/fonts/sarasa-fixed-sc/44b74f10fbe44658d1acb6627b3a8b94.woff2 +0 -0
  77. package/assets/fonts/sarasa-fixed-sc/450be7e82a27dca52c8c1c582fcf182d.woff2 +0 -0
  78. package/assets/fonts/sarasa-fixed-sc/454ab2db1835c113f7b99f366a627247.woff2 +0 -0
  79. package/assets/fonts/sarasa-fixed-sc/45ec91cf9684c378adb99130acd6477e.woff2 +0 -0
  80. package/assets/fonts/sarasa-fixed-sc/4610b2980f41ab106bfe74b6951b34bb.woff2 +0 -0
  81. package/assets/fonts/sarasa-fixed-sc/46cad9f32908fa67609b51b74bc5693b.woff2 +0 -0
  82. package/assets/fonts/sarasa-fixed-sc/4a51964240b7e2c93d276dfef3794b5b.woff2 +0 -0
  83. package/assets/fonts/sarasa-fixed-sc/4b800716e356efb3e4b429085bcdb61a.woff2 +0 -0
  84. package/assets/fonts/sarasa-fixed-sc/4d8dad1e6c57ca3629d21230981cbb0e.woff2 +0 -0
  85. package/assets/fonts/sarasa-fixed-sc/4e625675fc1cb257e07a9ff2e331a9fe.woff2 +0 -0
  86. package/assets/fonts/sarasa-fixed-sc/4f2207b0342d6e2dd4290614e53cf0c5.woff2 +0 -0
  87. package/assets/fonts/sarasa-fixed-sc/50b0854263c0cf842979ce447020051a.woff2 +0 -0
  88. package/assets/fonts/sarasa-fixed-sc/552fe6d8e7144695a8fb38b8760295df.woff2 +0 -0
  89. package/assets/fonts/sarasa-fixed-sc/56175fa000590397d0ec4074c1f1e9f2.woff2 +0 -0
  90. package/assets/fonts/sarasa-fixed-sc/570f8c343327a818e4f0b0c4962ba4a1.woff2 +0 -0
  91. package/assets/fonts/sarasa-fixed-sc/579fbe9abeb15d0e96ee4770e6719719.woff2 +0 -0
  92. package/assets/fonts/sarasa-fixed-sc/57c37c54a9699166f03c0e6f871e6735.woff2 +0 -0
  93. package/assets/fonts/sarasa-fixed-sc/58e7c2324d8d292d58534d9f236f1552.woff2 +0 -0
  94. package/assets/fonts/sarasa-fixed-sc/59584acacc4c1852a6814f7e389ea191.woff2 +0 -0
  95. package/assets/fonts/sarasa-fixed-sc/598705446ef817283f04a709e974d473.woff2 +0 -0
  96. package/assets/fonts/sarasa-fixed-sc/59e93050018a5206bebf980e0f52fa78.woff2 +0 -0
  97. package/assets/fonts/sarasa-fixed-sc/5c129ed60734fd511937f3e72a07f936.woff2 +0 -0
  98. package/assets/fonts/sarasa-fixed-sc/5c40c780ef301baacb66efb9750e258b.woff2 +0 -0
  99. package/assets/fonts/sarasa-fixed-sc/5d1a982cb896e55ee96ada379c4a1d0c.woff2 +0 -0
  100. package/assets/fonts/sarasa-fixed-sc/5eba8579b88524026f6f6ec30cb81baa.woff2 +0 -0
  101. package/assets/fonts/sarasa-fixed-sc/601ad6234d43753b4e3efe8486b315c1.woff2 +0 -0
  102. package/assets/fonts/sarasa-fixed-sc/603b9e361dc9dd05250221356255a176.woff2 +0 -0
  103. package/assets/fonts/sarasa-fixed-sc/6133adaaf70a9467188b7e9e34039480.woff2 +0 -0
  104. package/assets/fonts/sarasa-fixed-sc/6158d9e8e90c6a553485c49e8adc991c.woff2 +0 -0
  105. package/assets/fonts/sarasa-fixed-sc/63c707f20b4af887788bfe53c6eee3ec.woff2 +0 -0
  106. package/assets/fonts/sarasa-fixed-sc/63c8475e9aa8acc0033fde9f792b8acf.woff2 +0 -0
  107. package/assets/fonts/sarasa-fixed-sc/64466c4274848cdee185275fb52660df.woff2 +0 -0
  108. package/assets/fonts/sarasa-fixed-sc/652d2da7b3c1dc0a70efdbb85e820055.woff2 +0 -0
  109. package/assets/fonts/sarasa-fixed-sc/6653b549c4d40a370724f6f32223e8a7.woff2 +0 -0
  110. package/assets/fonts/sarasa-fixed-sc/6734957b7e0bdd9f9e66cf24be7dc058.woff2 +0 -0
  111. package/assets/fonts/sarasa-fixed-sc/689a7d17cf5f82d7f148d660a21df759.woff2 +0 -0
  112. package/assets/fonts/sarasa-fixed-sc/6a864ec605e3b28cfdebf1d8fab2451e.woff2 +0 -0
  113. package/assets/fonts/sarasa-fixed-sc/6b4101e34138d999fd3ba9d2684ddebf.woff2 +0 -0
  114. package/assets/fonts/sarasa-fixed-sc/6b70494a68af284b2f21967109bf0311.woff2 +0 -0
  115. package/assets/fonts/sarasa-fixed-sc/6dde2f1140ce0a20159b7c1625bc3b45.woff2 +0 -0
  116. package/assets/fonts/sarasa-fixed-sc/6faa59cc7dff0a7751347b5baa8a5015.woff2 +0 -0
  117. package/assets/fonts/sarasa-fixed-sc/70364e9456886f13e2d5c9674ffb08f4.woff2 +0 -0
  118. package/assets/fonts/sarasa-fixed-sc/736f91321c58d7a9316ea9cddf4d83a7.woff2 +0 -0
  119. package/assets/fonts/sarasa-fixed-sc/7458762c6210651559b6019d5508ae2c.woff2 +0 -0
  120. package/assets/fonts/sarasa-fixed-sc/7460d9b012420e17a4bbfd2cb6796468.woff2 +0 -0
  121. package/assets/fonts/sarasa-fixed-sc/75a0c446c8bf8e4051d7cd65ed17e114.woff2 +0 -0
  122. package/assets/fonts/sarasa-fixed-sc/77ee59e7be1d66b59e87880e9c283491.woff2 +0 -0
  123. package/assets/fonts/sarasa-fixed-sc/782cda0bc3f24b5553f93c3d5b6fe581.woff2 +0 -0
  124. package/assets/fonts/sarasa-fixed-sc/78e84df88175c20e7f2562e4cdaad257.woff2 +0 -0
  125. package/assets/fonts/sarasa-fixed-sc/79e46c399f5e7383f853281b499269ee.woff2 +0 -0
  126. package/assets/fonts/sarasa-fixed-sc/7acff1baf69f6628c6372be0554191f1.woff2 +0 -0
  127. package/assets/fonts/sarasa-fixed-sc/7c5323aa737b291adacceecedac3e56a.woff2 +0 -0
  128. package/assets/fonts/sarasa-fixed-sc/7c9c9097a200464fd8ccec8eafc80178.woff2 +0 -0
  129. package/assets/fonts/sarasa-fixed-sc/7f77666d943dd01e947a2e35ffed945f.woff2 +0 -0
  130. package/assets/fonts/sarasa-fixed-sc/7f7e1463e29e6528378ed51589c4ebc0.woff2 +0 -0
  131. package/assets/fonts/sarasa-fixed-sc/80d4285084348d043a775221eb1c93b1.woff2 +0 -0
  132. package/assets/fonts/sarasa-fixed-sc/81258e5d1abdeafe44be69ad32f74c19.woff2 +0 -0
  133. package/assets/fonts/sarasa-fixed-sc/83a8da8e1de25f9eb39fe9d980e04a5d.woff2 +0 -0
  134. package/assets/fonts/sarasa-fixed-sc/83be0efd62f30539368d37cf7529f95c.woff2 +0 -0
  135. package/assets/fonts/sarasa-fixed-sc/84266b5a67707c745ce2c7101c559735.woff2 +0 -0
  136. package/assets/fonts/sarasa-fixed-sc/84905ac6e9d149bcacc34e2a71b82a67.woff2 +0 -0
  137. package/assets/fonts/sarasa-fixed-sc/852ae9183bf83dc2a2414290d3aa0236.woff2 +0 -0
  138. package/assets/fonts/sarasa-fixed-sc/85f5aa76f5f1e09b6852af36ec781960.woff2 +0 -0
  139. package/assets/fonts/sarasa-fixed-sc/860a1c6d20d4e631bd8e80735f5f3581.woff2 +0 -0
  140. package/assets/fonts/sarasa-fixed-sc/8afa5d1ab930027046ad09ec6349ab31.woff2 +0 -0
  141. package/assets/fonts/sarasa-fixed-sc/8b81739358772947619c9a58575c863a.woff2 +0 -0
  142. package/assets/fonts/sarasa-fixed-sc/8b9212f06a935902e9a4d8ad435c6a6e.woff2 +0 -0
  143. package/assets/fonts/sarasa-fixed-sc/8cc210fa91949bb44ea2bf607991b7bf.woff2 +0 -0
  144. package/assets/fonts/sarasa-fixed-sc/8f0b853b5728d58ac137870b3fd7ab02.woff2 +0 -0
  145. package/assets/fonts/sarasa-fixed-sc/8f0cdbad1758d5f68a48effbca6f74f8.woff2 +0 -0
  146. package/assets/fonts/sarasa-fixed-sc/8f5261cd877891595879479ee2440ae4.woff2 +0 -0
  147. package/assets/fonts/sarasa-fixed-sc/910a758ea12189f5a6a1e025fca33c5a.woff2 +0 -0
  148. package/assets/fonts/sarasa-fixed-sc/911993a058e817f1a231fbac27b3781c.woff2 +0 -0
  149. package/assets/fonts/sarasa-fixed-sc/91daedcfd8d0745222bf82c1fd310a33.woff2 +0 -0
  150. package/assets/fonts/sarasa-fixed-sc/943d6560828548692eeb4a533aa89494.woff2 +0 -0
  151. package/assets/fonts/sarasa-fixed-sc/945d0c206221572e76cc03fdd474685a.woff2 +0 -0
  152. package/assets/fonts/sarasa-fixed-sc/946411ab6f4f655273da613188359c06.woff2 +0 -0
  153. package/assets/fonts/sarasa-fixed-sc/9514ffa1f75fb1ad995392125c963b1b.woff2 +0 -0
  154. package/assets/fonts/sarasa-fixed-sc/971106de1248ba224fbfe50cf5c8a3dc.woff2 +0 -0
  155. package/assets/fonts/sarasa-fixed-sc/9714a586ded7b76747709b1fbe32c344.woff2 +0 -0
  156. package/assets/fonts/sarasa-fixed-sc/98b75b8dba788b62ef809337088ef030.woff2 +0 -0
  157. package/assets/fonts/sarasa-fixed-sc/98b7b436090e7874e2affbfd4ad28fd9.woff2 +0 -0
  158. package/assets/fonts/sarasa-fixed-sc/9a952ea6da219d92841b5503d43c746e.woff2 +0 -0
  159. package/assets/fonts/sarasa-fixed-sc/a0eabe8e58be580b4a5b0db87e3d282b.woff2 +0 -0
  160. package/assets/fonts/sarasa-fixed-sc/a11e9313ebf3147b1622c47e8cb1bacd.woff2 +0 -0
  161. package/assets/fonts/sarasa-fixed-sc/a2030c8eca8b7b75cf8d6fbbf29940cb.woff2 +0 -0
  162. package/assets/fonts/sarasa-fixed-sc/a2e0e75587d53e7e6c50da180ad76815.woff2 +0 -0
  163. package/assets/fonts/sarasa-fixed-sc/a3e96d6e1a2bdfe27d35140d1cb84600.woff2 +0 -0
  164. package/assets/fonts/sarasa-fixed-sc/a645858417844eaeb35eec60463a8758.woff2 +0 -0
  165. package/assets/fonts/sarasa-fixed-sc/a691a029b0ffe2f516dc0ba2680a7b24.woff2 +0 -0
  166. package/assets/fonts/sarasa-fixed-sc/a8731b06984003e15944af2045bac0ba.woff2 +0 -0
  167. package/assets/fonts/sarasa-fixed-sc/a8b5a37da8e22efc24e1d51e3dd133f2.woff2 +0 -0
  168. package/assets/fonts/sarasa-fixed-sc/aa4030d4e6a032ebd37ab080a87f0b15.woff2 +0 -0
  169. package/assets/fonts/sarasa-fixed-sc/ab0f237ab351bf258b3579011285bdb2.woff2 +0 -0
  170. package/assets/fonts/sarasa-fixed-sc/ac3a84416815f292543ac24c909b8f53.woff2 +0 -0
  171. package/assets/fonts/sarasa-fixed-sc/ac4188ae92c726da6cce789716706455.woff2 +0 -0
  172. package/assets/fonts/sarasa-fixed-sc/ac9e1d7b7d0e738c0965e0c37a171594.woff2 +0 -0
  173. package/assets/fonts/sarasa-fixed-sc/ad0efa49e97c104bb4f7d2a5d15eabef.woff2 +0 -0
  174. package/assets/fonts/sarasa-fixed-sc/ad3ac003a089fb00191c1e00aa56061e.woff2 +0 -0
  175. package/assets/fonts/sarasa-fixed-sc/ae481a90ce6dc650ffb8e777ab9fc089.woff2 +0 -0
  176. package/assets/fonts/sarasa-fixed-sc/aef606698752748d6b411ab230cd1760.woff2 +0 -0
  177. package/assets/fonts/sarasa-fixed-sc/aefc6f1da14ea5d5daf751fbf71b29a2.woff2 +0 -0
  178. package/assets/fonts/sarasa-fixed-sc/afe37b0740a4f9cc8f6a8aa5515a7826.woff2 +0 -0
  179. package/assets/fonts/sarasa-fixed-sc/b1a36f37842e3cd3981ee51b5d9b3edc.woff2 +0 -0
  180. package/assets/fonts/sarasa-fixed-sc/b3791b3fb0de0352fb1265cb02a021fb.woff2 +0 -0
  181. package/assets/fonts/sarasa-fixed-sc/b37e11130ac85f74432248a20b134622.woff2 +0 -0
  182. package/assets/fonts/sarasa-fixed-sc/b39422dfd00810613d15bd6572b05aa3.woff2 +0 -0
  183. package/assets/fonts/sarasa-fixed-sc/b3e514ad857c99211370e0bc572f776f.woff2 +0 -0
  184. package/assets/fonts/sarasa-fixed-sc/b4bd13b2c8086a5320644893d557b747.woff2 +0 -0
  185. package/assets/fonts/sarasa-fixed-sc/b6b23d001929bf0450f2ee16e9236f1f.woff2 +0 -0
  186. package/assets/fonts/sarasa-fixed-sc/b78cefc4f6a28eb5b5dd551dd873bcbd.woff2 +0 -0
  187. package/assets/fonts/sarasa-fixed-sc/b7e35bfc1e67312ea4f7399d87101681.woff2 +0 -0
  188. package/assets/fonts/sarasa-fixed-sc/b8493cdc783af55e9c50fc16c9b55bc3.woff2 +0 -0
  189. package/assets/fonts/sarasa-fixed-sc/b8e187dc66c9864eb348ee91951eacdf.woff2 +0 -0
  190. package/assets/fonts/sarasa-fixed-sc/bbf72353d229f1e5d6ccea65888451fe.woff2 +0 -0
  191. package/assets/fonts/sarasa-fixed-sc/bc6f1bd38107e5c152f4d568e75b3cf0.woff2 +0 -0
  192. package/assets/fonts/sarasa-fixed-sc/bc884e50f2cf284e61c6cc8274bb3cc5.woff2 +0 -0
  193. package/assets/fonts/sarasa-fixed-sc/bd0f4de744997828af2f5be5ec11428f.woff2 +0 -0
  194. package/assets/fonts/sarasa-fixed-sc/bf85faaa5f8efc2e1023f5e9504c0d4e.woff2 +0 -0
  195. package/assets/fonts/sarasa-fixed-sc/c509336eb0f6e35088064f15ddddc844.woff2 +0 -0
  196. package/assets/fonts/sarasa-fixed-sc/c5dd4097aac455d055badef188473371.woff2 +0 -0
  197. package/assets/fonts/sarasa-fixed-sc/c66f535c634eadaf34e0fe041672ae4b.woff2 +0 -0
  198. package/assets/fonts/sarasa-fixed-sc/c693a3ac340ea557c7ee39ddef0451a2.woff2 +0 -0
  199. package/assets/fonts/sarasa-fixed-sc/c8e0baa6e08346d410255ea827a8be27.woff2 +0 -0
  200. package/assets/fonts/sarasa-fixed-sc/ca8a49ee7846219a65d63ee1ec51c194.woff2 +0 -0
  201. package/assets/fonts/sarasa-fixed-sc/cd52152b8b48869f0fb1175983d9a505.woff2 +0 -0
  202. package/assets/fonts/sarasa-fixed-sc/cea8a7b71e661b383a7dc2f15cc2f47a.woff2 +0 -0
  203. package/assets/fonts/sarasa-fixed-sc/d2e284352932a735dda55727c0879153.woff2 +0 -0
  204. package/assets/fonts/sarasa-fixed-sc/d2fd9591ebfd0e779de7682d2745dbbb.woff2 +0 -0
  205. package/assets/fonts/sarasa-fixed-sc/d4a14f2651525d4affbbb753b6dbdf07.woff2 +0 -0
  206. package/assets/fonts/sarasa-fixed-sc/d5caf76d2720e294f4d0b1625f01ec9c.woff2 +0 -0
  207. package/assets/fonts/sarasa-fixed-sc/d5d463ae4788148f67a28de9ec4c1dca.woff2 +0 -0
  208. package/assets/fonts/sarasa-fixed-sc/d6e2c1e4c4b88bc14079e9c4a7549497.woff2 +0 -0
  209. package/assets/fonts/sarasa-fixed-sc/d70ea44ff65a30a175801cb104f83448.woff2 +0 -0
  210. package/assets/fonts/sarasa-fixed-sc/d750b314131793e148b4ebf50ffb91af.woff2 +0 -0
  211. package/assets/fonts/sarasa-fixed-sc/d7ae616c789f29be08782214e0288a0d.woff2 +0 -0
  212. package/assets/fonts/sarasa-fixed-sc/d8ae70630c929796cb0364e5202037e4.woff2 +0 -0
  213. package/assets/fonts/sarasa-fixed-sc/da5bdab3ae018a4c1207bd67ab854bc2.woff2 +0 -0
  214. package/assets/fonts/sarasa-fixed-sc/dab7b758f386eaae246f675ec7e23f0f.woff2 +0 -0
  215. package/assets/fonts/sarasa-fixed-sc/db1cc9bd868a6ff45d7bbe5b28d4b8d7.woff2 +0 -0
  216. package/assets/fonts/sarasa-fixed-sc/dc34cae5db8221efc393ea494597b1ec.woff2 +0 -0
  217. package/assets/fonts/sarasa-fixed-sc/dd32cf24e261a9aa261b7e196da33f2d.woff2 +0 -0
  218. package/assets/fonts/sarasa-fixed-sc/dda618cc791e2c20645ca5b7843c50dc.woff2 +0 -0
  219. package/assets/fonts/sarasa-fixed-sc/df9de632322bb3378c647420760aab61.woff2 +0 -0
  220. package/assets/fonts/sarasa-fixed-sc/e00c9c693f510281ead88131d09c18d2.woff2 +0 -0
  221. package/assets/fonts/sarasa-fixed-sc/e02d34cae25f04c920a31db2c80d1d8f.woff2 +0 -0
  222. package/assets/fonts/sarasa-fixed-sc/e22efb34fb5fe61f5b45504d76d20781.woff2 +0 -0
  223. package/assets/fonts/sarasa-fixed-sc/e25f5e7e1d5b88a59d981b5f6f669283.woff2 +0 -0
  224. package/assets/fonts/sarasa-fixed-sc/e3bb4be954796cb47ac6851ab724ecaf.woff2 +0 -0
  225. package/assets/fonts/sarasa-fixed-sc/e3bdeda10d9131256fad8417fd1da09f.woff2 +0 -0
  226. package/assets/fonts/sarasa-fixed-sc/e73e3950ff542ab97969b032c1f7241f.woff2 +0 -0
  227. package/assets/fonts/sarasa-fixed-sc/e78879cfa489561d3ec6eef16a4a0a4d.woff2 +0 -0
  228. package/assets/fonts/sarasa-fixed-sc/e811136959f0512a25a18acc520970b5.woff2 +0 -0
  229. package/assets/fonts/sarasa-fixed-sc/e9a7cf7d3d15fb2e50730469b07f2e39.woff2 +0 -0
  230. package/assets/fonts/sarasa-fixed-sc/eb514af63c1e9b14ccec7dee432b8edc.woff2 +0 -0
  231. package/assets/fonts/sarasa-fixed-sc/ed0b4344be5485ee0b0c207f28762fb8.woff2 +0 -0
  232. package/assets/fonts/sarasa-fixed-sc/edf89add9339773b26c1c2dd2244a179.woff2 +0 -0
  233. package/assets/fonts/sarasa-fixed-sc/ef8de64aca34834c6f110f4adc7515bd.woff2 +0 -0
  234. package/assets/fonts/sarasa-fixed-sc/f0438b1823af0a0de674c239fa03eaba.woff2 +0 -0
  235. package/assets/fonts/sarasa-fixed-sc/f0604027051cb387c2f85940dfff3755.woff2 +0 -0
  236. package/assets/fonts/sarasa-fixed-sc/f065b514c78eb2344c42e496865369a9.woff2 +0 -0
  237. package/assets/fonts/sarasa-fixed-sc/f0d97a6f8e1880316015bc2283d0f156.woff2 +0 -0
  238. package/assets/fonts/sarasa-fixed-sc/f1834c69e3074e71093d17a68b9d4d7d.woff2 +0 -0
  239. package/assets/fonts/sarasa-fixed-sc/f29b465128cba69d5e68235945cd3edd.woff2 +0 -0
  240. package/assets/fonts/sarasa-fixed-sc/f330fd41c2fbf0ae89bb98047c08d516.woff2 +0 -0
  241. package/assets/fonts/sarasa-fixed-sc/f383e78159d65c11032dc8b8d9c360f3.woff2 +0 -0
  242. package/assets/fonts/sarasa-fixed-sc/f647c059126fa5bf24355dcbfed09c4f.woff2 +0 -0
  243. package/assets/fonts/sarasa-fixed-sc/f6af810265a209ab307886171ded4113.woff2 +0 -0
  244. package/assets/fonts/sarasa-fixed-sc/f74844d2228da8984fe156b5b9c28acc.woff2 +0 -0
  245. package/assets/fonts/sarasa-fixed-sc/f7f2840050c367f26cfbc2b81c3a5fad.woff2 +0 -0
  246. package/assets/fonts/sarasa-fixed-sc/f85435b23bb50124e045f50cfe54dd3e.woff2 +0 -0
  247. package/assets/fonts/sarasa-fixed-sc/f865cd6ba4d22d8d61fce2a2d63be7dc.woff2 +0 -0
  248. package/assets/fonts/sarasa-fixed-sc/f8a66c1ab7db90820de697f664ea136d.woff2 +0 -0
  249. package/assets/fonts/sarasa-fixed-sc/fa6cf45901b333e63b3eabc045783c69.woff2 +0 -0
  250. package/assets/fonts/sarasa-fixed-sc/fa827d13030f9366a57edbfc22ce4336.woff2 +0 -0
  251. package/assets/fonts/sarasa-fixed-sc/fb6ac0e1252b3d366009d4f4ffeab4fc.woff2 +0 -0
  252. package/assets/fonts/sarasa-fixed-sc/fbb64faabca9f28743b2d0d5dc448e5f.woff2 +0 -0
  253. package/assets/fonts/sarasa-fixed-sc/fd299dbef627ab6738d61d4e4bac56e2.woff2 +0 -0
  254. package/assets/fonts/sarasa-fixed-sc/fd8cd455b632147c1758bfb9bd0ba42e.woff2 +0 -0
  255. package/assets/fonts/sarasa-fixed-sc/ff8cf784cc8ef060bf9d2dc7e698b451.woff2 +0 -0
  256. package/assets/fonts/sarasa-fixed-sc/result.css +20 -0
  257. package/dist/chunk-2Q3Z3ICU.js +155 -0
  258. package/dist/chunk-2Q3Z3ICU.js.map +1 -0
  259. package/dist/chunk-6O6JTF24.js +318 -0
  260. package/dist/chunk-6O6JTF24.js.map +1 -0
  261. package/dist/chunk-OO64L35C.js +34 -0
  262. package/dist/chunk-OO64L35C.js.map +1 -0
  263. package/dist/chunk-QJ5CQDK7.js +1038 -0
  264. package/dist/chunk-QJ5CQDK7.js.map +1 -0
  265. package/dist/chunk-UGFYGF3Y.js +142 -0
  266. package/dist/chunk-UGFYGF3Y.js.map +1 -0
  267. package/dist/chunk-ZUWAB67J.js +55 -0
  268. package/dist/chunk-ZUWAB67J.js.map +1 -0
  269. package/dist/index.js +279 -0
  270. package/dist/index.js.map +1 -0
  271. package/dist/serve.js +4375 -0
  272. package/dist/serve.js.map +1 -0
  273. package/dist/session-worker.js +357 -0
  274. package/dist/session-worker.js.map +1 -0
  275. package/dist/terminal-TTRA2VJY.js +671 -0
  276. package/dist/terminal-TTRA2VJY.js.map +1 -0
  277. package/package.json +67 -0
package/dist/serve.js ADDED
@@ -0,0 +1,4375 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ContentBlockDeltaSchema,
4
+ IGNORED_EVENT_TYPES,
5
+ KnownContentBlockSchema,
6
+ SeqCounter,
7
+ StreamJsonEventSchema
8
+ } from "./chunk-2Q3Z3ICU.js";
9
+ import {
10
+ createFSM,
11
+ defineFSM,
12
+ extractOscSequences,
13
+ extractOscSignals,
14
+ serviceLogger,
15
+ shouldReleaseApprovalWait,
16
+ stateAfterApprovalRelease
17
+ } from "./chunk-UGFYGF3Y.js";
18
+ import {
19
+ spawnScript
20
+ } from "./chunk-ZUWAB67J.js";
21
+ import {
22
+ CLAUDE_PROVIDER,
23
+ CODEX_PROVIDER,
24
+ detectAgentCliStatus
25
+ } from "./chunk-6O6JTF24.js";
26
+ import {
27
+ CONFIG_PATH,
28
+ ControlErrorCode,
29
+ DATA_DIR,
30
+ PID_PATH,
31
+ SESSIONS_PATH,
32
+ SOCK_PATH,
33
+ STOPPED_PATH,
34
+ SessionState,
35
+ buildMessage,
36
+ createIpcReader,
37
+ createWorkerReader,
38
+ serializeIpc,
39
+ serializeWorkerMsg,
40
+ sessionPaths,
41
+ tildify
42
+ } from "./chunk-QJ5CQDK7.js";
43
+
44
+ // src/serve.ts
45
+ import { createServer as createServer2 } from "net";
46
+ import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, rmSync as rmSync2 } from "fs";
47
+
48
+ // src/serve/session-manager.ts
49
+ import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "fs";
50
+ import { dirname } from "path";
51
+ import { nanoid } from "nanoid";
52
+ var PTY_TRANSITIONS = {
53
+ [SessionState.IDLE]: [
54
+ // claude 开始响应用户输入 → handlePtyData 首字节翻 working
55
+ SessionState.WORKING,
56
+ // provider hook 是语义事件,可能比 PTY 字节观察更早到达;PermissionRequest 可直接进入审批等待。
57
+ SessionState.WAITING_APPROVAL,
58
+ // 终态兜底;现阶段 terminated 走 terminateSession 直接删 map 不经 updateState,本边未被触发
59
+ SessionState.TERMINATED
60
+ ],
61
+ [SessionState.WORKING]: [
62
+ // 5s 静默且 currentPtyState === "working" → idle timer 推 turn_complete
63
+ SessionState.IDLE,
64
+ // claude 发 OSC 9 "needs your permission: <tool>" → handlePtyData 推 approval_wait
65
+ SessionState.WAITING_APPROVAL,
66
+ // 终态兜底
67
+ SessionState.TERMINATED
68
+ ],
69
+ [SessionState.WAITING_APPROVAL]: [
70
+ // 审批解除后 provider 可能继续工作,也可能直接结束本轮。
71
+ // 真实 Claude 拒绝工具审批后会直接发 turn_complete,因此 WAITING_APPROVAL -> IDLE 是合法边。
72
+ SessionState.WORKING,
73
+ SessionState.IDLE,
74
+ // 终态兜底
75
+ SessionState.TERMINATED
76
+ ],
77
+ // PTY 永不进入 ERROR;本行仅为满足 Record<SessionState,_> 枚举完整性保留
78
+ [SessionState.ERROR]: [SessionState.TERMINATED],
79
+ [SessionState.TERMINATED]: []
80
+ };
81
+ var JSON_TRANSITIONS = {
82
+ [SessionState.IDLE]: [
83
+ // 用户在 relay/web 端发消息 → onTurnStart,turn 开始
84
+ SessionState.WORKING,
85
+ // 空闲期观察通道失联(worker socket 死但 pid 仍在等)→ onChannelBroken
86
+ SessionState.ERROR,
87
+ // 终态兜底;同 PTY,当前不经 updateState
88
+ SessionState.TERMINATED
89
+ ],
90
+ [SessionState.WORKING]: [
91
+ // stream-json result event → onTurnResult,turn 结束
92
+ SessionState.IDLE,
93
+ // claude 发 control_request → onApprovalRequested,阻塞等审批
94
+ SessionState.WAITING_APPROVAL,
95
+ // turn 进行中通道失联 → onChannelBroken
96
+ SessionState.ERROR,
97
+ // 终态兜底
98
+ SessionState.TERMINATED
99
+ ],
100
+ [SessionState.WAITING_APPROVAL]: [
101
+ // 粒度丢失:审批解除后 claude 继续跑,proxy 观察不到中间的 WORKING 信号,
102
+ // 直到 result event 才感知 → onTurnResult 一次性从 WAITING_APPROVAL 跳到 IDLE。
103
+ // 因此不列 WAITING_APPROVAL → WORKING 这条边。
104
+ SessionState.IDLE,
105
+ // 审批死锁:control_response 写 worker stdin 失败 → onChannelBroken。
106
+ // 这是 ERROR 态最明确的落地场景,让 UI 能区分 "正在等用户决定" 和 "审批通道坏了"。
107
+ SessionState.ERROR,
108
+ // 终态兜底
109
+ SessionState.TERMINATED
110
+ ],
111
+ [SessionState.ERROR]: [
112
+ // 观察通道坏了之后只能 terminate,不回 IDLE/WORKING——恢复机制未实现
113
+ SessionState.TERMINATED
114
+ ],
115
+ [SessionState.TERMINATED]: []
116
+ };
117
+ var ptyFSM = defineFSM(PTY_TRANSITIONS);
118
+ var jsonFSM = defineFSM(JSON_TRANSITIONS);
119
+ function fsmForMode(mode) {
120
+ return mode === "pty" ? ptyFSM : jsonFSM;
121
+ }
122
+ function isProviderId(value) {
123
+ return value === "claude" || value === "codex";
124
+ }
125
+ var SessionManager = class {
126
+ sessions = /* @__PURE__ */ new Map();
127
+ reaperTimer = null;
128
+ persistPath;
129
+ reaperIntervalMs;
130
+ onSessionRemoved;
131
+ constructor(options) {
132
+ this.persistPath = options.persistPath;
133
+ this.reaperIntervalMs = options.reaperIntervalMs ?? 6e4;
134
+ this.onSessionRemoved = options.onSessionRemoved;
135
+ this.load();
136
+ }
137
+ createSession(mode, cwd, pid, name, id, provider = "claude", ptyOwner) {
138
+ const now = Date.now();
139
+ const info = {
140
+ id: id ?? nanoid(),
141
+ mode,
142
+ provider,
143
+ ...mode === "pty" && ptyOwner !== void 0 ? { ptyOwner } : {},
144
+ state: SessionState.IDLE,
145
+ createdAt: now,
146
+ updatedAt: now,
147
+ cwd,
148
+ pid,
149
+ ...name !== void 0 ? { name } : {}
150
+ };
151
+ this.sessions.set(info.id, info);
152
+ this.save();
153
+ serviceLogger.info({ sessionId: info.id, mode, provider, ptyOwner, name }, "Session created");
154
+ return info;
155
+ }
156
+ listSessions() {
157
+ return Array.from(this.sessions.values()).sort((a, b) => b.createdAt - a.createdAt);
158
+ }
159
+ getSession(id) {
160
+ return this.sessions.get(id);
161
+ }
162
+ updateState(id, newState) {
163
+ const session = this.sessions.get(id);
164
+ if (!session) {
165
+ throw new Error(`Session not found: ${id}`);
166
+ }
167
+ const oldState = session.state;
168
+ if (oldState === newState) return false;
169
+ const fsm = fsmForMode(session.mode);
170
+ if (!fsm.canTransition(oldState, newState)) {
171
+ const level = fsm.isAbsorbing(oldState) ? "debug" : "warn";
172
+ serviceLogger[level](
173
+ { sessionId: id, from: oldState, to: newState, mode: session.mode },
174
+ level === "debug" ? "State change after absorbing state (residual, likely race)" : "Invalid state transition rejected by FSM"
175
+ );
176
+ return false;
177
+ }
178
+ session.state = newState;
179
+ session.updatedAt = Date.now();
180
+ this.save();
181
+ serviceLogger.info({ sessionId: id, from: oldState, to: newState }, "Session state changed");
182
+ return true;
183
+ }
184
+ terminateSession(id, context) {
185
+ const session = this.sessions.get(id);
186
+ if (!session) {
187
+ return { success: false };
188
+ }
189
+ const pid = session.pid;
190
+ this.sessions.delete(id);
191
+ this.save();
192
+ serviceLogger.info({ sessionId: id, mode: session.mode, pid }, "Session terminated");
193
+ this.onSessionRemoved?.(id, context);
194
+ return { success: true, pid };
195
+ }
196
+ terminateAll() {
197
+ const pids = [];
198
+ const ids = Array.from(this.sessions.keys());
199
+ for (const id of ids) {
200
+ const session = this.sessions.get(id);
201
+ if (session.mode === "json" && session.pid !== void 0) {
202
+ pids.push(session.pid);
203
+ }
204
+ this.sessions.delete(id);
205
+ this.onSessionRemoved?.(id);
206
+ }
207
+ this.save();
208
+ return pids;
209
+ }
210
+ setClaudeSessionId(id, claudeSessionId) {
211
+ const session = this.sessions.get(id);
212
+ if (!session) {
213
+ throw new Error(`Session not found: ${id}`);
214
+ }
215
+ session.claudeSessionId = claudeSessionId;
216
+ this.save();
217
+ }
218
+ setPid(id, pid) {
219
+ const session = this.sessions.get(id);
220
+ if (!session) {
221
+ throw new Error(`Session not found: ${id}`);
222
+ }
223
+ session.pid = pid;
224
+ this.save();
225
+ }
226
+ startReaper(intervalMs = this.reaperIntervalMs) {
227
+ this.stopReaper();
228
+ this.reaperTimer = setInterval(() => this.reap(), intervalMs);
229
+ }
230
+ stopReaper() {
231
+ if (this.reaperTimer) {
232
+ clearInterval(this.reaperTimer);
233
+ this.reaperTimer = null;
234
+ }
235
+ }
236
+ reap() {
237
+ const toRemove = [];
238
+ for (const session of this.sessions.values()) {
239
+ if (session.mode === "json" && session.pid !== void 0 && session.state !== SessionState.TERMINATED) {
240
+ if (!this.isProcessAlive(session.pid)) {
241
+ toRemove.push({ id: session.id, reason: `JSON worker process ${session.pid} is dead` });
242
+ }
243
+ }
244
+ }
245
+ for (const { id, reason } of toRemove) {
246
+ serviceLogger.warn({ sessionId: id, reason }, "Reaping stale session");
247
+ this.terminateSession(id);
248
+ }
249
+ }
250
+ isProcessAlive(pid) {
251
+ try {
252
+ process.kill(pid, 0);
253
+ return true;
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ save() {
259
+ const dir = dirname(this.persistPath);
260
+ mkdirSync(dir, { recursive: true });
261
+ const persisted = Array.from(this.sessions.values()).map((s) => ({
262
+ id: s.id,
263
+ mode: s.mode,
264
+ provider: s.provider,
265
+ createdAt: s.createdAt,
266
+ updatedAt: s.updatedAt,
267
+ cwd: s.cwd,
268
+ pid: s.pid,
269
+ ...s.name !== void 0 ? { name: s.name } : {},
270
+ ...s.claudeSessionId !== void 0 ? { claudeSessionId: s.claudeSessionId } : {}
271
+ }));
272
+ const data = JSON.stringify(persisted, null, 2);
273
+ const tmpPath = this.persistPath + ".tmp";
274
+ writeFileSync(tmpPath, data, "utf-8");
275
+ renameSync(tmpPath, this.persistPath);
276
+ }
277
+ load() {
278
+ if (!existsSync(this.persistPath)) {
279
+ return;
280
+ }
281
+ const raw = readFileSync(this.persistPath, "utf-8");
282
+ let parsed;
283
+ try {
284
+ parsed = JSON.parse(raw);
285
+ } catch (err) {
286
+ throw new Error(`Failed to parse session persistence file at ${this.persistPath}`, {
287
+ cause: err
288
+ });
289
+ }
290
+ if (!Array.isArray(parsed)) {
291
+ throw new Error(
292
+ `Session persistence file has invalid format at ${this.persistPath}: expected array`
293
+ );
294
+ }
295
+ for (const item of parsed) {
296
+ if (item && typeof item === "object" && "state" in item) {
297
+ throw new Error(
298
+ `Session persistence file has invalid persisted state for session ${String(
299
+ item.id
300
+ )}`
301
+ );
302
+ }
303
+ const info = item;
304
+ if (!isProviderId(info.provider)) {
305
+ const sessionId = String(info.id);
306
+ this.onSessionRemoved?.(sessionId);
307
+ serviceLogger.warn(
308
+ { sessionId, provider: info.provider },
309
+ "Session persistence file has invalid provider; cleaning session"
310
+ );
311
+ continue;
312
+ }
313
+ if (info.mode === "pty") {
314
+ if (info.pid && this.isProcessAlive(info.pid)) {
315
+ serviceLogger.info(
316
+ { sessionId: info.id, pid: info.pid },
317
+ "PTY session skipped on load, terminal alive"
318
+ );
319
+ } else {
320
+ this.onSessionRemoved?.(info.id);
321
+ serviceLogger.info(
322
+ { sessionId: info.id, pid: info.pid },
323
+ "PTY session cleaned on load, terminal dead"
324
+ );
325
+ }
326
+ continue;
327
+ }
328
+ if (info.pid && this.isProcessAlive(info.pid)) {
329
+ this.sessions.set(info.id, { ...info, state: SessionState.IDLE });
330
+ } else {
331
+ this.onSessionRemoved?.(info.id);
332
+ serviceLogger.info(
333
+ { sessionId: info.id, pid: info.pid },
334
+ "JSON session cleaned on load, worker dead"
335
+ );
336
+ }
337
+ }
338
+ this.save();
339
+ if (this.sessions.size > 0) {
340
+ serviceLogger.info({ count: this.sessions.size }, "Sessions restored from persistence");
341
+ }
342
+ }
343
+ };
344
+
345
+ // src/serve/relay-connection.ts
346
+ import WebSocket from "ws";
347
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
348
+ import { homedir } from "os";
349
+ import { dirname as dirname2, join } from "path";
350
+ import { nanoid as nanoid2 } from "nanoid";
351
+ import { EventEmitter } from "events";
352
+
353
+ // src/serve/message-queue.ts
354
+ var MemoryMessageQueue = class {
355
+ items = [];
356
+ enqueue(raw) {
357
+ this.items.push(raw);
358
+ }
359
+ drain() {
360
+ const all = this.items;
361
+ this.items = [];
362
+ return all;
363
+ }
364
+ size() {
365
+ return this.items.length;
366
+ }
367
+ clear() {
368
+ this.items = [];
369
+ }
370
+ // 丢弃最旧消息,返回被丢弃的 raw 供 caller 做补偿(例如清理对应 pending 审批)
371
+ dropOldest() {
372
+ return this.items.shift() ?? null;
373
+ }
374
+ };
375
+
376
+ // src/serve/relay-connection.ts
377
+ var DEFAULT_PROXY_ID_PATH = join(homedir(), ".dev-anywhere", "proxy-id");
378
+ var MAX_BACKOFF_MS = 3e4;
379
+ var BASE_BACKOFF_MS = 1e3;
380
+ var MAX_QUEUE_SIZE = 1e4;
381
+ var RelayConnectionState = {
382
+ DISCONNECTED: "disconnected",
383
+ CONNECTING: "connecting",
384
+ REGISTERING: "registering",
385
+ SYNCED: "synced",
386
+ WAITING_RECONNECT: "waiting_reconnect",
387
+ CLOSED: "closed"
388
+ };
389
+ var RELAY_TRANSITIONS = {
390
+ [RelayConnectionState.DISCONNECTED]: [
391
+ RelayConnectionState.CONNECTING,
392
+ RelayConnectionState.CLOSED
393
+ ],
394
+ [RelayConnectionState.CONNECTING]: [
395
+ RelayConnectionState.REGISTERING,
396
+ RelayConnectionState.WAITING_RECONNECT,
397
+ RelayConnectionState.CLOSED
398
+ ],
399
+ [RelayConnectionState.REGISTERING]: [
400
+ RelayConnectionState.SYNCED,
401
+ RelayConnectionState.WAITING_RECONNECT,
402
+ RelayConnectionState.CLOSED
403
+ ],
404
+ [RelayConnectionState.SYNCED]: [
405
+ RelayConnectionState.WAITING_RECONNECT,
406
+ RelayConnectionState.CLOSED
407
+ ],
408
+ [RelayConnectionState.WAITING_RECONNECT]: [
409
+ RelayConnectionState.CONNECTING,
410
+ RelayConnectionState.CLOSED
411
+ ],
412
+ [RelayConnectionState.CLOSED]: []
413
+ };
414
+ var RelayConnection = class extends EventEmitter {
415
+ ws = null;
416
+ proxyId;
417
+ relayUrl;
418
+ queue = new MemoryMessageQueue();
419
+ reconnectAttempt = 0;
420
+ reconnectTimer = null;
421
+ fsm = createFSM({
422
+ initial: RelayConnectionState.DISCONNECTED,
423
+ transitions: RELAY_TRANSITIONS,
424
+ onTransition: (from, to) => serviceLogger.info({ from, to }, "RelayConnection state transition"),
425
+ onRejected: (from, to, isAbsorbing) => serviceLogger[isAbsorbing ? "debug" : "warn"](
426
+ { from, to },
427
+ isAbsorbing ? "Late event after absorbing state, ignored" : "Invalid relay connection transition rejected"
428
+ )
429
+ });
430
+ name;
431
+ token;
432
+ constructor(relayUrl, options) {
433
+ super();
434
+ this.relayUrl = relayUrl;
435
+ this.proxyId = this.loadOrCreateProxyId(options?.proxyIdPath ?? DEFAULT_PROXY_ID_PATH);
436
+ this.name = options?.name;
437
+ this.token = options?.token;
438
+ }
439
+ // 从文件读取或生成新的 proxyId,生成后持久化到文件
440
+ loadOrCreateProxyId(idPath) {
441
+ if (existsSync2(idPath)) {
442
+ const existing = readFileSync2(idPath, "utf-8").trim();
443
+ if (existing.length > 0) {
444
+ return existing;
445
+ }
446
+ }
447
+ const id = nanoid2(21);
448
+ const dir = dirname2(idPath);
449
+ if (!existsSync2(dir)) {
450
+ mkdirSync2(dir, { recursive: true });
451
+ }
452
+ writeFileSync2(idPath, id, "utf-8");
453
+ return id;
454
+ }
455
+ // 连接到 relay server
456
+ connect() {
457
+ if (!this.fsm.tryTransitionTo(RelayConnectionState.CONNECTING)) return;
458
+ this.doConnect();
459
+ }
460
+ // 实际建立 WebSocket 连接的内部方法
461
+ doConnect() {
462
+ try {
463
+ const base = this.relayUrl.replace(/\/$/, "") + "/proxy";
464
+ const url = this.token ? `${base}?token=${encodeURIComponent(this.token)}` : base;
465
+ this.ws = new WebSocket(url);
466
+ this.ws.on("open", () => {
467
+ if (!this.fsm.tryTransitionTo(RelayConnectionState.REGISTERING)) return;
468
+ serviceLogger.info(
469
+ { proxyId: this.proxyId, url: base, tokenSet: !!this.token },
470
+ "Connected to relay server"
471
+ );
472
+ this.ws.send(
473
+ JSON.stringify({
474
+ type: "proxy_register",
475
+ proxyId: this.proxyId,
476
+ ...this.name ? { name: this.name } : {}
477
+ })
478
+ );
479
+ });
480
+ this.ws.on("message", (data) => {
481
+ const raw = data.toString();
482
+ let msg;
483
+ try {
484
+ msg = JSON.parse(raw);
485
+ } catch (err) {
486
+ serviceLogger.warn({ error: String(err) }, "Non-JSON message from relay, dropped");
487
+ return;
488
+ }
489
+ if (msg.type === "proxy_register_response") {
490
+ serviceLogger.info({ status: msg.status }, "Received register response");
491
+ if (!this.fsm.tryTransitionTo(RelayConnectionState.SYNCED)) return;
492
+ this.reconnectAttempt = 0;
493
+ this.flushQueue();
494
+ this.emit("connected");
495
+ return;
496
+ }
497
+ this.emit("message", msg);
498
+ });
499
+ this.ws.on("close", () => {
500
+ this.ws = null;
501
+ if (this.fsm.current() !== RelayConnectionState.CLOSED) {
502
+ this.fsm.tryTransitionTo(RelayConnectionState.WAITING_RECONNECT);
503
+ serviceLogger.info("Relay connection closed unexpectedly");
504
+ this.emit("disconnected");
505
+ this.scheduleReconnect();
506
+ } else {
507
+ serviceLogger.info("Relay connection closed");
508
+ }
509
+ });
510
+ this.ws.on("error", (err) => {
511
+ serviceLogger.error({ error: String(err) }, "Relay connection error");
512
+ });
513
+ } catch (err) {
514
+ serviceLogger.error({ error: String(err) }, "Failed to create relay connection");
515
+ if (this.fsm.current() !== RelayConnectionState.CLOSED) {
516
+ this.fsm.tryTransitionTo(RelayConnectionState.WAITING_RECONNECT);
517
+ this.scheduleReconnect();
518
+ }
519
+ }
520
+ }
521
+ // 将队列中缓存的消息依次发送到 relay
522
+ flushQueue() {
523
+ for (const raw of this.queue.drain()) {
524
+ this.ws?.send(raw);
525
+ }
526
+ }
527
+ // 计算全抖动指数退避延迟并调度重连
528
+ scheduleReconnect() {
529
+ const backoff = Math.random() * Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * Math.pow(2, this.reconnectAttempt));
530
+ serviceLogger.info(
531
+ { attempt: this.reconnectAttempt + 1, backoffMs: Math.round(backoff) },
532
+ "Scheduling reconnect"
533
+ );
534
+ this.reconnectTimer = setTimeout(() => {
535
+ this.reconnectAttempt++;
536
+ if (!this.fsm.tryTransitionTo(RelayConnectionState.CONNECTING)) return;
537
+ this.doConnect();
538
+ }, backoff);
539
+ }
540
+ // 发送 MessageEnvelope 到 relay,离线时自动入队
541
+ sendEnvelope(envelope) {
542
+ const raw = JSON.stringify(envelope);
543
+ this.sendRaw(raw);
544
+ }
545
+ // 发送 binary PTY 帧到 relay,断线时直接丢弃不入队
546
+ sendBinary(data) {
547
+ if (this.fsm.current() === RelayConnectionState.SYNCED && this.ws?.readyState === WebSocket.OPEN) {
548
+ this.ws.send(data);
549
+ }
550
+ }
551
+ // 发送原始 JSON 字符串到 relay,根据 connectionState 决定直发、入队或丢弃
552
+ sendRaw(raw) {
553
+ if (this.fsm.current() === RelayConnectionState.SYNCED && this.ws?.readyState === WebSocket.OPEN) {
554
+ this.ws.send(raw);
555
+ } else if (this.fsm.current() === RelayConnectionState.CLOSED) {
556
+ serviceLogger.warn("Message discarded: connection is closed");
557
+ } else {
558
+ if (this.queue.size() >= MAX_QUEUE_SIZE) {
559
+ const dropped = this.queue.dropOldest();
560
+ serviceLogger.warn(
561
+ { maxSize: MAX_QUEUE_SIZE },
562
+ "Message queue overflow, oldest message dropped"
563
+ );
564
+ if (dropped !== null) this.emit("envelope_dropped", dropped);
565
+ }
566
+ this.queue.enqueue(raw);
567
+ serviceLogger.debug({ queueSize: this.queue.size() }, "Message queued during disconnect");
568
+ }
569
+ }
570
+ // 主动关闭连接,发送 proxy_disconnect 通知 relay 立即清理,不触发重连
571
+ close() {
572
+ if (this.fsm.is(RelayConnectionState.CLOSED)) return;
573
+ this.fsm.tryTransitionTo(RelayConnectionState.CLOSED);
574
+ if (this.reconnectTimer) {
575
+ clearTimeout(this.reconnectTimer);
576
+ this.reconnectTimer = null;
577
+ }
578
+ if (this.ws) {
579
+ if (this.ws.readyState === WebSocket.OPEN) {
580
+ this.ws.send(JSON.stringify({ type: "proxy_disconnect", proxyId: this.proxyId }));
581
+ }
582
+ this.ws.close();
583
+ this.ws = null;
584
+ }
585
+ }
586
+ // 获取当前 proxyId
587
+ getProxyId() {
588
+ return this.proxyId;
589
+ }
590
+ // 获取连接状态摘要,用于 CLI status 输出
591
+ getStatus() {
592
+ return {
593
+ connected: this.fsm.current() === RelayConnectionState.SYNCED,
594
+ connectionState: this.fsm.current(),
595
+ proxyId: this.proxyId,
596
+ reconnectAttempt: this.reconnectAttempt,
597
+ queueDepth: this.queue.size()
598
+ };
599
+ }
600
+ };
601
+
602
+ // src/common/config.ts
603
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
604
+ import { dirname as dirname3, isAbsolute } from "path";
605
+ function parsePort(value, source) {
606
+ if (!value) return void 0;
607
+ const port = Number(value);
608
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
609
+ throw new Error(`Invalid ${source}: expected TCP port 1-65535`);
610
+ }
611
+ return port;
612
+ }
613
+ function resolveFileConfig(fromFile, requestedEnv) {
614
+ if (!fromFile.envs) {
615
+ return {
616
+ envName: void 0,
617
+ envNameSource: fromFile.relayUrl || fromFile.relayToken || fromFile.hookPort ? "single" : "none",
618
+ config: fromFile
619
+ };
620
+ }
621
+ const envName = requestedEnv ?? fromFile.defaultEnv ?? "local";
622
+ const config = fromFile.envs[envName];
623
+ if (!config) {
624
+ const available = Object.keys(fromFile.envs).sort();
625
+ throw new Error(
626
+ `Unknown config env "${envName}". Available envs: ${available.length > 0 ? available.join(", ") : "(none)"}`
627
+ );
628
+ }
629
+ return {
630
+ envName,
631
+ envNameSource: requestedEnv ? "cli" : fromFile.defaultEnv ? "file" : "default",
632
+ config
633
+ };
634
+ }
635
+ function readConfigFile() {
636
+ if (!existsSync3(CONFIG_PATH)) return {};
637
+ return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
638
+ }
639
+ function agentCliField(provider) {
640
+ return provider === "claude" ? "claudeBin" : "codexBin";
641
+ }
642
+ function agentCliHistoryField(provider) {
643
+ return provider === "claude" ? "claudeBinHistory" : "codexBinHistory";
644
+ }
645
+ function validateAgentCliPath(path) {
646
+ const normalized = path.trim();
647
+ if (!normalized) throw new Error("\u8BF7\u8F93\u5165 CLI \u8DEF\u5F84");
648
+ if (!isAbsolute(normalized)) throw new Error("CLI \u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
649
+ return normalized;
650
+ }
651
+ function uniqueAbsolutePaths(paths) {
652
+ const seen = /* @__PURE__ */ new Set();
653
+ const result = [];
654
+ for (const path of paths) {
655
+ const normalized = path?.trim();
656
+ if (!normalized || !isAbsolute(normalized) || seen.has(normalized)) continue;
657
+ seen.add(normalized);
658
+ result.push(normalized);
659
+ }
660
+ return result;
661
+ }
662
+ function updateAgentCliPathInEnvConfig(config, provider, path) {
663
+ const field = agentCliField(provider);
664
+ const historyField = agentCliHistoryField(provider);
665
+ const history = uniqueAbsolutePaths([path, ...config[historyField] ?? []]).slice(0, 8);
666
+ return {
667
+ ...config,
668
+ [field]: path,
669
+ [historyField]: history
670
+ };
671
+ }
672
+ function loadConfig(options) {
673
+ let fromFile = {};
674
+ if (existsSync3(CONFIG_PATH)) {
675
+ try {
676
+ fromFile = readConfigFile();
677
+ } catch (err) {
678
+ serviceLogger.warn(
679
+ { path: CONFIG_PATH, err: err instanceof Error ? err.message : String(err) },
680
+ "Failed to parse config file, falling back to env-only"
681
+ );
682
+ }
683
+ } else {
684
+ serviceLogger.debug({ path: CONFIG_PATH }, "Config file not found, using env-only");
685
+ }
686
+ const resolved = resolveFileConfig(fromFile, options?.envName);
687
+ const hookPortFromFile = resolved.config.hookPort ?? fromFile.hookPort;
688
+ const claudeBinFromFile = resolved.config.claudeBin ?? fromFile.claudeBin;
689
+ const codexBinFromFile = resolved.config.codexBin ?? fromFile.codexBin;
690
+ const claudeBinHistory = [
691
+ ...resolved.config.claudeBinHistory ?? [],
692
+ ...fromFile.claudeBinHistory ?? []
693
+ ];
694
+ const codexBinHistory = [
695
+ ...resolved.config.codexBinHistory ?? [],
696
+ ...fromFile.codexBinHistory ?? []
697
+ ];
698
+ const claudeBin = process.env.CLAUDE_BIN ?? claudeBinFromFile;
699
+ const codexBin = process.env.CODEX_BIN ?? codexBinFromFile;
700
+ const config = {
701
+ envName: resolved.envName,
702
+ relayUrl: process.env.RELAY_URL ?? resolved.config.relayUrl,
703
+ relayToken: process.env.RELAY_PROXY_TOKEN ?? resolved.config.relayToken,
704
+ hookPort: parsePort(process.env.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT") ?? hookPortFromFile ?? 17654,
705
+ claudeBin,
706
+ codexBin,
707
+ agentCliSuggestions: {
708
+ claude: uniqueAbsolutePaths([process.env.CLAUDE_BIN, claudeBinFromFile, ...claudeBinHistory]),
709
+ codex: uniqueAbsolutePaths([process.env.CODEX_BIN, codexBinFromFile, ...codexBinHistory])
710
+ },
711
+ sources: {
712
+ envName: resolved.envNameSource,
713
+ relayUrl: process.env.RELAY_URL ? "env" : resolved.config.relayUrl ? "file" : "none",
714
+ relayToken: process.env.RELAY_PROXY_TOKEN ? "env" : resolved.config.relayToken ? "file" : "none",
715
+ hookPort: process.env.DEV_ANYWHERE_HOOK_PORT ? "env" : hookPortFromFile ? "file" : "default",
716
+ claudeBin: process.env.CLAUDE_BIN ? "env" : claudeBinFromFile ? "file" : "none",
717
+ codexBin: process.env.CODEX_BIN ? "env" : codexBinFromFile ? "file" : "none"
718
+ }
719
+ };
720
+ serviceLogger.info(
721
+ {
722
+ envName: config.envName ?? "(single)",
723
+ envNameSource: config.sources.envName,
724
+ relayUrl: config.relayUrl ?? "(unset)",
725
+ relayUrlSource: config.sources.relayUrl,
726
+ relayTokenSource: config.sources.relayToken,
727
+ hookPort: config.hookPort,
728
+ hookPortSource: config.sources.hookPort,
729
+ claudeBinSource: config.sources.claudeBin,
730
+ codexBinSource: config.sources.codexBin
731
+ },
732
+ "Config loaded"
733
+ );
734
+ return config;
735
+ }
736
+ function buildProviderEnv(config, baseEnv = process.env) {
737
+ return {
738
+ ...baseEnv,
739
+ ...config.claudeBin ? { CLAUDE_BIN: config.claudeBin } : {},
740
+ ...config.codexBin ? { CODEX_BIN: config.codexBin } : {}
741
+ };
742
+ }
743
+ function saveAgentCliPath(provider, path, options) {
744
+ const normalized = validateAgentCliPath(path);
745
+ const fromFile = readConfigFile();
746
+ const resolved = resolveFileConfig(fromFile, options?.envName);
747
+ if (fromFile.envs) {
748
+ const envName = resolved.envName ?? options?.envName ?? fromFile.defaultEnv ?? "local";
749
+ fromFile.envs[envName] = updateAgentCliPathInEnvConfig(
750
+ fromFile.envs[envName] ?? {},
751
+ provider,
752
+ normalized
753
+ );
754
+ } else {
755
+ Object.assign(fromFile, updateAgentCliPathInEnvConfig(fromFile, provider, normalized));
756
+ }
757
+ mkdirSync3(dirname3(CONFIG_PATH), { recursive: true });
758
+ writeFileSync3(CONFIG_PATH, `${JSON.stringify(fromFile, null, 2)}
759
+ `, "utf-8");
760
+ }
761
+
762
+ // src/serve/handlers/control-messages.ts
763
+ import { readdir as readdir2, mkdir } from "fs/promises";
764
+ import { join as join4, isAbsolute as isAbsolute2, normalize } from "path";
765
+
766
+ // src/serve/session-history.ts
767
+ import { readdir, stat, access } from "fs/promises";
768
+ import { createReadStream } from "fs";
769
+ import { join as join2 } from "path";
770
+ import { homedir as homedir2 } from "os";
771
+ import { createInterface } from "readline";
772
+ var claudeProjectsDir = () => join2(homedir2(), ".claude", "projects");
773
+ var codexSessionsDir = () => join2(homedir2(), ".codex", "sessions");
774
+ var UNTITLED_SESSION_TITLE = "\u672A\u547D\u540D\u4F1A\u8BDD";
775
+ var MAX_HISTORY_TITLE_LENGTH = 40;
776
+ var IGNORED_SLASH_COMMANDS = /* @__PURE__ */ new Set([
777
+ "/clear",
778
+ "/model",
779
+ "/compact",
780
+ "/help",
781
+ "/config",
782
+ "/logout"
783
+ ]);
784
+ var XMLISH_NOISE_PREFIXES = [
785
+ "environment",
786
+ "system",
787
+ "developer",
788
+ "assistant",
789
+ "user",
790
+ "tool",
791
+ "context"
792
+ ];
793
+ var INTERNAL_TITLE_PATTERNS = [
794
+ /^the following is the codex agent history\b/i,
795
+ /^codex agent history\b/i,
796
+ /^conversation summary\b/i
797
+ ];
798
+ async function scanSessionHistory() {
799
+ const entries = [...await scanClaudeSessionHistory(), ...await scanCodexSessionHistory()];
800
+ entries.sort((a, b) => b.updatedAt - a.updatedAt);
801
+ const seen = /* @__PURE__ */ new Set();
802
+ return entries.filter((e) => {
803
+ const key = `${e.provider}::${e.projectDir}::${e.title}`;
804
+ if (seen.has(key)) return false;
805
+ seen.add(key);
806
+ return true;
807
+ });
808
+ }
809
+ async function scanClaudeSessionHistory() {
810
+ const entries = [];
811
+ let projectDirs;
812
+ try {
813
+ projectDirs = await readdir(claudeProjectsDir());
814
+ } catch {
815
+ return [];
816
+ }
817
+ for (const encodedDir of projectDirs) {
818
+ const projectPath = join2(claudeProjectsDir(), encodedDir);
819
+ let files;
820
+ try {
821
+ files = await readdir(projectPath);
822
+ } catch {
823
+ continue;
824
+ }
825
+ for (const file of files) {
826
+ if (!file.endsWith(".jsonl")) continue;
827
+ const filePath = join2(projectPath, file);
828
+ try {
829
+ const fileStat = await stat(filePath);
830
+ const sessionId = file.replace(/\.jsonl$/, "");
831
+ const { title, cwd } = await extractTitleAndCwd(filePath);
832
+ entries.push({
833
+ id: sessionId,
834
+ title: title || UNTITLED_SESSION_TITLE,
835
+ projectDir: cwd || "/" + encodedDir.replace(/^-/, "").split("-").join("/"),
836
+ updatedAt: fileStat.mtimeMs,
837
+ provider: "claude"
838
+ });
839
+ } catch {
840
+ continue;
841
+ }
842
+ }
843
+ }
844
+ return entries;
845
+ }
846
+ async function scanCodexSessionHistory() {
847
+ const files = await collectJsonlFiles(codexSessionsDir());
848
+ const entries = [];
849
+ for (const filePath of files) {
850
+ try {
851
+ const fileStat = await stat(filePath);
852
+ const meta = await extractCodexTitleAndCwd(filePath);
853
+ if (!meta.id) continue;
854
+ entries.push({
855
+ id: meta.id,
856
+ title: meta.title || UNTITLED_SESSION_TITLE,
857
+ projectDir: meta.cwd || homedir2(),
858
+ updatedAt: fileStat.mtimeMs,
859
+ provider: "codex"
860
+ });
861
+ } catch {
862
+ continue;
863
+ }
864
+ }
865
+ return entries;
866
+ }
867
+ async function readSessionMessages(claudeSessionId) {
868
+ let projectDirs;
869
+ try {
870
+ projectDirs = await readdir(claudeProjectsDir());
871
+ } catch {
872
+ return [];
873
+ }
874
+ for (const encodedDir of projectDirs) {
875
+ const filePath = join2(claudeProjectsDir(), encodedDir, `${claudeSessionId}.jsonl`);
876
+ try {
877
+ await access(filePath);
878
+ } catch {
879
+ continue;
880
+ }
881
+ const messages = [];
882
+ return new Promise((resolve) => {
883
+ const rl = createInterface({
884
+ input: createReadStream(filePath, { encoding: "utf-8" }),
885
+ crlfDelay: Infinity
886
+ });
887
+ rl.on("line", (line) => {
888
+ if (!line.trim()) return;
889
+ try {
890
+ const obj = JSON.parse(line);
891
+ if (obj.type === "user") {
892
+ if (obj.isMeta) return;
893
+ const text = extractConversationText(obj.message);
894
+ if (!text) return;
895
+ const ts = typeof obj.timestamp === "string" ? new Date(obj.timestamp).getTime() : void 0;
896
+ messages.push({ role: "user", text, timestamp: ts });
897
+ } else if (obj.type === "assistant") {
898
+ const text = extractConversationText(obj.message);
899
+ const ts = typeof obj.timestamp === "string" ? new Date(obj.timestamp).getTime() : void 0;
900
+ if (text) messages.push({ role: "assistant", text, timestamp: ts });
901
+ }
902
+ } catch {
903
+ }
904
+ });
905
+ rl.on("close", () => resolve(messages));
906
+ rl.on("error", () => resolve(messages));
907
+ });
908
+ }
909
+ return [];
910
+ }
911
+ function collapseWhitespace(text) {
912
+ return text.replace(/\s+/g, " ").trim();
913
+ }
914
+ function truncateTitle(text) {
915
+ const chars = Array.from(text);
916
+ return chars.length > MAX_HISTORY_TITLE_LENGTH ? `${chars.slice(0, MAX_HISTORY_TITLE_LENGTH).join("")}...` : text;
917
+ }
918
+ function isXmlishNoise(text) {
919
+ const match = text.match(/^<([A-Za-z][\w:-]*)\b/);
920
+ if (!match) return false;
921
+ const tag = match[1].toLowerCase();
922
+ return XMLISH_NOISE_PREFIXES.some((prefix) => tag === prefix || tag.startsWith(`${prefix}_`));
923
+ }
924
+ function normalizeHistoryTitle(raw) {
925
+ if (!raw) return null;
926
+ const text = collapseWhitespace(raw);
927
+ if (text.length < 2) return null;
928
+ if (text.startsWith("<") || isXmlishNoise(text)) return null;
929
+ if (INTERNAL_TITLE_PATTERNS.some((pattern) => pattern.test(text))) return null;
930
+ const slashCommand = text.match(/^\/\S+/)?.[0];
931
+ if (slashCommand && IGNORED_SLASH_COMMANDS.has(slashCommand)) return null;
932
+ return truncateTitle(text);
933
+ }
934
+ function extractSlashCommand(text) {
935
+ const nameMatch = text.match(/<command-name>([^<]+)<\/command-name>/);
936
+ if (!nameMatch) return null;
937
+ const argsMatch = text.match(/<command-args>([^<]+)<\/command-args>/);
938
+ const args = argsMatch ? argsMatch[1].trim() : "";
939
+ return normalizeHistoryTitle(args ? `${nameMatch[1]} ${args}` : nameMatch[1]);
940
+ }
941
+ function extractMessageText(msg) {
942
+ if (typeof msg === "string") {
943
+ const cmd = extractSlashCommand(msg);
944
+ if (cmd) return cmd;
945
+ return normalizeHistoryTitle(msg);
946
+ }
947
+ if (msg && typeof msg === "object" && "content" in msg) {
948
+ const content = msg.content;
949
+ if (typeof content === "string") {
950
+ const cmd = extractSlashCommand(content);
951
+ if (cmd) return cmd;
952
+ return normalizeHistoryTitle(content);
953
+ }
954
+ if (Array.isArray(content)) {
955
+ const texts = content.filter(
956
+ (b) => b.type === "text" && typeof b.text === "string"
957
+ ).map((b) => b.text);
958
+ const joined = texts.join("\n").trim();
959
+ return normalizeHistoryTitle(joined);
960
+ }
961
+ }
962
+ if (Array.isArray(msg)) {
963
+ const texts = msg.filter(
964
+ (b) => b.type === "text" && typeof b.text === "string"
965
+ ).map((b) => b.text);
966
+ const joined = texts.join("\n").trim();
967
+ return normalizeHistoryTitle(joined);
968
+ }
969
+ return null;
970
+ }
971
+ function normalizeConversationText(text) {
972
+ const trimmed = text.trim();
973
+ if (!trimmed) return null;
974
+ return trimmed;
975
+ }
976
+ function extractConversationText(msg) {
977
+ if (typeof msg === "string") {
978
+ const cmd = extractSlashCommand(msg);
979
+ if (cmd) return cmd;
980
+ return normalizeConversationText(msg);
981
+ }
982
+ if (msg && typeof msg === "object" && "content" in msg) {
983
+ const content = msg.content;
984
+ if (typeof content === "string") {
985
+ const cmd = extractSlashCommand(content);
986
+ if (cmd) return cmd;
987
+ return normalizeConversationText(content);
988
+ }
989
+ if (Array.isArray(content)) {
990
+ const texts = content.filter(
991
+ (b) => b.type === "text" && typeof b.text === "string"
992
+ ).map((b) => b.text);
993
+ return normalizeConversationText(texts.join("\n"));
994
+ }
995
+ }
996
+ if (Array.isArray(msg)) {
997
+ const texts = msg.filter(
998
+ (b) => b.type === "text" && typeof b.text === "string"
999
+ ).map((b) => b.text);
1000
+ return normalizeConversationText(texts.join("\n"));
1001
+ }
1002
+ return null;
1003
+ }
1004
+ async function extractTitleAndCwd(filePath) {
1005
+ return new Promise((resolve) => {
1006
+ const rl = createInterface({
1007
+ input: createReadStream(filePath, { encoding: "utf-8" }),
1008
+ crlfDelay: Infinity
1009
+ });
1010
+ let resolved = false;
1011
+ let cwd = null;
1012
+ let title = null;
1013
+ rl.on("line", (line) => {
1014
+ if (resolved) return;
1015
+ if (!line.trim()) return;
1016
+ try {
1017
+ const obj = JSON.parse(line);
1018
+ if (!cwd && typeof obj.cwd === "string") {
1019
+ cwd = obj.cwd;
1020
+ }
1021
+ if (!title && obj.type === "user" && !obj.isMeta) {
1022
+ const text = extractMessageText(obj.message);
1023
+ if (text) title = text;
1024
+ }
1025
+ if (cwd && title) {
1026
+ resolved = true;
1027
+ rl.close();
1028
+ }
1029
+ } catch {
1030
+ }
1031
+ });
1032
+ rl.on("close", () => {
1033
+ if (!resolved) resolve({ title, cwd });
1034
+ else resolve({ title, cwd });
1035
+ });
1036
+ rl.on("error", () => resolve({ title, cwd }));
1037
+ });
1038
+ }
1039
+ async function collectJsonlFiles(root) {
1040
+ let entries;
1041
+ try {
1042
+ entries = await readdir(root, { withFileTypes: true });
1043
+ } catch {
1044
+ return [];
1045
+ }
1046
+ const files = [];
1047
+ for (const entry of entries) {
1048
+ const child = join2(root, entry.name);
1049
+ if (entry.isDirectory()) {
1050
+ files.push(...await collectJsonlFiles(child));
1051
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
1052
+ files.push(child);
1053
+ }
1054
+ }
1055
+ return files;
1056
+ }
1057
+ async function extractCodexTitleAndCwd(filePath) {
1058
+ return new Promise((resolve) => {
1059
+ const rl = createInterface({
1060
+ input: createReadStream(filePath, { encoding: "utf-8" }),
1061
+ crlfDelay: Infinity
1062
+ });
1063
+ let id = null;
1064
+ let cwd = null;
1065
+ let title = null;
1066
+ rl.on("line", (line) => {
1067
+ if (!line.trim()) return;
1068
+ try {
1069
+ const obj = JSON.parse(line);
1070
+ if (obj.type === "session_meta" && obj.payload) {
1071
+ if (!id && typeof obj.payload.id === "string") id = obj.payload.id;
1072
+ if (!cwd && typeof obj.payload.cwd === "string") cwd = obj.payload.cwd;
1073
+ }
1074
+ if (!title && obj.type === "response_item") {
1075
+ const text = extractCodexUserText(obj.payload);
1076
+ if (text) title = text;
1077
+ }
1078
+ if (id && cwd && title) rl.close();
1079
+ } catch {
1080
+ }
1081
+ });
1082
+ rl.on("close", () => resolve({ id, title, cwd }));
1083
+ rl.on("error", () => resolve({ id, title, cwd }));
1084
+ });
1085
+ }
1086
+ function extractCodexUserText(payload) {
1087
+ if (!payload || typeof payload !== "object") return null;
1088
+ const item = payload;
1089
+ if (item.type !== "message" || item.role !== "user") return null;
1090
+ if (typeof item.content === "string") return normalizeHistoryTitle(item.content);
1091
+ if (!Array.isArray(item.content)) return null;
1092
+ const texts = item.content.map((block) => {
1093
+ if (!block || typeof block !== "object") return "";
1094
+ const typed = block;
1095
+ return typed.type === "input_text" && typeof typed.text === "string" ? typed.text : "";
1096
+ }).filter(Boolean);
1097
+ const joined = texts.join("\n").trim();
1098
+ return normalizeHistoryTitle(joined);
1099
+ }
1100
+
1101
+ // src/serve/command-discovery.ts
1102
+ import { readdirSync, readFileSync as readFileSync4 } from "fs";
1103
+ import { homedir as homedir3 } from "os";
1104
+ import { join as join3 } from "path";
1105
+ var REPL_BUILTINS = [
1106
+ { name: "/compact", description: "Compact conversation history", source: "builtin" },
1107
+ { name: "/status", description: "Show session status", source: "builtin" },
1108
+ { name: "/cost", description: "Show token usage and cost", source: "builtin" },
1109
+ { name: "/clear", description: "Clear conversation history", source: "builtin" },
1110
+ {
1111
+ name: "/model",
1112
+ description: "Switch AI model",
1113
+ argumentHint: "model name (e.g., Haiku, Sonnet)",
1114
+ source: "builtin"
1115
+ },
1116
+ { name: "/help", description: "Show available commands", source: "builtin" },
1117
+ { name: "/memory", description: "Edit CLAUDE.md memory", source: "builtin" },
1118
+ { name: "/review", description: "Review diff of changes", source: "builtin" },
1119
+ { name: "/vim", description: "Enter vim mode", source: "builtin" },
1120
+ { name: "/terminal-setup", description: "Configure terminal integration", source: "builtin" },
1121
+ { name: "/permissions", description: "View and manage permissions", source: "builtin" },
1122
+ { name: "/allowed-tools", description: "View allowed tools", source: "builtin" },
1123
+ {
1124
+ name: "/add-dir",
1125
+ description: "Add working directory",
1126
+ argumentHint: "directory path",
1127
+ source: "builtin"
1128
+ },
1129
+ { name: "/init", description: "Initialize CLAUDE.md in project", source: "builtin" },
1130
+ { name: "/listen", description: "Listen for multi-turn responses", source: "builtin" },
1131
+ { name: "/pr-comments", description: "View PR comments", source: "builtin" },
1132
+ { name: "/release-notes", description: "Generate release notes", source: "builtin" },
1133
+ { name: "/ide", description: "Open IDE integration", source: "builtin" }
1134
+ ];
1135
+ var COMMAND_BLACKLIST = /* @__PURE__ */ new Set([
1136
+ "/login",
1137
+ "/logout",
1138
+ "/config",
1139
+ "/plugin",
1140
+ "/mcp",
1141
+ "/install",
1142
+ "/setup-token",
1143
+ "/doctor",
1144
+ "/update",
1145
+ "/upgrade",
1146
+ "/memory",
1147
+ "/vim",
1148
+ "/terminal-setup",
1149
+ "/permissions",
1150
+ "/allowed-tools",
1151
+ "/ide",
1152
+ "/listen"
1153
+ ]);
1154
+ function parseSkillFrontmatter(content) {
1155
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1156
+ if (!match) return {};
1157
+ const yaml = match[1];
1158
+ const result = {};
1159
+ const nameMatch = yaml.match(/^name:\s*(.+)$/m);
1160
+ if (nameMatch) result.name = nameMatch[1].trim();
1161
+ const descMatch = yaml.match(/^description:\s*(.+)$/m);
1162
+ if (descMatch) result.description = descMatch[1].trim();
1163
+ const hintMatch = yaml.match(/^argument-hint:\s*(.+)$/m);
1164
+ if (hintMatch) result.argumentHint = hintMatch[1].trim();
1165
+ return result;
1166
+ }
1167
+ function scanSkillsDir(dirPath, source) {
1168
+ let entries;
1169
+ try {
1170
+ entries = readdirSync(dirPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1171
+ } catch {
1172
+ return [];
1173
+ }
1174
+ const commands = [];
1175
+ for (const name of entries) {
1176
+ const skillPath = join3(dirPath, name, "SKILL.md");
1177
+ try {
1178
+ const content = readFileSync4(skillPath, "utf-8");
1179
+ const parsed = parseSkillFrontmatter(content);
1180
+ commands.push({
1181
+ name: `/${parsed.name ?? name}`,
1182
+ description: parsed.description ?? "",
1183
+ argumentHint: parsed.argumentHint,
1184
+ source
1185
+ });
1186
+ } catch {
1187
+ }
1188
+ }
1189
+ return commands;
1190
+ }
1191
+ function scanCommandsDir(dirPath, source) {
1192
+ let entries;
1193
+ try {
1194
+ entries = readdirSync(dirPath).filter((f) => f.endsWith(".md"));
1195
+ } catch {
1196
+ return [];
1197
+ }
1198
+ const commands = [];
1199
+ for (const filename of entries) {
1200
+ const cmdName = filename.replace(/\.md$/, "");
1201
+ try {
1202
+ const content = readFileSync4(join3(dirPath, filename), "utf-8");
1203
+ const firstLine = content.split("\n")[0].trim();
1204
+ commands.push({
1205
+ name: `/${cmdName}`,
1206
+ description: firstLine,
1207
+ source
1208
+ });
1209
+ } catch {
1210
+ commands.push({
1211
+ name: `/${cmdName}`,
1212
+ description: "",
1213
+ source
1214
+ });
1215
+ }
1216
+ }
1217
+ return commands;
1218
+ }
1219
+ function scanPluginDirs(homeDir) {
1220
+ const pluginCacheDir = join3(homeDir, ".claude", "plugins", "cache");
1221
+ let pluginNames;
1222
+ try {
1223
+ pluginNames = readdirSync(pluginCacheDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1224
+ } catch {
1225
+ return [];
1226
+ }
1227
+ const commands = [];
1228
+ for (const pluginName of pluginNames) {
1229
+ const pluginDir = join3(pluginCacheDir, pluginName);
1230
+ const skillCmds = scanSkillsDir(join3(pluginDir, "skills"), "plugin-skill");
1231
+ const cmdCmds = scanCommandsDir(join3(pluginDir, "commands"), "plugin-command");
1232
+ commands.push(...skillCmds, ...cmdCmds);
1233
+ }
1234
+ return commands;
1235
+ }
1236
+ async function discoverCommands(workDir, options) {
1237
+ const homeDir = options?.homeDir ?? homedir3();
1238
+ const builtins = REPL_BUILTINS.filter((c) => !COMMAND_BLACKLIST.has(c.name));
1239
+ const userSkills = scanSkillsDir(join3(homeDir, ".claude", "skills"), "user-skill");
1240
+ const projectSkills = scanSkillsDir(join3(workDir, ".claude", "skills"), "project-skill");
1241
+ const userCommands = scanCommandsDir(join3(homeDir, ".claude", "commands"), "user-command");
1242
+ const projectCommands = scanCommandsDir(join3(workDir, ".claude", "commands"), "project-command");
1243
+ const pluginCommands = scanPluginDirs(homeDir);
1244
+ const commandMap = /* @__PURE__ */ new Map();
1245
+ for (const cmd of builtins) commandMap.set(cmd.name, cmd);
1246
+ for (const cmd of pluginCommands) commandMap.set(cmd.name, cmd);
1247
+ for (const cmd of userSkills) commandMap.set(cmd.name, cmd);
1248
+ for (const cmd of userCommands) commandMap.set(cmd.name, cmd);
1249
+ for (const cmd of projectSkills) commandMap.set(cmd.name, cmd);
1250
+ for (const cmd of projectCommands) commandMap.set(cmd.name, cmd);
1251
+ const result = [];
1252
+ for (const cmd of commandMap.values()) {
1253
+ if (!COMMAND_BLACKLIST.has(cmd.name)) {
1254
+ result.push(cmd);
1255
+ }
1256
+ }
1257
+ return result;
1258
+ }
1259
+
1260
+ // src/serve/path-errors.ts
1261
+ function getFsErrorCode(err) {
1262
+ return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
1263
+ }
1264
+ function classifyPathError(err) {
1265
+ switch (getFsErrorCode(err)) {
1266
+ case "ENOENT":
1267
+ return ControlErrorCode.PATH_NOT_FOUND;
1268
+ case "ENOTDIR":
1269
+ return ControlErrorCode.PATH_NOT_DIRECTORY;
1270
+ case "EACCES":
1271
+ case "EPERM":
1272
+ return ControlErrorCode.PATH_ACCESS_DENIED;
1273
+ default:
1274
+ return ControlErrorCode.UNKNOWN;
1275
+ }
1276
+ }
1277
+
1278
+ // src/serve/handlers/control-messages.ts
1279
+ var COMMAND_REFRESH_MS = 6 * 60 * 60 * 1e3;
1280
+ function isPathSafe(path) {
1281
+ if (!isAbsolute2(path)) return false;
1282
+ const normalized = normalize(path);
1283
+ if (normalized.includes("..")) return false;
1284
+ return true;
1285
+ }
1286
+ var HIDDEN_ENTRY_NAMES = /* @__PURE__ */ new Set(["node_modules"]);
1287
+ function isPickerVisible(name) {
1288
+ return !name.startsWith(".") && !HIDDEN_ENTRY_NAMES.has(name);
1289
+ }
1290
+ function sortEntries(a, b) {
1291
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1292
+ return a.name.localeCompare(b.name);
1293
+ }
1294
+ async function scanDir(dirPath) {
1295
+ const entries = await readdir2(dirPath, { withFileTypes: true });
1296
+ return entries.filter((e) => isPickerVisible(e.name)).map((e) => ({ name: e.name, isDir: e.isDirectory() })).sort(sortEntries);
1297
+ }
1298
+ async function getFileTree(rootPath) {
1299
+ const groups = [];
1300
+ let rootEntries;
1301
+ try {
1302
+ rootEntries = await scanDir(rootPath);
1303
+ } catch {
1304
+ return groups;
1305
+ }
1306
+ groups.push({ path: rootPath, entries: rootEntries });
1307
+ for (const sub of rootEntries) {
1308
+ if (!sub.isDir) continue;
1309
+ const subPath = join4(rootPath, sub.name);
1310
+ try {
1311
+ const subEntries = await scanDir(subPath);
1312
+ groups.push({ path: subPath, entries: subEntries });
1313
+ } catch {
1314
+ }
1315
+ }
1316
+ return groups;
1317
+ }
1318
+ function createControlMessageHandlers(send, sessionManager) {
1319
+ const sessionResources = /* @__PURE__ */ new Map();
1320
+ function getResources(sessionId) {
1321
+ let res = sessionResources.get(sessionId);
1322
+ if (!res) {
1323
+ res = {};
1324
+ sessionResources.set(sessionId, res);
1325
+ }
1326
+ return res;
1327
+ }
1328
+ function scheduleCommandRefresh(sessionId, workDir) {
1329
+ const resources = getResources(sessionId);
1330
+ if (resources.commandRefreshTimer) {
1331
+ clearInterval(resources.commandRefreshTimer);
1332
+ }
1333
+ resources.commandRefreshTimer = setInterval(async () => {
1334
+ try {
1335
+ const commands = await discoverCommands(workDir);
1336
+ send(
1337
+ JSON.stringify({
1338
+ type: "command_list_push",
1339
+ commands
1340
+ })
1341
+ );
1342
+ serviceLogger.debug({ sessionId, count: commands.length }, "Command list refreshed");
1343
+ } catch (err) {
1344
+ serviceLogger.warn({ sessionId, error: String(err) }, "Command refresh failed");
1345
+ }
1346
+ }, COMMAND_REFRESH_MS);
1347
+ }
1348
+ return {
1349
+ async handleDirListRequest(msg) {
1350
+ if (!isPathSafe(msg.path)) {
1351
+ send(
1352
+ JSON.stringify({
1353
+ type: "dir_list_response",
1354
+ requestId: msg.requestId,
1355
+ path: msg.path,
1356
+ entries: [],
1357
+ errorCode: ControlErrorCode.INVALID_PATH,
1358
+ error: "Invalid path: must be absolute and must not contain path traversal"
1359
+ })
1360
+ );
1361
+ serviceLogger.warn({ path: msg.path }, "Rejected dir_list_request: unsafe path");
1362
+ return;
1363
+ }
1364
+ try {
1365
+ const entries = await scanDir(msg.path);
1366
+ send(
1367
+ JSON.stringify({
1368
+ type: "dir_list_response",
1369
+ requestId: msg.requestId,
1370
+ path: msg.path,
1371
+ entries
1372
+ })
1373
+ );
1374
+ serviceLogger.debug({ path: msg.path, count: entries.length }, "Dir list response sent");
1375
+ } catch (err) {
1376
+ send(
1377
+ JSON.stringify({
1378
+ type: "dir_list_response",
1379
+ requestId: msg.requestId,
1380
+ path: msg.path,
1381
+ entries: [],
1382
+ errorCode: classifyPathError(err),
1383
+ error: String(err)
1384
+ })
1385
+ );
1386
+ serviceLogger.warn({ path: msg.path, error: String(err) }, "Dir list request failed");
1387
+ }
1388
+ },
1389
+ async handleDirCreateRequest(msg) {
1390
+ if (!isPathSafe(msg.path)) {
1391
+ send(
1392
+ JSON.stringify({
1393
+ type: "dir_create_response",
1394
+ requestId: msg.requestId,
1395
+ path: msg.path,
1396
+ success: false,
1397
+ errorCode: ControlErrorCode.INVALID_PATH,
1398
+ error: "Invalid path: must be absolute and must not contain path traversal"
1399
+ })
1400
+ );
1401
+ serviceLogger.warn({ path: msg.path }, "Rejected dir_create_request: unsafe path");
1402
+ return;
1403
+ }
1404
+ try {
1405
+ await mkdir(msg.path, { recursive: true });
1406
+ send(
1407
+ JSON.stringify({
1408
+ type: "dir_create_response",
1409
+ requestId: msg.requestId,
1410
+ path: msg.path,
1411
+ success: true
1412
+ })
1413
+ );
1414
+ serviceLogger.info({ path: msg.path }, "Directory created");
1415
+ } catch (err) {
1416
+ send(
1417
+ JSON.stringify({
1418
+ type: "dir_create_response",
1419
+ requestId: msg.requestId,
1420
+ path: msg.path,
1421
+ success: false,
1422
+ errorCode: classifyPathError(err),
1423
+ error: String(err)
1424
+ })
1425
+ );
1426
+ serviceLogger.warn({ path: msg.path, error: String(err) }, "Dir create failed");
1427
+ }
1428
+ },
1429
+ async handleSessionHistoryRequest(msg) {
1430
+ try {
1431
+ const sessions = await scanSessionHistory();
1432
+ send(
1433
+ JSON.stringify({
1434
+ type: "session_history_response",
1435
+ requestId: msg.requestId,
1436
+ sessions
1437
+ })
1438
+ );
1439
+ serviceLogger.debug({ count: sessions.length }, "Session history response sent");
1440
+ } catch (err) {
1441
+ send(
1442
+ JSON.stringify({
1443
+ type: "session_history_response",
1444
+ requestId: msg.requestId,
1445
+ sessions: []
1446
+ })
1447
+ );
1448
+ serviceLogger.warn({ error: String(err) }, "Session history scan failed");
1449
+ }
1450
+ },
1451
+ async handleSessionResourcesRequest(msg) {
1452
+ getResources(msg.sessionId).fileTreeWorkDir = msg.workDir;
1453
+ scheduleCommandRefresh(msg.sessionId, msg.workDir);
1454
+ const [commandsResult, groupsResult] = await Promise.allSettled([
1455
+ discoverCommands(msg.workDir),
1456
+ getFileTree(msg.workDir)
1457
+ ]);
1458
+ const commands = commandsResult.status === "fulfilled" ? commandsResult.value : [];
1459
+ const groups = groupsResult.status === "fulfilled" ? groupsResult.value : [];
1460
+ const failedReason = commandsResult.status === "rejected" ? commandsResult.reason : groupsResult.status === "rejected" ? groupsResult.reason : void 0;
1461
+ send(
1462
+ JSON.stringify({
1463
+ type: "session_resources_response",
1464
+ requestId: msg.requestId,
1465
+ sessionId: msg.sessionId,
1466
+ commands,
1467
+ groups,
1468
+ ...failedReason ? {
1469
+ errorCode: classifyPathError(failedReason),
1470
+ error: String(failedReason)
1471
+ } : {}
1472
+ })
1473
+ );
1474
+ serviceLogger.info(
1475
+ { sessionId: msg.sessionId, commandCount: commands.length, groupCount: groups.length },
1476
+ "Session resources snapshot sent"
1477
+ );
1478
+ },
1479
+ async pushCommandList(sessionId, workDir) {
1480
+ try {
1481
+ const commands = await discoverCommands(workDir);
1482
+ send(
1483
+ JSON.stringify({
1484
+ type: "command_list_push",
1485
+ commands
1486
+ })
1487
+ );
1488
+ serviceLogger.info({ sessionId, count: commands.length, workDir }, "Command list pushed");
1489
+ } catch (err) {
1490
+ serviceLogger.warn({ sessionId, error: String(err) }, "Command discovery failed");
1491
+ }
1492
+ scheduleCommandRefresh(sessionId, workDir);
1493
+ },
1494
+ async pushFileTree(sessionId, workDir) {
1495
+ const resources = getResources(sessionId);
1496
+ resources.fileTreeWorkDir = workDir;
1497
+ try {
1498
+ const groups = await getFileTree(workDir);
1499
+ send(
1500
+ JSON.stringify({
1501
+ type: "file_tree_push",
1502
+ groups
1503
+ })
1504
+ );
1505
+ serviceLogger.debug(
1506
+ { sessionId, path: workDir, groupCount: groups.length },
1507
+ "File tree pushed"
1508
+ );
1509
+ } catch (err) {
1510
+ serviceLogger.warn({ sessionId, error: String(err) }, "File tree push failed");
1511
+ }
1512
+ },
1513
+ // relay 重连时同步 session 列表并重新推送控制数据
1514
+ async reinitializeOnReconnect() {
1515
+ const activeSessions = sessionManager.listSessions().filter((s) => s.state !== "terminated");
1516
+ if (activeSessions.length > 0) {
1517
+ send(
1518
+ JSON.stringify({
1519
+ type: "session_sync",
1520
+ sessions: activeSessions.map((s) => ({
1521
+ id: s.id,
1522
+ mode: s.mode,
1523
+ provider: s.provider,
1524
+ ...s.ptyOwner !== void 0 ? { ptyOwner: s.ptyOwner } : {},
1525
+ state: s.state
1526
+ }))
1527
+ })
1528
+ );
1529
+ serviceLogger.info({ count: activeSessions.length }, "Session list synced to relay");
1530
+ }
1531
+ for (const session of activeSessions) {
1532
+ const resources = sessionResources.get(session.id);
1533
+ const workDir = resources?.fileTreeWorkDir;
1534
+ if (workDir) {
1535
+ try {
1536
+ const commands = await discoverCommands(workDir);
1537
+ send(
1538
+ JSON.stringify({
1539
+ type: "command_list_push",
1540
+ commands
1541
+ })
1542
+ );
1543
+ const groups = await getFileTree(workDir);
1544
+ send(
1545
+ JSON.stringify({
1546
+ type: "file_tree_push",
1547
+ groups
1548
+ })
1549
+ );
1550
+ serviceLogger.info(
1551
+ { sessionId: session.id },
1552
+ "Reinitialized control data after reconnect"
1553
+ );
1554
+ } catch (err) {
1555
+ serviceLogger.warn(
1556
+ { sessionId: session.id, error: String(err) },
1557
+ "Reinitialize failed"
1558
+ );
1559
+ }
1560
+ }
1561
+ }
1562
+ },
1563
+ cleanup(sessionId) {
1564
+ const resources = sessionResources.get(sessionId);
1565
+ if (resources) {
1566
+ if (resources.commandRefreshTimer) {
1567
+ clearInterval(resources.commandRefreshTimer);
1568
+ }
1569
+ sessionResources.delete(sessionId);
1570
+ }
1571
+ }
1572
+ };
1573
+ }
1574
+
1575
+ // src/serve/worker-registry.ts
1576
+ import { connect } from "net";
1577
+ import { unlinkSync, existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
1578
+ var WorkerRegistry = class {
1579
+ constructor(deps) {
1580
+ this.deps = deps;
1581
+ deps.relayConnection.on("envelope_dropped", (raw) => this.onEnvelopeDropped(raw));
1582
+ }
1583
+ deps;
1584
+ sockets = /* @__PURE__ */ new Map();
1585
+ children = /* @__PURE__ */ new Map();
1586
+ // 记录哪些 session 是 spawn 时带 --stream-delta 的;forwardEvent 据此决定是否跳过 aggregated 去重
1587
+ streamDeltaSessions = /* @__PURE__ */ new Set();
1588
+ onEnvelopeDropped(raw) {
1589
+ let parsed;
1590
+ try {
1591
+ parsed = JSON.parse(raw);
1592
+ } catch {
1593
+ return;
1594
+ }
1595
+ if (!parsed || typeof parsed !== "object" || parsed.type !== "tool_use_request") {
1596
+ return;
1597
+ }
1598
+ const envelope = parsed;
1599
+ const sessionId = typeof envelope.sessionId === "string" ? envelope.sessionId : null;
1600
+ const requestId = envelope.payload && typeof envelope.payload.toolId === "string" ? envelope.payload.toolId : null;
1601
+ if (!sessionId || !requestId) return;
1602
+ if (!this.deps.permissionBroker.resolve(requestId, {
1603
+ behavior: "deny",
1604
+ message: "Approval request was dropped due to relay queue overflow."
1605
+ })) {
1606
+ return;
1607
+ }
1608
+ serviceLogger.warn(
1609
+ { sessionId, requestId },
1610
+ "Tool approval request lost to relay queue overflow, denying worker"
1611
+ );
1612
+ }
1613
+ spawn(sessionId, options) {
1614
+ const paths = sessionPaths(sessionId);
1615
+ const args = [sessionId, paths.workerSock];
1616
+ if (options?.cwd) args.push("--cwd", options.cwd);
1617
+ if (options?.resumeSessionId) args.push("--resume", options.resumeSessionId);
1618
+ args.push("--permission-mode", options?.permissionMode ?? "default");
1619
+ if (options?.streamDelta) {
1620
+ args.push("--stream-delta");
1621
+ this.streamDeltaSessions.add(sessionId);
1622
+ }
1623
+ if (options?.hook) {
1624
+ args.push(
1625
+ "--hook-provider",
1626
+ options.hook.provider,
1627
+ "--hook-url",
1628
+ options.hook.hookUrl,
1629
+ "--hook-marker",
1630
+ options.hook.marker
1631
+ );
1632
+ }
1633
+ args.push("--");
1634
+ const providerEnv = this.deps.getProviderEnv();
1635
+ const child = spawnScript(new URL("../session-worker", import.meta.url), args, {
1636
+ logger: serviceLogger,
1637
+ env: options?.hook ? { ...providerEnv, DEV_ANYWHERE_HOOK_TOKEN: options.hook.token } : providerEnv
1638
+ });
1639
+ const workerPid = child.pid;
1640
+ this.children.set(sessionId, child);
1641
+ serviceLogger.info(
1642
+ { sessionId, workerPid, cwd: options?.cwd, resume: options?.resumeSessionId },
1643
+ "Worker process spawned"
1644
+ );
1645
+ return workerPid;
1646
+ }
1647
+ connect(sessionId, sockPath) {
1648
+ return new Promise((resolve) => {
1649
+ const sock = connect(sockPath);
1650
+ sock.on("connect", () => {
1651
+ this.sockets.set(sessionId, sock);
1652
+ createWorkerReader(sock, (msg) => this.handleWorkerMessage(sessionId, msg));
1653
+ sock.on("close", () => this.onDisconnect(sessionId));
1654
+ sock.on("error", () => this.onDisconnect(sessionId));
1655
+ resolve(sock);
1656
+ });
1657
+ sock.on("error", () => resolve(null));
1658
+ });
1659
+ }
1660
+ // 枚举 DATA_DIR 下所有 session 目录,尝试连接存活的 worker.sock;失败则清理 stale socket。
1661
+ async reconnectAll() {
1662
+ if (!existsSync4(DATA_DIR)) return;
1663
+ const dirs = readdirSync2(DATA_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
1664
+ for (const dir of dirs) {
1665
+ const sessionId = dir.name;
1666
+ const paths = sessionPaths(sessionId);
1667
+ if (!existsSync4(paths.workerSock)) continue;
1668
+ const sock = await this.connect(sessionId, paths.workerSock);
1669
+ if (sock) {
1670
+ if (!this.deps.sessionManager.getSession(sessionId)) {
1671
+ serviceLogger.warn(
1672
+ { sessionId },
1673
+ "Orphaned worker found without session data, terminating"
1674
+ );
1675
+ sock.end();
1676
+ this.sockets.delete(sessionId);
1677
+ continue;
1678
+ }
1679
+ serviceLogger.info({ sessionId }, "Reconnected to existing worker");
1680
+ } else {
1681
+ try {
1682
+ unlinkSync(paths.workerSock);
1683
+ } catch {
1684
+ }
1685
+ serviceLogger.info({ sessionId }, "Cleaned up stale worker socket");
1686
+ }
1687
+ }
1688
+ }
1689
+ has(sessionId) {
1690
+ return this.sockets.has(sessionId);
1691
+ }
1692
+ delete(sessionId) {
1693
+ this.children.delete(sessionId);
1694
+ this.sockets.delete(sessionId);
1695
+ this.streamDeltaSessions.delete(sessionId);
1696
+ }
1697
+ terminateProcess(sessionId, signal = "SIGTERM") {
1698
+ const child = this.children.get(sessionId);
1699
+ const sock = this.sockets.get(sessionId);
1700
+ sock?.destroy();
1701
+ this.sockets.delete(sessionId);
1702
+ this.streamDeltaSessions.delete(sessionId);
1703
+ this.children.delete(sessionId);
1704
+ if (!child || child.killed) return false;
1705
+ return child.kill(signal);
1706
+ }
1707
+ // 向指定 session 的 worker 写 WorkerMessage;socket 缺失或不可写返回 false 由 caller 决定日志。
1708
+ send(sessionId, msg) {
1709
+ const sock = this.sockets.get(sessionId);
1710
+ if (!sock?.writable) return false;
1711
+ sock.write(serializeWorkerMsg(msg));
1712
+ return true;
1713
+ }
1714
+ destroyAll() {
1715
+ for (const [, ws] of this.sockets) {
1716
+ ws.destroy();
1717
+ }
1718
+ this.sockets.clear();
1719
+ }
1720
+ handleWorkerMessage(sessionId, msg) {
1721
+ switch (msg.type) {
1722
+ case "worker_ready":
1723
+ serviceLogger.info({ sessionId, pid: msg.pid }, "Worker ready");
1724
+ break;
1725
+ case "worker_event":
1726
+ try {
1727
+ this.forwardEvent(sessionId, msg.seq, msg.event);
1728
+ } catch (err) {
1729
+ serviceLogger.debug(
1730
+ { sessionId, error: String(err) },
1731
+ "Failed to forward event to relay"
1732
+ );
1733
+ }
1734
+ serviceLogger.debug({ sessionId, eventType: msg.event.type }, "JSON session event");
1735
+ break;
1736
+ case "worker_exit":
1737
+ this.deps.sessionManager.terminateSession(sessionId);
1738
+ this.delete(sessionId);
1739
+ serviceLogger.info({ sessionId, exitCode: msg.code }, "JSON session exited");
1740
+ break;
1741
+ case "worker_approval_request":
1742
+ this.forwardApprovalRequest(sessionId, msg);
1743
+ break;
1744
+ case "worker_claude_session_id":
1745
+ this.deps.sessionManager.setClaudeSessionId(sessionId, msg.sessionId);
1746
+ serviceLogger.info(
1747
+ { sessionId, claudeSessionId: msg.sessionId },
1748
+ "Claude session ID captured"
1749
+ );
1750
+ break;
1751
+ }
1752
+ }
1753
+ // worker 连接断开或异常时的统一清理入口。仅记录一份,不再区分 close vs error 语义。
1754
+ onDisconnect(sessionId) {
1755
+ this.sockets.delete(sessionId);
1756
+ this.deps.permissionBroker.cleanupSession(sessionId, "Worker disconnected");
1757
+ }
1758
+ // 对齐 Claude CLI stream-json 输出,按 type 分发:
1759
+ // stream_event.content_block_delta → 增量 text/thinking envelope(仅 streamDelta 会话产生)
1760
+ // assistant.content[].text → assistant_message envelope(streamDelta 下跳过,避免重复)
1761
+ // assistant.content[].thinking → thinking envelope(streamDelta 下跳过)
1762
+ // assistant.content[].tool_use → assistant_tool_use envelope
1763
+ // user.content[].tool_result → tool_result envelope
1764
+ // result → turn_result control + 会话状态回 IDLE
1765
+ // system/rate_limit_event/其他 → 静默忽略
1766
+ // schema 未识别的 event/block 以 warn 暴露,作为 Claude CLI 协议变化的 runtime canary。
1767
+ forwardEvent(sessionId, seq, event) {
1768
+ const relay = this.deps.relayConnection;
1769
+ const parsed = StreamJsonEventSchema.safeParse(event);
1770
+ if (!parsed.success) {
1771
+ const rawType = typeof event.type === "string" ? event.type : "<missing>";
1772
+ if (IGNORED_EVENT_TYPES.has(rawType)) {
1773
+ serviceLogger.debug({ sessionId, type: rawType }, "Dropped ignored stream-json event");
1774
+ return;
1775
+ }
1776
+ serviceLogger.warn(
1777
+ { sessionId, type: rawType, issues: parsed.error.issues.slice(0, 3) },
1778
+ "Unknown stream-json event type; Claude CLI schema may have changed"
1779
+ );
1780
+ return;
1781
+ }
1782
+ const ev = parsed.data;
1783
+ const isStreamDeltaSession = this.streamDeltaSessions.has(sessionId);
1784
+ if (ev.type === "stream_event") {
1785
+ const delta = ContentBlockDeltaSchema.safeParse(ev.event);
1786
+ if (!delta.success) return;
1787
+ const d = delta.data.delta;
1788
+ if (d.type === "text_delta" && d.text) {
1789
+ relay.sendEnvelope(
1790
+ buildMessage(
1791
+ "assistant_message",
1792
+ sessionId,
1793
+ seq,
1794
+ { text: d.text, isPartial: true },
1795
+ "proxy"
1796
+ )
1797
+ );
1798
+ } else if (d.type === "thinking_delta" && d.thinking) {
1799
+ relay.sendEnvelope(buildMessage("thinking", sessionId, seq, { text: d.thinking }, "proxy"));
1800
+ }
1801
+ return;
1802
+ }
1803
+ if (ev.type === "assistant") {
1804
+ for (const raw of ev.message.content) {
1805
+ const blockParse = KnownContentBlockSchema.safeParse(raw);
1806
+ if (!blockParse.success) {
1807
+ const rawType = raw && typeof raw === "object" ? raw.type : void 0;
1808
+ serviceLogger.warn(
1809
+ { sessionId, seq, blockType: rawType ?? "<missing>" },
1810
+ "Unknown assistant content block; Claude CLI schema may have changed"
1811
+ );
1812
+ continue;
1813
+ }
1814
+ const block = blockParse.data;
1815
+ if (block.type === "text") {
1816
+ if (!isStreamDeltaSession && block.text) {
1817
+ relay.sendEnvelope(
1818
+ buildMessage(
1819
+ "assistant_message",
1820
+ sessionId,
1821
+ seq,
1822
+ { text: block.text, isPartial: true },
1823
+ "proxy"
1824
+ )
1825
+ );
1826
+ }
1827
+ } else if (block.type === "thinking") {
1828
+ if (!isStreamDeltaSession && block.thinking) {
1829
+ relay.sendEnvelope(
1830
+ buildMessage("thinking", sessionId, seq, { text: block.thinking }, "proxy")
1831
+ );
1832
+ }
1833
+ } else if (block.type === "tool_use") {
1834
+ relay.sendEnvelope(
1835
+ buildMessage(
1836
+ "assistant_tool_use",
1837
+ sessionId,
1838
+ seq,
1839
+ { toolName: block.name, toolId: block.id, parameters: block.input },
1840
+ "proxy"
1841
+ )
1842
+ );
1843
+ }
1844
+ }
1845
+ return;
1846
+ }
1847
+ if (ev.type === "user") {
1848
+ for (const raw of ev.message.content) {
1849
+ const blockParse = KnownContentBlockSchema.safeParse(raw);
1850
+ if (!blockParse.success) continue;
1851
+ const block = blockParse.data;
1852
+ if (block.type !== "tool_result") continue;
1853
+ relay.sendEnvelope(
1854
+ buildMessage(
1855
+ "tool_result",
1856
+ sessionId,
1857
+ seq,
1858
+ { toolId: block.tool_use_id, result: block.content, isError: block.is_error ?? false },
1859
+ "proxy"
1860
+ )
1861
+ );
1862
+ }
1863
+ return;
1864
+ }
1865
+ if (ev.type === "result") {
1866
+ const resultText = typeof ev.result === "string" ? ev.result : void 0;
1867
+ relay.sendRaw(
1868
+ JSON.stringify({
1869
+ type: "turn_result",
1870
+ sessionId,
1871
+ success: ev.subtype === "success",
1872
+ isError: ev.is_error ?? false,
1873
+ ...resultText ? { result: resultText } : {}
1874
+ })
1875
+ );
1876
+ this.deps.jsonObserver.onTurnResult(sessionId);
1877
+ }
1878
+ }
1879
+ forwardApprovalRequest(sessionId, msg) {
1880
+ serviceLogger.info(
1881
+ { sessionId, toolName: msg.toolName, requestId: msg.requestId },
1882
+ "Tool approval forwarding to relay"
1883
+ );
1884
+ this.deps.jsonObserver.onApprovalRequested(sessionId);
1885
+ try {
1886
+ const approvalSeq = this.deps.nextSeq?.(sessionId) ?? new SeqCounter(sessionId).next();
1887
+ const envelope = buildMessage(
1888
+ "tool_use_request",
1889
+ sessionId,
1890
+ approvalSeq,
1891
+ {
1892
+ toolName: msg.toolName,
1893
+ toolId: msg.requestId,
1894
+ parameters: msg.input
1895
+ },
1896
+ "proxy"
1897
+ );
1898
+ const session = this.deps.sessionManager.getSession(sessionId);
1899
+ const registered = this.deps.permissionBroker.registerWorkerRequest(
1900
+ {
1901
+ requestId: msg.requestId,
1902
+ provider: session?.provider ?? "claude",
1903
+ sessionId,
1904
+ toolName: msg.toolName,
1905
+ input: msg.input
1906
+ },
1907
+ (decision) => {
1908
+ this.send(sessionId, {
1909
+ type: "worker_approval_response",
1910
+ requestId: msg.requestId,
1911
+ behavior: decision.behavior,
1912
+ ...decision.message ? { message: decision.message } : {}
1913
+ });
1914
+ }
1915
+ );
1916
+ if (!registered) return;
1917
+ this.deps.relayConnection.sendEnvelope(envelope);
1918
+ } catch (err) {
1919
+ const resolved = this.deps.permissionBroker.resolve(msg.requestId, {
1920
+ behavior: "deny",
1921
+ message: "Failed to forward approval request to relay."
1922
+ });
1923
+ if (!resolved) {
1924
+ this.send(sessionId, {
1925
+ type: "worker_approval_response",
1926
+ requestId: msg.requestId,
1927
+ behavior: "deny",
1928
+ message: "Failed to forward approval request to relay."
1929
+ });
1930
+ }
1931
+ serviceLogger.warn(
1932
+ { sessionId, error: String(err) },
1933
+ "Failed to forward tool approval to relay, denying"
1934
+ );
1935
+ }
1936
+ }
1937
+ };
1938
+
1939
+ // src/serve/session-termination.ts
1940
+ function terminateSessionByOwnership(deps, sessionId) {
1941
+ const session = deps.sessionManager.getSession(sessionId);
1942
+ if (session?.mode === "pty" && session.ptyOwner === "local-terminal") {
1943
+ const terminalSocket = deps.terminalSockets.get(sessionId);
1944
+ if (terminalSocket?.writable) {
1945
+ terminalSocket.write(serializeIpc({ type: "pty_detach", sessionId }));
1946
+ }
1947
+ deps.terminalSockets.delete(sessionId);
1948
+ const result = deps.sessionManager.terminateSession(sessionId, {
1949
+ preserveProviderHooks: true
1950
+ });
1951
+ deps.controlHandlers.cleanup(sessionId);
1952
+ deps.agentStatusRegistry.delete(sessionId);
1953
+ serviceLogger.info(
1954
+ { sessionId, success: result.success },
1955
+ "Local terminal session detached from remote view"
1956
+ );
1957
+ return { success: result.success, action: "detach_local_terminal" };
1958
+ }
1959
+ if (session?.mode === "pty" && session.ptyOwner === "proxy-hosted") {
1960
+ const success = deps.hostedPtyRegistry.terminate(sessionId);
1961
+ serviceLogger.info({ sessionId, success }, "Hosted PTY termination requested");
1962
+ return { success, action: "terminate_hosted_pty" };
1963
+ }
1964
+ if (session?.mode === "json") {
1965
+ deps.workerRegistry.send(sessionId, { type: "worker_stop" });
1966
+ deps.workerRegistry.delete(sessionId);
1967
+ const result = deps.sessionManager.terminateSession(sessionId);
1968
+ deps.controlHandlers.cleanup(sessionId);
1969
+ deps.agentStatusRegistry.delete(sessionId);
1970
+ serviceLogger.info({ sessionId, success: result.success }, "JSON worker session terminated");
1971
+ return { success: result.success, action: "terminate_json_worker" };
1972
+ }
1973
+ const hostedTerminated = deps.hostedPtyRegistry.terminate(sessionId);
1974
+ if (hostedTerminated) {
1975
+ return { success: true, action: "terminate_hosted_pty" };
1976
+ }
1977
+ return { success: false, action: "not_found" };
1978
+ }
1979
+
1980
+ // src/serve/pty-input.ts
1981
+ function serializeRawPtyInput(sessionId, data) {
1982
+ return serializeIpc({ type: "pty_input", sessionId, data });
1983
+ }
1984
+
1985
+ // src/serve/relay-input-handlers.ts
1986
+ var RelayInputHandlers = class {
1987
+ constructor(deps) {
1988
+ this.deps = deps;
1989
+ }
1990
+ deps;
1991
+ onUserInput(msg) {
1992
+ const sessionId = msg.sessionId;
1993
+ if (!sessionId) return;
1994
+ const session = this.deps.sessionManager.getSession(sessionId);
1995
+ if (!session) {
1996
+ serviceLogger.warn({ sessionId }, "Remote input dropped: session not found");
1997
+ return;
1998
+ }
1999
+ const payload = msg.payload;
2000
+ const text = payload?.text ?? "";
2001
+ if (session.mode === "json") {
2002
+ this.deps.jsonObserver.onTurnStart(sessionId);
2003
+ const sent = this.deps.workerRegistry.send(sessionId, {
2004
+ type: "worker_input",
2005
+ content: text
2006
+ });
2007
+ if (!sent) {
2008
+ serviceLogger.warn({ sessionId }, "Remote input dropped: JSON worker socket not available");
2009
+ return;
2010
+ }
2011
+ serviceLogger.info({ sessionId }, "Remote input forwarded to JSON worker");
2012
+ return;
2013
+ }
2014
+ serviceLogger.warn(
2015
+ { sessionId, mode: session.mode },
2016
+ "Remote batch input dropped: PTY sessions require remote_input_raw"
2017
+ );
2018
+ }
2019
+ onRemoteInputRaw(msg) {
2020
+ const sessionId = msg.sessionId;
2021
+ const data = msg.data;
2022
+ if (!sessionId || data === void 0) return;
2023
+ const ts = this.deps.terminalSockets.get(sessionId);
2024
+ if (!ts?.writable && this.deps.hostedPtyRegistry.write(sessionId, data)) {
2025
+ serviceLogger.info(
2026
+ { sessionId, bytes: data.length },
2027
+ "Raw PTY input forwarded to hosted PTY"
2028
+ );
2029
+ return;
2030
+ }
2031
+ if (!ts?.writable) {
2032
+ serviceLogger.warn({ sessionId }, "Raw PTY input dropped: terminal socket unavailable");
2033
+ return;
2034
+ }
2035
+ ts.write(serializeRawPtyInput(sessionId, data));
2036
+ serviceLogger.info({ sessionId, bytes: data.length }, "Raw PTY input forwarded");
2037
+ }
2038
+ };
2039
+
2040
+ // src/serve/relay-history-handlers.ts
2041
+ var RelayHistoryHandlers = class {
2042
+ constructor(deps) {
2043
+ this.deps = deps;
2044
+ }
2045
+ deps;
2046
+ onSessionMessagesRequest(msg) {
2047
+ const sid = msg.sessionId;
2048
+ if (!sid) return;
2049
+ const requestId = msg.requestId;
2050
+ const session = this.deps.sessionManager.getSession(sid);
2051
+ if (session?.claudeSessionId) {
2052
+ readSessionMessages(session.claudeSessionId).then((messages) => {
2053
+ this.deps.relaySend(
2054
+ JSON.stringify({
2055
+ type: "session_history_messages",
2056
+ requestId,
2057
+ sessionId: sid,
2058
+ messages
2059
+ })
2060
+ );
2061
+ serviceLogger.info(
2062
+ { sessionId: sid, messageCount: messages.length },
2063
+ "History messages sent on request"
2064
+ );
2065
+ }).catch((err) => {
2066
+ serviceLogger.warn(
2067
+ { sessionId: sid, error: String(err) },
2068
+ "Failed to read session history messages on request"
2069
+ );
2070
+ this.deps.relaySend(
2071
+ JSON.stringify({
2072
+ type: "session_history_messages",
2073
+ requestId,
2074
+ sessionId: sid,
2075
+ messages: []
2076
+ })
2077
+ );
2078
+ });
2079
+ } else {
2080
+ this.deps.relaySend(
2081
+ JSON.stringify({
2082
+ type: "session_history_messages",
2083
+ requestId,
2084
+ sessionId: sid,
2085
+ messages: []
2086
+ })
2087
+ );
2088
+ }
2089
+ const approvals = this.deps.permissionBroker.listSession(sid).map((approval) => ({
2090
+ requestId: approval.requestId,
2091
+ toolName: approval.toolName,
2092
+ input: approval.input
2093
+ }));
2094
+ this.deps.relaySend(
2095
+ JSON.stringify({ type: "pending_approvals_push", sessionId: sid, approvals })
2096
+ );
2097
+ serviceLogger.info({ sessionId: sid, count: approvals.length }, "Pending approvals pushed");
2098
+ }
2099
+ };
2100
+
2101
+ // src/serve/relay-permission-handlers.ts
2102
+ var RelayPermissionHandlers = class {
2103
+ constructor(deps) {
2104
+ this.deps = deps;
2105
+ }
2106
+ deps;
2107
+ onToolApprove(msg) {
2108
+ const sessionId = msg.sessionId;
2109
+ const payload = msg.payload;
2110
+ if (!sessionId || !payload?.toolId) return;
2111
+ const pending = this.deps.permissionBroker.get(payload.toolId);
2112
+ if (!pending) {
2113
+ this.pushPermissionDecisionResult(
2114
+ sessionId,
2115
+ payload.toolId,
2116
+ "allow",
2117
+ false,
2118
+ "Permission request is no longer pending."
2119
+ );
2120
+ return;
2121
+ }
2122
+ if (!this.deps.permissionBroker.resolve(payload.toolId, { behavior: "allow" })) {
2123
+ this.pushPermissionDecisionResult(
2124
+ pending.sessionId,
2125
+ payload.toolId,
2126
+ "allow",
2127
+ false,
2128
+ "Permission request is no longer pending."
2129
+ );
2130
+ return;
2131
+ }
2132
+ this.deps.hookEventRouter.onPermissionResolved(
2133
+ pending.sessionId,
2134
+ pending.provider,
2135
+ payload.toolId,
2136
+ "allow",
2137
+ { toolName: pending.toolName, toolInput: pending.input }
2138
+ );
2139
+ if (pending.source === "worker" && payload.whitelistTool) {
2140
+ const toolName = pending.toolName;
2141
+ if (toolName) {
2142
+ const whitelisted = this.deps.workerRegistry.send(pending.sessionId, {
2143
+ type: "worker_whitelist_add",
2144
+ toolName
2145
+ });
2146
+ if (whitelisted) {
2147
+ serviceLogger.info(
2148
+ { sessionId: pending.sessionId, toolName },
2149
+ "Tool added to session whitelist via relay"
2150
+ );
2151
+ }
2152
+ }
2153
+ }
2154
+ this.pushPermissionDecisionResult(pending.sessionId, payload.toolId, "allow", true);
2155
+ serviceLogger.info(
2156
+ { sessionId, toolId: payload.toolId, whitelistTool: payload.whitelistTool },
2157
+ "Tool approved via relay"
2158
+ );
2159
+ }
2160
+ onToolDeny(msg) {
2161
+ const sessionId = msg.sessionId;
2162
+ const payload = msg.payload;
2163
+ if (!sessionId || !payload?.toolId) return;
2164
+ const reason = payload.reason ?? "Denied by remote user";
2165
+ const pending = this.deps.permissionBroker.get(payload.toolId);
2166
+ if (!pending) {
2167
+ this.pushPermissionDecisionResult(
2168
+ sessionId,
2169
+ payload.toolId,
2170
+ "deny",
2171
+ false,
2172
+ "Permission request is no longer pending."
2173
+ );
2174
+ return;
2175
+ }
2176
+ if (!this.deps.permissionBroker.resolve(payload.toolId, {
2177
+ behavior: "deny",
2178
+ message: reason
2179
+ })) {
2180
+ this.pushPermissionDecisionResult(
2181
+ pending.sessionId,
2182
+ payload.toolId,
2183
+ "deny",
2184
+ false,
2185
+ "Permission request is no longer pending."
2186
+ );
2187
+ return;
2188
+ }
2189
+ this.deps.hookEventRouter.onPermissionResolved(
2190
+ pending.sessionId,
2191
+ pending.provider,
2192
+ payload.toolId,
2193
+ "deny",
2194
+ { toolName: pending.toolName, toolInput: pending.input }
2195
+ );
2196
+ this.pushPermissionDecisionResult(pending.sessionId, payload.toolId, "deny", true, reason);
2197
+ serviceLogger.info({ sessionId, toolId: payload.toolId }, "Tool denied via relay");
2198
+ }
2199
+ onPermissionRequestDelivered(msg) {
2200
+ const sid = msg.sessionId;
2201
+ const requestId = msg.requestId;
2202
+ if (!sid || !requestId) return;
2203
+ const marked = this.deps.permissionBroker.markDelivered(requestId);
2204
+ serviceLogger.info({ sessionId: sid, requestId, marked }, "Permission request delivered");
2205
+ }
2206
+ pushPermissionDecisionResult(sessionId, requestId, outcome, delivered, message) {
2207
+ this.deps.relaySend(
2208
+ JSON.stringify({
2209
+ type: "permission_decision_result",
2210
+ sessionId,
2211
+ requestId,
2212
+ outcome,
2213
+ delivered,
2214
+ ...message ? { message } : {}
2215
+ })
2216
+ );
2217
+ }
2218
+ };
2219
+
2220
+ // src/serve/relay-resource-handlers.ts
2221
+ import { homedir as homedir4 } from "os";
2222
+ import { accessSync, constants, statSync } from "fs";
2223
+ function errorMessage(err) {
2224
+ return err instanceof Error ? err.message : String(err);
2225
+ }
2226
+ function validateExecutablePath(path) {
2227
+ const normalized = path.trim();
2228
+ if (!normalized.startsWith("/")) throw new Error("CLI \u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
2229
+ const stat2 = statSync(normalized);
2230
+ if (!stat2.isFile()) throw new Error("CLI \u8DEF\u5F84\u4E0D\u662F\u53EF\u6267\u884C\u6587\u4EF6");
2231
+ accessSync(normalized, constants.X_OK);
2232
+ return normalized;
2233
+ }
2234
+ var RelayResourceHandlers = class {
2235
+ constructor(deps) {
2236
+ this.deps = deps;
2237
+ }
2238
+ deps;
2239
+ onProxyInfoRequest(msg) {
2240
+ this.deps.relaySend(
2241
+ JSON.stringify({
2242
+ type: "proxy_info",
2243
+ requestId: msg.requestId,
2244
+ homePath: homedir4() || "/",
2245
+ agentCli: detectAgentCliStatus(this.deps.getProviderEnv(), {
2246
+ suggestions: this.deps.getAgentCliSuggestions()
2247
+ })
2248
+ })
2249
+ );
2250
+ }
2251
+ onAgentCliConfigUpdate(msg) {
2252
+ const requestId = msg.requestId;
2253
+ const provider = msg.provider;
2254
+ const rawPath = msg.path;
2255
+ if (provider !== "claude" && provider !== "codex") {
2256
+ this.deps.relaySend(
2257
+ JSON.stringify({
2258
+ type: "agent_cli_config_update_response",
2259
+ requestId,
2260
+ provider: "claude",
2261
+ errorCode: ControlErrorCode.PROVIDER_UNSUPPORTED,
2262
+ error: "Unsupported Agent CLI."
2263
+ })
2264
+ );
2265
+ return;
2266
+ }
2267
+ try {
2268
+ const path = validateExecutablePath(rawPath ?? "");
2269
+ saveAgentCliPath(provider, path, { envName: this.deps.envName });
2270
+ this.deps.setAgentCliPath(provider, path);
2271
+ const agentCli = detectAgentCliStatus(this.deps.getProviderEnv(), {
2272
+ suggestions: this.deps.getAgentCliSuggestions()
2273
+ });
2274
+ this.deps.relaySend(
2275
+ JSON.stringify({
2276
+ type: "agent_cli_config_update_response",
2277
+ requestId,
2278
+ provider,
2279
+ agentCli
2280
+ })
2281
+ );
2282
+ serviceLogger.info({ provider, path }, "Agent CLI path updated");
2283
+ } catch (err) {
2284
+ const error = errorMessage(err);
2285
+ this.deps.relaySend(
2286
+ JSON.stringify({
2287
+ type: "agent_cli_config_update_response",
2288
+ requestId,
2289
+ provider,
2290
+ errorCode: ControlErrorCode.INVALID_PATH,
2291
+ error
2292
+ })
2293
+ );
2294
+ serviceLogger.warn({ provider, path: rawPath, error }, "Agent CLI path update rejected");
2295
+ }
2296
+ }
2297
+ onDirListRequest(msg) {
2298
+ this.deps.controlHandlers.handleDirListRequest({
2299
+ path: msg.path ?? "",
2300
+ requestId: msg.requestId
2301
+ });
2302
+ }
2303
+ onDirCreateRequest(msg) {
2304
+ this.deps.controlHandlers.handleDirCreateRequest({
2305
+ path: msg.path ?? "",
2306
+ requestId: msg.requestId
2307
+ });
2308
+ }
2309
+ onSessionResourcesRequest(msg) {
2310
+ const sid = msg.sessionId;
2311
+ if (!sid) return;
2312
+ const session = this.deps.sessionManager.getSession(sid);
2313
+ if (!session?.cwd) {
2314
+ serviceLogger.warn({ sessionId: sid }, "Session resources request: no cwd available");
2315
+ this.deps.relaySend(
2316
+ JSON.stringify({
2317
+ type: "session_resources_response",
2318
+ requestId: msg.requestId,
2319
+ sessionId: sid,
2320
+ commands: [],
2321
+ groups: [],
2322
+ errorCode: ControlErrorCode.SESSION_NOT_FOUND,
2323
+ error: "Session not found or cwd unavailable"
2324
+ })
2325
+ );
2326
+ return;
2327
+ }
2328
+ this.deps.controlHandlers.handleSessionResourcesRequest({
2329
+ sessionId: sid,
2330
+ requestId: msg.requestId,
2331
+ workDir: session.cwd
2332
+ });
2333
+ serviceLogger.info({ sessionId: sid, cwd: session.cwd }, "Session resources requested");
2334
+ }
2335
+ };
2336
+
2337
+ // src/serve/relay-session-create-handler.ts
2338
+ import { rmSync, statSync as statSync2 } from "fs";
2339
+ import { isAbsolute as isAbsolute3 } from "path";
2340
+ import { nanoid as nanoid3 } from "nanoid";
2341
+
2342
+ // src/serve/hosted-pty-registry.ts
2343
+ import * as pty from "node-pty";
2344
+ import pkg from "@xterm/headless";
2345
+ import { SerializeAddon } from "@xterm/addon-serialize";
2346
+ var { Terminal: HeadlessTerminal } = pkg;
2347
+ var DEFAULT_COLS = 80;
2348
+ var DEFAULT_ROWS = 24;
2349
+ var IDLE_CHECK_INTERVAL_MS = 3e3;
2350
+ var IDLE_THRESHOLD_MS = 3e3;
2351
+ var PROVIDERS = {
2352
+ claude: CLAUDE_PROVIDER,
2353
+ codex: CODEX_PROVIDER
2354
+ };
2355
+ var HOSTED_PTY_TERM = "xterm-256color";
2356
+ var HOSTED_PTY_COLORTERM = "truecolor";
2357
+ function buildHostedPtyArgs(provider, resumeSessionId) {
2358
+ if (!resumeSessionId) return [];
2359
+ return provider === "codex" ? ["resume", resumeSessionId] : ["--resume", resumeSessionId];
2360
+ }
2361
+ function normalizeHostedPtyEnv(env) {
2362
+ const normalized = {};
2363
+ for (const [key, value] of Object.entries(env)) {
2364
+ if (value === void 0) continue;
2365
+ normalized[key] = value;
2366
+ }
2367
+ delete normalized.NO_COLOR;
2368
+ if (normalized.CLICOLOR === "0") {
2369
+ delete normalized.CLICOLOR;
2370
+ }
2371
+ normalized.TERM = HOSTED_PTY_TERM;
2372
+ normalized.COLORTERM = HOSTED_PTY_COLORTERM;
2373
+ normalized.CLICOLOR = "1";
2374
+ return normalized;
2375
+ }
2376
+ var HostedPtyRegistry = class {
2377
+ constructor(deps) {
2378
+ this.deps = deps;
2379
+ }
2380
+ deps;
2381
+ sessions = /* @__PURE__ */ new Map();
2382
+ start(options) {
2383
+ const provider = PROVIDERS[options.provider];
2384
+ const cols = options.cols ?? DEFAULT_COLS;
2385
+ const rows = options.rows ?? DEFAULT_ROWS;
2386
+ const command = provider.buildTerminalCommand(
2387
+ { args: options.args, permissionMode: options.permissionMode, hook: options.hook },
2388
+ this.deps.getProviderEnv()
2389
+ );
2390
+ const env = normalizeHostedPtyEnv(command.env);
2391
+ const child = pty.spawn(command.command, command.args, {
2392
+ name: HOSTED_PTY_TERM,
2393
+ cols,
2394
+ rows,
2395
+ cwd: options.cwd,
2396
+ env
2397
+ });
2398
+ const terminal = new HeadlessTerminal({ cols, rows, scrollback: 5e3, allowProposedApi: true });
2399
+ const serializeAddon = new SerializeAddon();
2400
+ terminal.loadAddon(serializeAddon);
2401
+ void import("@xterm/addon-unicode-graphemes").then(({ UnicodeGraphemesAddon }) => terminal.loadAddon(new UnicodeGraphemesAddon())).catch((err) => {
2402
+ serviceLogger.warn(
2403
+ { sessionId: options.sessionId, error: String(err) },
2404
+ "Unicode addon unavailable"
2405
+ );
2406
+ });
2407
+ const hosted = {
2408
+ child,
2409
+ terminal,
2410
+ serializeAddon,
2411
+ idleTimer: setInterval(() => this.checkIdle(options.sessionId), IDLE_CHECK_INTERVAL_MS),
2412
+ lastOutputTime: 0,
2413
+ currentState: "turn_complete",
2414
+ outputSeq: 0
2415
+ };
2416
+ this.sessions.set(options.sessionId, hosted);
2417
+ child.onData((data) => this.handleData(options.sessionId, data));
2418
+ child.onExit(({ exitCode, signal }) => {
2419
+ const code = signal ? 128 + signal : exitCode;
2420
+ serviceLogger.info({ sessionId: options.sessionId, code }, "Hosted PTY exited");
2421
+ this.close(options.sessionId, { kill: false, notify: true });
2422
+ });
2423
+ serviceLogger.info(
2424
+ {
2425
+ sessionId: options.sessionId,
2426
+ provider: options.provider,
2427
+ command: command.command,
2428
+ pid: child.pid,
2429
+ cwd: options.cwd,
2430
+ cols,
2431
+ rows
2432
+ },
2433
+ "Hosted PTY started"
2434
+ );
2435
+ return child.pid;
2436
+ }
2437
+ has(sessionId) {
2438
+ return this.sessions.has(sessionId);
2439
+ }
2440
+ write(sessionId, data) {
2441
+ const hosted = this.sessions.get(sessionId);
2442
+ if (!hosted) return false;
2443
+ hosted.child.write(data);
2444
+ return true;
2445
+ }
2446
+ resize(sessionId, cols, rows) {
2447
+ const hosted = this.sessions.get(sessionId);
2448
+ if (!hosted) return false;
2449
+ hosted.child.resize(cols, rows);
2450
+ hosted.terminal.resize(cols, rows);
2451
+ this.deps.relayConnection.sendRaw(
2452
+ JSON.stringify({ type: "terminal_resize", sessionId, cols, rows })
2453
+ );
2454
+ serviceLogger.info({ sessionId, cols, rows }, "Hosted PTY resized");
2455
+ return true;
2456
+ }
2457
+ snapshot(sessionId, requestId) {
2458
+ const hosted = this.sessions.get(sessionId);
2459
+ if (!hosted) return false;
2460
+ const data = hosted.serializeAddon.serialize();
2461
+ this.deps.relayConnection.sendRaw(
2462
+ JSON.stringify({
2463
+ type: "session_snapshot",
2464
+ sessionId,
2465
+ cols: hosted.terminal.cols,
2466
+ rows: hosted.terminal.rows,
2467
+ data,
2468
+ outputSeq: hosted.outputSeq,
2469
+ requestId
2470
+ })
2471
+ );
2472
+ serviceLogger.info(
2473
+ { sessionId, cols: hosted.terminal.cols, rows: hosted.terminal.rows, bytes: data.length },
2474
+ "Hosted PTY snapshot sent"
2475
+ );
2476
+ return true;
2477
+ }
2478
+ terminate(sessionId) {
2479
+ return this.close(sessionId, { kill: true, notify: true });
2480
+ }
2481
+ destroyAll() {
2482
+ for (const sessionId of Array.from(this.sessions.keys())) {
2483
+ this.close(sessionId, { kill: true, notify: false });
2484
+ }
2485
+ }
2486
+ handleData(sessionId, data) {
2487
+ const hosted = this.sessions.get(sessionId);
2488
+ if (!hosted) return;
2489
+ hosted.lastOutputTime = Date.now();
2490
+ hosted.outputSeq += 1;
2491
+ hosted.terminal.write(data);
2492
+ this.sendBinary(sessionId, Buffer.from(data, "utf-8"), hosted.outputSeq);
2493
+ const oscSequences = extractOscSequences(data);
2494
+ const session = this.deps.sessionManager.getSession(sessionId);
2495
+ const signal = extractOscSignals(data, session?.provider);
2496
+ if (oscSequences.length > 0) {
2497
+ serviceLogger.debug(
2498
+ {
2499
+ sessionId,
2500
+ oscSequences,
2501
+ signal
2502
+ },
2503
+ "Hosted PTY OSC sequences parsed"
2504
+ );
2505
+ }
2506
+ if (signal?.title) {
2507
+ this.sendTerminalTitle(sessionId, signal.title);
2508
+ }
2509
+ if (signal?.state === "approval_wait") {
2510
+ hosted.currentState = "approval_wait";
2511
+ this.deps.changeSessionState(sessionId, SessionState.WAITING_APPROVAL);
2512
+ this.sendPtyState(sessionId, "approval_wait", { title: signal?.title, tool: signal?.tool });
2513
+ return;
2514
+ }
2515
+ if (shouldReleaseApprovalWait({
2516
+ currentState: hosted.currentState,
2517
+ signalState: signal?.state
2518
+ })) {
2519
+ const nextState = stateAfterApprovalRelease(signal?.state);
2520
+ hosted.currentState = nextState;
2521
+ if (nextState === "turn_complete") {
2522
+ this.deps.onTurnComplete(sessionId);
2523
+ this.deps.changeSessionState(sessionId, SessionState.IDLE);
2524
+ } else {
2525
+ this.deps.changeSessionState(sessionId, SessionState.WORKING);
2526
+ }
2527
+ this.sendPtyState(sessionId, nextState, { title: signal?.title, tool: signal?.tool });
2528
+ return;
2529
+ }
2530
+ if ((session?.state === SessionState.WAITING_APPROVAL || hosted.currentState === "approval_wait") && signal?.state !== "turn_complete") {
2531
+ hosted.currentState = "approval_wait";
2532
+ this.sendPtyState(sessionId, "approval_wait", { title: signal?.title, tool: signal?.tool });
2533
+ return;
2534
+ }
2535
+ if (signal && signal.state !== "working") {
2536
+ hosted.currentState = signal.state;
2537
+ if (signal.state === "turn_complete") {
2538
+ this.deps.onTurnComplete(sessionId);
2539
+ this.deps.changeSessionState(sessionId, SessionState.IDLE);
2540
+ }
2541
+ this.sendPtyState(sessionId, signal.state, { title: signal.title, tool: signal.tool });
2542
+ return;
2543
+ }
2544
+ if (hosted.currentState !== "working") {
2545
+ hosted.currentState = "working";
2546
+ this.deps.changeSessionState(sessionId, SessionState.WORKING);
2547
+ this.sendPtyState(sessionId, "working");
2548
+ }
2549
+ }
2550
+ checkIdle(sessionId) {
2551
+ const hosted = this.sessions.get(sessionId);
2552
+ if (!hosted) return;
2553
+ if (hosted.lastOutputTime === 0 || Date.now() - hosted.lastOutputTime <= IDLE_THRESHOLD_MS) {
2554
+ return;
2555
+ }
2556
+ hosted.lastOutputTime = 0;
2557
+ if (hosted.currentState !== "working") return;
2558
+ hosted.currentState = "turn_complete";
2559
+ this.deps.onTurnComplete(sessionId);
2560
+ this.deps.changeSessionState(sessionId, SessionState.IDLE);
2561
+ this.sendPtyState(sessionId, "turn_complete");
2562
+ }
2563
+ sendPtyState(sessionId, state, meta) {
2564
+ const payload = {
2565
+ state,
2566
+ ...meta?.title !== void 0 ? { title: meta.title } : {},
2567
+ ...meta?.tool !== void 0 ? { tool: meta.tool } : {}
2568
+ };
2569
+ this.deps.relayConnection.sendRaw(
2570
+ JSON.stringify({
2571
+ type: "pty_state",
2572
+ sessionId,
2573
+ payload
2574
+ })
2575
+ );
2576
+ const logPayload = { sessionId, ...payload };
2577
+ if (state === "approval_wait" || state === "turn_complete") {
2578
+ serviceLogger.info(logPayload, "Hosted PTY semantic event pushed");
2579
+ } else {
2580
+ serviceLogger.debug(logPayload, "Hosted PTY semantic event pushed");
2581
+ }
2582
+ }
2583
+ sendTerminalTitle(sessionId, title) {
2584
+ this.deps.relayConnection.sendRaw(
2585
+ JSON.stringify({
2586
+ type: "terminal_title",
2587
+ sessionId,
2588
+ title
2589
+ })
2590
+ );
2591
+ }
2592
+ sendBinary(sessionId, data, outputSeq) {
2593
+ const sessionIdBuf = Buffer.from(sessionId, "utf-8");
2594
+ const frame = Buffer.alloc(1 + sessionIdBuf.length + 4 + data.length);
2595
+ frame[0] = sessionIdBuf.length;
2596
+ sessionIdBuf.copy(frame, 1);
2597
+ frame.writeUInt32LE(outputSeq, 1 + sessionIdBuf.length);
2598
+ data.copy(frame, 1 + sessionIdBuf.length + 4);
2599
+ this.deps.relayConnection.sendBinary(frame);
2600
+ }
2601
+ close(sessionId, options) {
2602
+ const hosted = this.sessions.get(sessionId);
2603
+ if (!hosted) return false;
2604
+ this.sessions.delete(sessionId);
2605
+ clearInterval(hosted.idleTimer);
2606
+ if (options.kill) {
2607
+ try {
2608
+ hosted.child.kill();
2609
+ } catch {
2610
+ }
2611
+ }
2612
+ hosted.terminal.dispose();
2613
+ if (options.notify) {
2614
+ this.sendPtyState(sessionId, "turn_complete");
2615
+ this.deps.sessionManager.terminateSession(sessionId);
2616
+ this.deps.onSessionClosed(sessionId);
2617
+ }
2618
+ return true;
2619
+ }
2620
+ };
2621
+
2622
+ // src/serve/relay-session-create-handler.ts
2623
+ function validateSessionCwd(cwd) {
2624
+ if (typeof cwd !== "string" || !cwd.trim()) {
2625
+ return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
2626
+ }
2627
+ const trimmed = cwd.trim();
2628
+ if (!isAbsolute3(trimmed)) {
2629
+ return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
2630
+ }
2631
+ try {
2632
+ const stat2 = statSync2(trimmed);
2633
+ return stat2.isDirectory() ? null : { message: "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55", code: ControlErrorCode.PATH_NOT_DIRECTORY };
2634
+ } catch (err) {
2635
+ return {
2636
+ message: `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE: ${trimmed}`,
2637
+ code: classifyPathError(err)
2638
+ };
2639
+ }
2640
+ }
2641
+ var RelaySessionCreateHandler = class {
2642
+ constructor(deps) {
2643
+ this.deps = deps;
2644
+ }
2645
+ deps;
2646
+ onSessionCreate(msg) {
2647
+ const requestId = msg.requestId;
2648
+ const cwd = msg.cwd;
2649
+ const cwdError = validateSessionCwd(cwd);
2650
+ if (cwdError) {
2651
+ this.deps.relaySend(
2652
+ JSON.stringify({
2653
+ type: "session_create_response",
2654
+ requestId,
2655
+ sessionId: "",
2656
+ error: cwdError.message,
2657
+ errorCode: cwdError.code
2658
+ })
2659
+ );
2660
+ serviceLogger.warn({ cwd }, "Session create rejected: invalid cwd");
2661
+ return;
2662
+ }
2663
+ const sessionCwd = typeof cwd === "string" ? cwd.trim() : "";
2664
+ const provider = msg.provider;
2665
+ const mode = msg.mode ?? "json";
2666
+ const permissionMode = msg.permissionMode;
2667
+ if (mode === "pty") {
2668
+ this.createHostedPtySession(msg, sessionCwd, provider ?? "claude", permissionMode);
2669
+ return;
2670
+ }
2671
+ if (provider !== "claude") {
2672
+ this.deps.relaySend(
2673
+ JSON.stringify({
2674
+ type: "session_create_response",
2675
+ requestId,
2676
+ sessionId: "",
2677
+ errorCode: ControlErrorCode.PROVIDER_UNSUPPORTED,
2678
+ error: provider === "codex" ? "Codex chat sessions are not supported yet; start a Codex terminal session instead." : "Unsupported provider for JSON session."
2679
+ })
2680
+ );
2681
+ serviceLogger.warn({ provider }, "JSON session create rejected for unsupported provider");
2682
+ return;
2683
+ }
2684
+ const resumeSessionId = msg.resumeSessionId;
2685
+ const streamDelta = msg.streamDelta === true;
2686
+ const name = tildify(sessionCwd);
2687
+ const pendingId = nanoid3();
2688
+ const hook = this.deps.createHookContext(pendingId, provider);
2689
+ const workerPid = this.deps.workerRegistry.spawn(pendingId, {
2690
+ cwd: sessionCwd,
2691
+ resumeSessionId,
2692
+ permissionMode,
2693
+ streamDelta,
2694
+ hook
2695
+ });
2696
+ const paths = sessionPaths(pendingId);
2697
+ let attempt = 0;
2698
+ const maxRetries = 20;
2699
+ const tryConnect = () => {
2700
+ attempt++;
2701
+ this.deps.workerRegistry.connect(pendingId, paths.workerSock).then((sock) => {
2702
+ if (sock) {
2703
+ const session = this.deps.sessionManager.createSession(
2704
+ "json",
2705
+ sessionCwd,
2706
+ workerPid,
2707
+ name,
2708
+ pendingId,
2709
+ provider
2710
+ );
2711
+ if (resumeSessionId) {
2712
+ this.deps.sessionManager.setClaudeSessionId(session.id, resumeSessionId);
2713
+ }
2714
+ this.deps.relaySend(
2715
+ JSON.stringify({ type: "session_create_response", requestId, sessionId: session.id })
2716
+ );
2717
+ if (resumeSessionId) {
2718
+ this.pushHistoryMessages(session.id, resumeSessionId);
2719
+ }
2720
+ serviceLogger.info(
2721
+ { sessionId: session.id, cwd: sessionCwd },
2722
+ "JSON session created via relay"
2723
+ );
2724
+ this.deps.controlHandlers.pushCommandList(session.id, sessionCwd);
2725
+ this.deps.broadcastSessionSync(session);
2726
+ this.deps.broadcastSessionList();
2727
+ } else if (attempt < maxRetries) {
2728
+ setTimeout(tryConnect, Math.min(100 * attempt, 2e3));
2729
+ } else {
2730
+ this.cleanupPendingJsonSession(pendingId);
2731
+ this.deps.relaySend(
2732
+ JSON.stringify({
2733
+ type: "session_create_response",
2734
+ requestId,
2735
+ sessionId: pendingId,
2736
+ errorCode: ControlErrorCode.WORKER_START_FAILED,
2737
+ error: "Worker failed to start"
2738
+ })
2739
+ );
2740
+ serviceLogger.error({ sessionId: pendingId }, "Worker connection timeout via relay");
2741
+ }
2742
+ });
2743
+ };
2744
+ setTimeout(tryConnect, 100);
2745
+ }
2746
+ cleanupPendingJsonSession(sessionId) {
2747
+ const killed = this.deps.workerRegistry.terminateProcess(sessionId);
2748
+ const paths = sessionPaths(sessionId);
2749
+ rmSync(paths.dir, { recursive: true, force: true });
2750
+ this.deps.cleanupHookContext(sessionId);
2751
+ this.deps.permissionBroker.cleanupSession(sessionId, "Worker failed to start");
2752
+ this.deps.agentStatusRegistry.delete(sessionId);
2753
+ serviceLogger.warn(
2754
+ { sessionId, killed },
2755
+ "Cleaned up pending JSON session after startup failure"
2756
+ );
2757
+ }
2758
+ createHostedPtySession(msg, cwd, provider, permissionMode) {
2759
+ if (provider !== "claude" && provider !== "codex") {
2760
+ this.deps.relaySend(
2761
+ JSON.stringify({
2762
+ type: "session_create_response",
2763
+ requestId: msg.requestId,
2764
+ sessionId: "",
2765
+ errorCode: ControlErrorCode.PROVIDER_UNSUPPORTED,
2766
+ error: "Unsupported provider for PTY session."
2767
+ })
2768
+ );
2769
+ return;
2770
+ }
2771
+ const resumeSessionId = msg.resumeSessionId;
2772
+ const pendingId = nanoid3();
2773
+ const name = tildify(cwd);
2774
+ const hook = this.deps.createHookContext(pendingId, provider);
2775
+ try {
2776
+ const pid = this.deps.hostedPtyRegistry.start({
2777
+ sessionId: pendingId,
2778
+ provider,
2779
+ cwd,
2780
+ args: buildHostedPtyArgs(provider, resumeSessionId),
2781
+ permissionMode,
2782
+ hook
2783
+ });
2784
+ const session = this.deps.sessionManager.createSession(
2785
+ "pty",
2786
+ cwd,
2787
+ pid,
2788
+ name,
2789
+ pendingId,
2790
+ provider,
2791
+ "proxy-hosted"
2792
+ );
2793
+ if (resumeSessionId && provider === "claude") {
2794
+ this.deps.sessionManager.setClaudeSessionId(session.id, resumeSessionId);
2795
+ }
2796
+ this.deps.relaySend(
2797
+ JSON.stringify({
2798
+ type: "session_create_response",
2799
+ requestId: msg.requestId,
2800
+ sessionId: session.id,
2801
+ mode: "pty",
2802
+ provider,
2803
+ ptyOwner: "proxy-hosted"
2804
+ })
2805
+ );
2806
+ this.deps.controlHandlers.pushCommandList(session.id, cwd);
2807
+ this.deps.controlHandlers.pushFileTree(session.id, cwd);
2808
+ this.deps.broadcastSessionSync(session);
2809
+ this.deps.broadcastSessionList();
2810
+ serviceLogger.info({ sessionId: session.id, provider, cwd }, "Hosted PTY session created");
2811
+ } catch (err) {
2812
+ const error = err instanceof Error ? err.message : String(err);
2813
+ this.deps.relaySend(
2814
+ JSON.stringify({
2815
+ type: "session_create_response",
2816
+ requestId: msg.requestId,
2817
+ sessionId: "",
2818
+ errorCode: ControlErrorCode.PROCESS_START_FAILED,
2819
+ error
2820
+ })
2821
+ );
2822
+ const providerEnv = this.deps.getProviderEnv();
2823
+ serviceLogger.warn(
2824
+ {
2825
+ provider,
2826
+ cwd,
2827
+ error,
2828
+ claudeBin: providerEnv.CLAUDE_BIN,
2829
+ codexBin: providerEnv.CODEX_BIN,
2830
+ path: providerEnv.PATH
2831
+ },
2832
+ "Hosted PTY session create failed"
2833
+ );
2834
+ }
2835
+ }
2836
+ pushHistoryMessages(sessionId, resumeSessionId) {
2837
+ readSessionMessages(resumeSessionId).then((messages) => {
2838
+ if (messages.length === 0) return;
2839
+ this.deps.relaySend(
2840
+ JSON.stringify({ type: "session_history_messages", sessionId, messages })
2841
+ );
2842
+ serviceLogger.info(
2843
+ { sessionId, resumeSessionId, messageCount: messages.length },
2844
+ "History messages sent for resumed session"
2845
+ );
2846
+ }).catch((err) => {
2847
+ serviceLogger.warn(
2848
+ { sessionId, error: String(err) },
2849
+ "Failed to read session history messages"
2850
+ );
2851
+ });
2852
+ }
2853
+ };
2854
+
2855
+ // src/serve/relay-router.ts
2856
+ var RelayRouter = class {
2857
+ constructor(deps) {
2858
+ this.deps = deps;
2859
+ this.historyHandlers = new RelayHistoryHandlers({
2860
+ relaySend: deps.relaySend,
2861
+ sessionManager: deps.sessionManager,
2862
+ permissionBroker: deps.permissionBroker
2863
+ });
2864
+ this.inputHandlers = new RelayInputHandlers({
2865
+ sessionManager: deps.sessionManager,
2866
+ workerRegistry: deps.workerRegistry,
2867
+ terminalSockets: deps.terminalSockets,
2868
+ hostedPtyRegistry: deps.hostedPtyRegistry,
2869
+ jsonObserver: deps.jsonObserver
2870
+ });
2871
+ this.resourceHandlers = new RelayResourceHandlers({
2872
+ relaySend: deps.relaySend,
2873
+ controlHandlers: deps.controlHandlers,
2874
+ sessionManager: deps.sessionManager,
2875
+ envName: deps.envName,
2876
+ getProviderEnv: deps.getProviderEnv,
2877
+ getAgentCliSuggestions: deps.getAgentCliSuggestions,
2878
+ setAgentCliPath: deps.setAgentCliPath
2879
+ });
2880
+ this.permissionHandlers = new RelayPermissionHandlers({
2881
+ relaySend: deps.relaySend,
2882
+ permissionBroker: deps.permissionBroker,
2883
+ hookEventRouter: deps.hookEventRouter,
2884
+ workerRegistry: deps.workerRegistry
2885
+ });
2886
+ this.sessionCreateHandler = new RelaySessionCreateHandler({
2887
+ relaySend: deps.relaySend,
2888
+ workerRegistry: deps.workerRegistry,
2889
+ sessionManager: deps.sessionManager,
2890
+ hostedPtyRegistry: deps.hostedPtyRegistry,
2891
+ controlHandlers: deps.controlHandlers,
2892
+ permissionBroker: deps.permissionBroker,
2893
+ agentStatusRegistry: deps.agentStatusRegistry,
2894
+ getProviderEnv: deps.getProviderEnv,
2895
+ createHookContext: deps.createHookContext,
2896
+ cleanupHookContext: deps.cleanupHookContext,
2897
+ broadcastSessionSync: deps.broadcastSessionSync,
2898
+ broadcastSessionList: deps.broadcastSessionList
2899
+ });
2900
+ }
2901
+ deps;
2902
+ historyHandlers;
2903
+ inputHandlers;
2904
+ permissionHandlers;
2905
+ resourceHandlers;
2906
+ sessionCreateHandler;
2907
+ handle(parsed) {
2908
+ const type = parsed.type;
2909
+ if (!type) {
2910
+ serviceLogger.warn("Relay message without type discriminator");
2911
+ return;
2912
+ }
2913
+ const handler = this.handlers[type];
2914
+ if (!handler) {
2915
+ serviceLogger.warn({ type }, "Unhandled relay message type");
2916
+ return;
2917
+ }
2918
+ try {
2919
+ handler.call(this, parsed);
2920
+ } catch (err) {
2921
+ serviceLogger.warn({ type, error: String(err) }, "Relay handler threw");
2922
+ }
2923
+ }
2924
+ handlers = {
2925
+ user_input: (msg) => this.inputHandlers.onUserInput(msg),
2926
+ remote_input_raw: (msg) => this.inputHandlers.onRemoteInputRaw(msg),
2927
+ tool_approve: (msg) => this.permissionHandlers.onToolApprove(msg),
2928
+ tool_deny: (msg) => this.permissionHandlers.onToolDeny(msg),
2929
+ proxy_info_request: (msg) => this.resourceHandlers.onProxyInfoRequest(msg),
2930
+ agent_cli_config_update: (msg) => this.resourceHandlers.onAgentCliConfigUpdate(msg),
2931
+ dir_list_request: (msg) => this.resourceHandlers.onDirListRequest(msg),
2932
+ dir_create_request: (msg) => this.resourceHandlers.onDirCreateRequest(msg),
2933
+ session_create: (msg) => this.sessionCreateHandler.onSessionCreate(msg),
2934
+ session_messages_request: (msg) => this.historyHandlers.onSessionMessagesRequest(msg),
2935
+ session_resources_request: (msg) => this.resourceHandlers.onSessionResourcesRequest(msg),
2936
+ agent_status_request: (msg) => this.onAgentStatusRequest(msg),
2937
+ permission_request_delivered: (msg) => this.permissionHandlers.onPermissionRequestDelivered(msg),
2938
+ session_terminate: (msg) => this.onSessionTerminate(msg),
2939
+ session_worker_abort: (msg) => this.onSessionWorkerAbort(msg),
2940
+ session_history_request: (msg) => this.deps.controlHandlers.handleSessionHistoryRequest({
2941
+ requestId: msg.requestId
2942
+ }),
2943
+ session_list: () => this.onSessionList(),
2944
+ permission_mode_change: (msg) => this.onPermissionModeChange(msg),
2945
+ session_subscribe: (msg) => this.onSessionSubscribe(msg),
2946
+ terminal_resize_request: (msg) => this.onTerminalResizeRequest(msg)
2947
+ };
2948
+ onAgentStatusRequest(msg) {
2949
+ const sid = msg.sessionId;
2950
+ const requestId = msg.requestId;
2951
+ if (sid) {
2952
+ const status = this.deps.agentStatusRegistry.get(sid);
2953
+ const statuses2 = status && this.deps.sessionManager.getSession(sid) ? [{ sessionId: sid, payload: status }] : [];
2954
+ this.deps.relaySend(JSON.stringify({ type: "agent_status_response", requestId, statuses: statuses2 }));
2955
+ serviceLogger.info({ sessionId: sid, count: statuses2.length }, "Agent status snapshot sent");
2956
+ return;
2957
+ }
2958
+ const statuses = [];
2959
+ for (const { sessionId, status } of this.deps.agentStatusRegistry.list()) {
2960
+ if (!this.deps.sessionManager.getSession(sessionId)) continue;
2961
+ statuses.push({ sessionId, payload: status });
2962
+ }
2963
+ this.deps.relaySend(JSON.stringify({ type: "agent_status_response", requestId, statuses }));
2964
+ serviceLogger.info({ count: statuses.length }, "Agent status snapshot sent");
2965
+ }
2966
+ onSessionTerminate(msg) {
2967
+ const sid = msg.sessionId;
2968
+ if (!sid) return;
2969
+ const result = terminateSessionByOwnership(this.deps, sid);
2970
+ serviceLogger.info(
2971
+ { sessionId: sid, success: result.success, action: result.action },
2972
+ "Session termination handled via relay"
2973
+ );
2974
+ if (result.action !== "terminate_hosted_pty") this.deps.broadcastSessionList();
2975
+ }
2976
+ onSessionWorkerAbort(msg) {
2977
+ const sid = msg.sessionId;
2978
+ if (!sid) return;
2979
+ const session = this.deps.sessionManager.getSession(sid);
2980
+ if (!session) {
2981
+ serviceLogger.warn({ sessionId: sid }, "session_worker_abort: session not found");
2982
+ return;
2983
+ }
2984
+ if (session.state === SessionState.TERMINATED) {
2985
+ serviceLogger.info({ sessionId: sid }, "session_worker_abort: already terminated, dropping");
2986
+ return;
2987
+ }
2988
+ if (session.mode === "pty") {
2989
+ const ts = this.deps.terminalSockets.get(sid);
2990
+ if (this.deps.hostedPtyRegistry.write(sid, "")) {
2991
+ serviceLogger.info({ sessionId: sid }, "session_worker_abort: Ctrl+C sent to hosted PTY");
2992
+ } else if (ts?.writable) {
2993
+ ts.write(serializeIpc({ type: "pty_input", sessionId: sid, data: "" }));
2994
+ serviceLogger.info({ sessionId: sid }, "session_worker_abort: Ctrl+C sent to PTY");
2995
+ } else {
2996
+ serviceLogger.warn(
2997
+ { sessionId: sid },
2998
+ "session_worker_abort: PTY terminal socket unavailable"
2999
+ );
3000
+ }
3001
+ return;
3002
+ }
3003
+ try {
3004
+ process.kill(session.pid, "SIGINT");
3005
+ serviceLogger.info(
3006
+ { sessionId: sid, pid: session.pid },
3007
+ "session_worker_abort: SIGINT sent to worker"
3008
+ );
3009
+ } catch (err) {
3010
+ serviceLogger.warn(
3011
+ { sessionId: sid, pid: session.pid, error: String(err) },
3012
+ "session_worker_abort: kill failed"
3013
+ );
3014
+ }
3015
+ }
3016
+ onSessionList() {
3017
+ this.deps.broadcastSessionList();
3018
+ serviceLogger.info("Session list sent via relay");
3019
+ }
3020
+ onPermissionModeChange(msg) {
3021
+ const sid = msg.sessionId;
3022
+ const mode = msg.mode;
3023
+ if (!sid) {
3024
+ serviceLogger.info(
3025
+ { mode },
3026
+ "Permission mode change received via relay (global, no sessionId)"
3027
+ );
3028
+ return;
3029
+ }
3030
+ const session = this.deps.sessionManager.getSession(sid);
3031
+ if (session?.mode !== "pty") {
3032
+ serviceLogger.info(
3033
+ { sessionId: sid, mode },
3034
+ "Permission mode change received for JSON session (no-op, not supported)"
3035
+ );
3036
+ return;
3037
+ }
3038
+ const ts = this.deps.terminalSockets.get(sid);
3039
+ if (this.deps.hostedPtyRegistry.write(sid, "\x1B[Z")) {
3040
+ serviceLogger.info(
3041
+ { sessionId: sid, mode },
3042
+ "Permission mode cycle: Shift+Tab sent to hosted PTY"
3043
+ );
3044
+ } else if (ts?.writable) {
3045
+ ts.write(serializeIpc({ type: "pty_input", sessionId: sid, data: "\x1B[Z" }));
3046
+ serviceLogger.info({ sessionId: sid, mode }, "Permission mode cycle: Shift+Tab sent to PTY");
3047
+ } else {
3048
+ serviceLogger.warn(
3049
+ { sessionId: sid },
3050
+ "Permission mode cycle: PTY terminal socket unavailable"
3051
+ );
3052
+ }
3053
+ }
3054
+ onSessionSubscribe(msg) {
3055
+ const sid = msg.sessionId;
3056
+ const requestId = msg.requestId;
3057
+ if (!sid) return;
3058
+ const ts = this.deps.terminalSockets.get(sid);
3059
+ if (this.deps.hostedPtyRegistry.snapshot(sid, requestId)) {
3060
+ serviceLogger.info({ sessionId: sid, requestId }, "Subscribe handled by hosted PTY");
3061
+ } else if (ts?.writable) {
3062
+ ts.write(serializeIpc({ type: "pty_subscribe", sessionId: sid, requestId }));
3063
+ serviceLogger.info({ sessionId: sid, requestId }, "Subscribe forwarded to terminal");
3064
+ } else {
3065
+ serviceLogger.warn({ sessionId: sid }, "Subscribe failed: terminal socket not available");
3066
+ }
3067
+ }
3068
+ onTerminalResizeRequest(msg) {
3069
+ const sid = msg.sessionId;
3070
+ const cols = msg.cols;
3071
+ const rows = msg.rows;
3072
+ if (!sid || !cols || !rows) return;
3073
+ if (!this.deps.hostedPtyRegistry.resize(sid, cols, rows)) {
3074
+ serviceLogger.debug({ sessionId: sid, cols, rows }, "Resize request ignored: not hosted PTY");
3075
+ }
3076
+ }
3077
+ };
3078
+
3079
+ // src/serve/json-observer.ts
3080
+ var JsonObserver = class {
3081
+ constructor(deps) {
3082
+ this.deps = deps;
3083
+ }
3084
+ deps;
3085
+ // 用户消息注入 worker(relay-router 收到 user_input)→ 进入 WORKING
3086
+ onTurnStart(sessionId) {
3087
+ this.deps.changeSessionState(sessionId, SessionState.WORKING);
3088
+ this.deps.emitAgentStatus?.(sessionId, "thinking");
3089
+ }
3090
+ // stream-json result event 到达 → turn 结束回 IDLE
3091
+ onTurnResult(sessionId) {
3092
+ this.deps.changeSessionState(sessionId, SessionState.IDLE);
3093
+ this.deps.emitAgentStatus?.(sessionId, "idle");
3094
+ }
3095
+ // control_request event 到达 → worker 阻塞等审批
3096
+ onApprovalRequested(sessionId) {
3097
+ this.deps.changeSessionState(sessionId, SessionState.WAITING_APPROVAL);
3098
+ this.deps.emitAgentStatus?.(sessionId, "waiting_permission");
3099
+ }
3100
+ // 观察通道失联 → ERROR,待人工干预或后续 terminate
3101
+ onChannelBroken(sessionId) {
3102
+ this.deps.changeSessionState(sessionId, SessionState.ERROR);
3103
+ }
3104
+ };
3105
+
3106
+ // src/serve/permission-broker.ts
3107
+ var PermissionBroker = class {
3108
+ pending = /* @__PURE__ */ new Map();
3109
+ request(request) {
3110
+ const existing = this.pending.get(request.requestId);
3111
+ if (existing) {
3112
+ return Promise.resolve({
3113
+ behavior: "deny",
3114
+ message: "Duplicate permission request id."
3115
+ });
3116
+ }
3117
+ return new Promise((resolve) => {
3118
+ this.pending.set(request.requestId, {
3119
+ ...request,
3120
+ source: "hook",
3121
+ resolve,
3122
+ createdAt: Date.now()
3123
+ });
3124
+ });
3125
+ }
3126
+ registerWorkerRequest(request, onDecision) {
3127
+ const existing = this.pending.get(request.requestId);
3128
+ if (existing) {
3129
+ onDecision({
3130
+ behavior: "deny",
3131
+ message: "Duplicate permission request id."
3132
+ });
3133
+ return false;
3134
+ }
3135
+ this.pending.set(request.requestId, {
3136
+ ...request,
3137
+ source: "worker",
3138
+ resolve: onDecision,
3139
+ createdAt: Date.now()
3140
+ });
3141
+ return true;
3142
+ }
3143
+ resolve(requestId, decision) {
3144
+ const pending = this.pending.get(requestId);
3145
+ if (!pending) return false;
3146
+ this.pending.delete(requestId);
3147
+ pending.resolve(decision);
3148
+ return true;
3149
+ }
3150
+ markDelivered(requestId) {
3151
+ const pending = this.pending.get(requestId);
3152
+ if (!pending) return false;
3153
+ pending.deliveredAt = Date.now();
3154
+ return true;
3155
+ }
3156
+ get(requestId) {
3157
+ const pending = this.pending.get(requestId);
3158
+ if (!pending) return null;
3159
+ return {
3160
+ requestId: pending.requestId,
3161
+ sessionId: pending.sessionId,
3162
+ provider: pending.provider,
3163
+ source: pending.source,
3164
+ toolName: pending.toolName,
3165
+ input: pending.input,
3166
+ createdAt: pending.createdAt,
3167
+ ...pending.deliveredAt !== void 0 ? { deliveredAt: pending.deliveredAt } : {}
3168
+ };
3169
+ }
3170
+ cleanupSession(sessionId, reason) {
3171
+ for (const [requestId, pending] of this.pending) {
3172
+ if (pending.sessionId !== sessionId) continue;
3173
+ this.pending.delete(requestId);
3174
+ pending.resolve({ behavior: "deny", message: reason });
3175
+ serviceLogger.info({ sessionId, requestId, reason }, "Pending hook permission dropped");
3176
+ }
3177
+ }
3178
+ listSession(sessionId) {
3179
+ const out = [];
3180
+ for (const pending of this.pending.values()) {
3181
+ if (pending.sessionId !== sessionId) continue;
3182
+ out.push({
3183
+ requestId: pending.requestId,
3184
+ sessionId: pending.sessionId,
3185
+ provider: pending.provider,
3186
+ source: pending.source,
3187
+ toolName: pending.toolName,
3188
+ input: pending.input,
3189
+ createdAt: pending.createdAt,
3190
+ ...pending.deliveredAt !== void 0 ? { deliveredAt: pending.deliveredAt } : {}
3191
+ });
3192
+ }
3193
+ return out;
3194
+ }
3195
+ };
3196
+
3197
+ // src/serve/hook-event-router.ts
3198
+ function hookPayloadRecord(value) {
3199
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3200
+ }
3201
+ function toolNameFromPayload(payload) {
3202
+ return typeof payload.toolName === "string" ? payload.toolName : typeof payload.tool_name === "string" ? payload.tool_name : "unknown";
3203
+ }
3204
+ function toolInputFromPayload(payload) {
3205
+ return hookPayloadRecord(payload.input ?? payload.tool_input);
3206
+ }
3207
+ var HookEventRouter = class {
3208
+ constructor(deps) {
3209
+ this.deps = deps;
3210
+ }
3211
+ deps;
3212
+ handle(event) {
3213
+ switch (event.event) {
3214
+ case "SessionStart":
3215
+ this.deps.changeSessionState(event.sessionId, SessionState.IDLE);
3216
+ this.forwardAgentStatus(event, "idle");
3217
+ break;
3218
+ case "UserPromptSubmit":
3219
+ this.deps.changeSessionState(event.sessionId, SessionState.WORKING);
3220
+ this.forwardAgentStatus(event, "thinking");
3221
+ break;
3222
+ case "PostToolUse":
3223
+ case "PostToolUseFailure":
3224
+ this.deps.changeSessionState(event.sessionId, SessionState.WORKING);
3225
+ this.forwardAgentStatus(event, "outputting");
3226
+ break;
3227
+ case "Stop":
3228
+ this.deps.changeSessionState(event.sessionId, SessionState.IDLE);
3229
+ this.forwardAgentStatus(event, "idle");
3230
+ break;
3231
+ case "PermissionRequest":
3232
+ this.forwardPermissionRequest(event);
3233
+ break;
3234
+ case "PreToolUse":
3235
+ this.forwardToolUse(event);
3236
+ break;
3237
+ default:
3238
+ serviceLogger.debug(
3239
+ { sessionId: event.sessionId, provider: event.provider, event: event.event },
3240
+ "Unknown provider hook event ignored"
3241
+ );
3242
+ break;
3243
+ }
3244
+ }
3245
+ onPermissionResolved(sessionId, provider, requestId, outcome, context) {
3246
+ this.deps.changeSessionState(sessionId, SessionState.WORKING);
3247
+ if (outcome === "deny") {
3248
+ this.deps.changeSessionState(sessionId, SessionState.IDLE);
3249
+ }
3250
+ this.forwardAgentStatus(
3251
+ {
3252
+ sessionId,
3253
+ provider,
3254
+ event: "PermissionResolved",
3255
+ requestId,
3256
+ payload: {}
3257
+ },
3258
+ outcome === "allow" ? "tool_use" : "idle",
3259
+ {
3260
+ toolName: context?.toolName,
3261
+ toolInput: context?.toolInput,
3262
+ permissionResolution: { requestId, outcome }
3263
+ }
3264
+ );
3265
+ serviceLogger.info({ sessionId, requestId, outcome }, "Hook permission resolved");
3266
+ }
3267
+ forwardPermissionRequest(event) {
3268
+ const requestId = event.requestId ?? `${event.sessionId}:${Date.now()}`;
3269
+ const toolName = toolNameFromPayload(event.payload);
3270
+ const input = toolInputFromPayload(event.payload);
3271
+ this.deps.changeSessionState(event.sessionId, SessionState.WAITING_APPROVAL);
3272
+ this.forwardAgentStatus(event, "waiting_permission", {
3273
+ toolName,
3274
+ toolInput: input,
3275
+ permissionRequest: {
3276
+ requestId,
3277
+ toolName,
3278
+ input
3279
+ }
3280
+ });
3281
+ const seq = this.deps.nextSeq?.(event.sessionId) ?? new SeqCounter(event.sessionId).next();
3282
+ const envelope = buildMessage(
3283
+ "tool_use_request",
3284
+ event.sessionId,
3285
+ seq,
3286
+ {
3287
+ toolName,
3288
+ toolId: requestId,
3289
+ parameters: input
3290
+ },
3291
+ "proxy"
3292
+ );
3293
+ this.deps.relayConnection.sendEnvelope(envelope);
3294
+ }
3295
+ forwardToolUse(event) {
3296
+ const toolName = toolNameFromPayload(event.payload);
3297
+ const input = toolInputFromPayload(event.payload);
3298
+ this.forwardAgentStatus(event, "tool_use", {
3299
+ toolName,
3300
+ toolInput: input
3301
+ });
3302
+ }
3303
+ forwardAgentStatus(event, phase, extra) {
3304
+ const payload = {
3305
+ provider: event.provider,
3306
+ phase,
3307
+ seq: this.nextSeq(event.sessionId),
3308
+ updatedAt: Date.now(),
3309
+ ...extra
3310
+ };
3311
+ this.deps.agentStatusRegistry.set(event.sessionId, payload);
3312
+ this.deps.relayConnection.sendRaw(
3313
+ JSON.stringify({
3314
+ type: "agent_status",
3315
+ sessionId: event.sessionId,
3316
+ payload
3317
+ })
3318
+ );
3319
+ }
3320
+ nextSeq(sessionId) {
3321
+ return this.deps.nextSeq?.(sessionId) ?? new SeqCounter(sessionId).next();
3322
+ }
3323
+ };
3324
+
3325
+ // src/serve/agent-status-registry.ts
3326
+ var AgentStatusRegistry = class {
3327
+ statuses = /* @__PURE__ */ new Map();
3328
+ set(sessionId, status) {
3329
+ const current = this.statuses.get(sessionId);
3330
+ if (current && current.seq > status.seq) return;
3331
+ this.statuses.set(sessionId, status);
3332
+ }
3333
+ get(sessionId) {
3334
+ return this.statuses.get(sessionId) ?? null;
3335
+ }
3336
+ list() {
3337
+ return Array.from(this.statuses, ([sessionId, status]) => ({ sessionId, status }));
3338
+ }
3339
+ delete(sessionId) {
3340
+ this.statuses.delete(sessionId);
3341
+ }
3342
+ };
3343
+
3344
+ // src/serve/session-broadcast.ts
3345
+ function toSessionListPayload(s) {
3346
+ return {
3347
+ sessionId: s.id,
3348
+ mode: s.mode,
3349
+ provider: s.provider,
3350
+ ...s.ptyOwner !== void 0 ? { ptyOwner: s.ptyOwner } : {},
3351
+ state: s.state,
3352
+ lastActive: s.updatedAt,
3353
+ ...s.name !== void 0 ? { name: s.name } : {}
3354
+ };
3355
+ }
3356
+ function pushSessionStatus(relay, sessionManager, sessionId) {
3357
+ const session = sessionManager.getSession(sessionId);
3358
+ if (!session) return;
3359
+ try {
3360
+ const envelope = buildMessage(
3361
+ "session_status",
3362
+ session.id,
3363
+ Date.now(),
3364
+ { sessionId: session.id, state: session.state, lastActive: session.updatedAt },
3365
+ "proxy"
3366
+ );
3367
+ relay.sendEnvelope(envelope);
3368
+ } catch (err) {
3369
+ serviceLogger.debug({ sessionId, error: String(err) }, "Failed to push session_status");
3370
+ }
3371
+ }
3372
+ function broadcastSessionList(relay, sessionManager) {
3373
+ relay.sendRaw(
3374
+ JSON.stringify({
3375
+ type: "session_list",
3376
+ sessionId: "",
3377
+ seq: 0,
3378
+ timestamp: Date.now(),
3379
+ source: "proxy",
3380
+ version: "1",
3381
+ payload: { sessions: sessionManager.listSessions().map(toSessionListPayload) }
3382
+ })
3383
+ );
3384
+ }
3385
+ function broadcastSessionSync(relay, session) {
3386
+ relay.sendRaw(
3387
+ JSON.stringify({
3388
+ type: "session_sync",
3389
+ sessions: [
3390
+ {
3391
+ id: session.id,
3392
+ mode: session.mode,
3393
+ provider: session.provider,
3394
+ ...session.ptyOwner !== void 0 ? { ptyOwner: session.ptyOwner } : {},
3395
+ state: session.state
3396
+ }
3397
+ ]
3398
+ })
3399
+ );
3400
+ }
3401
+ function changeSessionState(sessionManager, relay, sessionId, next) {
3402
+ if (!sessionManager.getSession(sessionId)) return false;
3403
+ const changed = sessionManager.updateState(sessionId, next);
3404
+ if (changed) pushSessionStatus(relay, sessionManager, sessionId);
3405
+ return changed;
3406
+ }
3407
+
3408
+ // src/serve/service-files.ts
3409
+ import { execSync } from "child_process";
3410
+ import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
3411
+ import { hostname } from "os";
3412
+ import { connect as connect2 } from "net";
3413
+ function tryConnectSocket(sockPath) {
3414
+ return new Promise((resolve) => {
3415
+ const s = connect2(sockPath);
3416
+ s.on("connect", () => resolve(s));
3417
+ s.on("error", () => resolve(null));
3418
+ });
3419
+ }
3420
+ function isProcessAlive(pid) {
3421
+ try {
3422
+ process.kill(pid, 0);
3423
+ return true;
3424
+ } catch {
3425
+ return false;
3426
+ }
3427
+ }
3428
+ async function cleanupStaleResources() {
3429
+ if (existsSync5(SOCK_PATH)) {
3430
+ const existing = await tryConnectSocket(SOCK_PATH);
3431
+ if (existing) {
3432
+ existing.destroy();
3433
+ const msg = `Another service is already running on ${SOCK_PATH}`;
3434
+ serviceLogger.error(msg);
3435
+ console.error(msg);
3436
+ process.exit(1);
3437
+ }
3438
+ unlinkSync2(SOCK_PATH);
3439
+ serviceLogger.info("Removed stale socket file");
3440
+ }
3441
+ if (existsSync5(PID_PATH)) {
3442
+ const pidStr = readFileSync5(PID_PATH, "utf-8").trim();
3443
+ const pid = parseInt(pidStr, 10);
3444
+ if (!isNaN(pid) && isProcessAlive(pid)) {
3445
+ const msg = `Another service is already running with PID ${pid}`;
3446
+ serviceLogger.error(msg);
3447
+ console.error(msg);
3448
+ process.exit(1);
3449
+ }
3450
+ unlinkSync2(PID_PATH);
3451
+ serviceLogger.info("Removed stale PID file");
3452
+ }
3453
+ }
3454
+ function getProxyName() {
3455
+ return process.env.DEV_ANYWHERE_PROXY_NAME || getComputerName() || hostname();
3456
+ }
3457
+ function getComputerName() {
3458
+ try {
3459
+ return execSync("scutil --get ComputerName", { stdio: ["pipe", "pipe", "ignore"] }).toString().trim() || null;
3460
+ } catch {
3461
+ return null;
3462
+ }
3463
+ }
3464
+
3465
+ // src/serve/pty-state-guard.ts
3466
+ function shouldPromotePtyActivityToWorking(session, pendingApprovalCount) {
3467
+ if (!session || session.mode !== "pty") return false;
3468
+ if (pendingApprovalCount > 0) return false;
3469
+ return session.state === SessionState.IDLE || session.state === SessionState.WAITING_APPROVAL;
3470
+ }
3471
+
3472
+ // src/serve/pty-semantic-lifecycle.ts
3473
+ function resolvePtySemanticSessionTransitions(currentState, semanticState) {
3474
+ if (semanticState !== "turn_complete") return [];
3475
+ if (currentState === SessionState.WAITING_APPROVAL) {
3476
+ return [SessionState.IDLE];
3477
+ }
3478
+ if (currentState === SessionState.WORKING) {
3479
+ return [SessionState.IDLE];
3480
+ }
3481
+ return [];
3482
+ }
3483
+
3484
+ // src/serve/terminal-ipc.ts
3485
+ function handleTerminalConnection(socket, deps) {
3486
+ const {
3487
+ sessionManager,
3488
+ workerRegistry,
3489
+ terminalSockets,
3490
+ hostedPtyRegistry,
3491
+ relayConnection,
3492
+ controlHandlers,
3493
+ agentStatusRegistry,
3494
+ permissionBroker,
3495
+ createHookContext,
3496
+ emitAgentStatus,
3497
+ resolveInterruptedApprovals: resolveInterruptedApprovals2,
3498
+ config
3499
+ } = deps;
3500
+ createIpcReader(
3501
+ socket,
3502
+ (msg) => {
3503
+ switch (msg.type) {
3504
+ case "session_create_request": {
3505
+ if (msg.mode !== "pty") {
3506
+ socket.write(
3507
+ serializeIpc({
3508
+ type: "session_create_response",
3509
+ sessionId: "",
3510
+ error: `Unsupported mode via IPC: ${msg.mode}`
3511
+ })
3512
+ );
3513
+ break;
3514
+ }
3515
+ const provider = msg.provider;
3516
+ const existing = msg.sessionId ? sessionManager.getSession(msg.sessionId) : void 0;
3517
+ const session = existing ?? sessionManager.createSession(
3518
+ "pty",
3519
+ msg.cwd,
3520
+ msg.pid,
3521
+ msg.name,
3522
+ msg.sessionId,
3523
+ provider,
3524
+ "local-terminal"
3525
+ );
3526
+ if (existing) {
3527
+ sessionManager.setPid(session.id, msg.pid);
3528
+ }
3529
+ socket.write(
3530
+ serializeIpc({
3531
+ type: "session_create_response",
3532
+ sessionId: session.id,
3533
+ hook: createHookContext(session.id, provider)
3534
+ })
3535
+ );
3536
+ serviceLogger.info(
3537
+ { sessionId: session.id, mode: "pty", provider },
3538
+ "PTY session created"
3539
+ );
3540
+ break;
3541
+ }
3542
+ case "service_status_request": {
3543
+ const relayStatus = relayConnection.getStatus();
3544
+ const sessions = sessionManager.listSessions();
3545
+ socket.write(
3546
+ serializeIpc({
3547
+ type: "service_status_response",
3548
+ config,
3549
+ relay: relayStatus,
3550
+ sessions: sessions.map((s) => ({
3551
+ id: s.id,
3552
+ mode: s.mode,
3553
+ provider: s.provider,
3554
+ state: s.state,
3555
+ createdAt: new Date(s.createdAt).toISOString(),
3556
+ ...s.name !== void 0 ? { name: s.name } : {},
3557
+ hasWorker: workerRegistry.has(s.id)
3558
+ }))
3559
+ })
3560
+ );
3561
+ break;
3562
+ }
3563
+ case "pty_title_change": {
3564
+ if (!sessionManager.getSession(msg.sessionId)) break;
3565
+ relayConnection.sendRaw(
3566
+ JSON.stringify({
3567
+ type: "terminal_title",
3568
+ sessionId: msg.sessionId,
3569
+ title: msg.title
3570
+ })
3571
+ );
3572
+ break;
3573
+ }
3574
+ case "pty_semantic_event": {
3575
+ if (!sessionManager.getSession(msg.sessionId)) break;
3576
+ const logPayload = {
3577
+ sessionId: msg.sessionId,
3578
+ state: msg.state,
3579
+ ...msg.title !== void 0 ? { title: msg.title } : {},
3580
+ ...msg.tool !== void 0 ? { tool: msg.tool } : {}
3581
+ };
3582
+ if (msg.state === "approval_wait" || msg.state === "turn_complete") {
3583
+ serviceLogger.info(logPayload, "PTY semantic event received");
3584
+ } else {
3585
+ serviceLogger.debug(logPayload, "PTY semantic event received");
3586
+ }
3587
+ if (msg.state === "approval_wait") {
3588
+ changeSessionState(
3589
+ sessionManager,
3590
+ relayConnection,
3591
+ msg.sessionId,
3592
+ SessionState.WAITING_APPROVAL
3593
+ );
3594
+ } else if (msg.state === "working" || msg.state === "mid_pause") {
3595
+ const session = sessionManager.getSession(msg.sessionId);
3596
+ const pendingApprovals = permissionBroker.listSession(msg.sessionId);
3597
+ if (shouldPromotePtyActivityToWorking(session, pendingApprovals.length)) {
3598
+ changeSessionState(
3599
+ sessionManager,
3600
+ relayConnection,
3601
+ msg.sessionId,
3602
+ SessionState.WORKING
3603
+ );
3604
+ } else if (session?.state === SessionState.WAITING_APPROVAL) {
3605
+ serviceLogger.debug(
3606
+ {
3607
+ sessionId: msg.sessionId,
3608
+ ptyState: msg.state,
3609
+ pendingApprovals: pendingApprovals.length
3610
+ },
3611
+ "PTY working signal ignored while permission approval is pending"
3612
+ );
3613
+ }
3614
+ }
3615
+ if (msg.state === "turn_complete") {
3616
+ resolveInterruptedApprovals2(msg.sessionId);
3617
+ const session = sessionManager.getSession(msg.sessionId);
3618
+ const transitions = resolvePtySemanticSessionTransitions(session?.state, msg.state);
3619
+ for (const next of transitions) {
3620
+ changeSessionState(sessionManager, relayConnection, msg.sessionId, next);
3621
+ }
3622
+ emitAgentStatus(msg.sessionId, "idle");
3623
+ }
3624
+ relayConnection.sendRaw(
3625
+ JSON.stringify({
3626
+ type: "pty_state",
3627
+ sessionId: msg.sessionId,
3628
+ payload: {
3629
+ state: msg.state,
3630
+ ...msg.title !== void 0 ? { title: msg.title } : {},
3631
+ ...msg.tool !== void 0 ? { tool: msg.tool } : {}
3632
+ }
3633
+ })
3634
+ );
3635
+ break;
3636
+ }
3637
+ case "pty_resize": {
3638
+ if (!sessionManager.getSession(msg.sessionId)) break;
3639
+ relayConnection.sendRaw(
3640
+ JSON.stringify({
3641
+ type: "terminal_resize",
3642
+ sessionId: msg.sessionId,
3643
+ cols: msg.cols,
3644
+ rows: msg.rows
3645
+ })
3646
+ );
3647
+ break;
3648
+ }
3649
+ case "session_terminate_request": {
3650
+ const result = terminateSessionByOwnership(
3651
+ {
3652
+ sessionManager,
3653
+ workerRegistry,
3654
+ controlHandlers,
3655
+ terminalSockets,
3656
+ hostedPtyRegistry,
3657
+ agentStatusRegistry
3658
+ },
3659
+ msg.sessionId
3660
+ );
3661
+ socket.write(
3662
+ serializeIpc({
3663
+ type: "session_terminate_response",
3664
+ sessionId: msg.sessionId,
3665
+ success: result.success
3666
+ })
3667
+ );
3668
+ serviceLogger.info(
3669
+ { sessionId: msg.sessionId, success: result.success, action: result.action },
3670
+ "Session termination request handled"
3671
+ );
3672
+ break;
3673
+ }
3674
+ case "pty_register": {
3675
+ if (!sessionManager.getSession(msg.sessionId)) {
3676
+ serviceLogger.warn(
3677
+ { sessionId: msg.sessionId },
3678
+ "PTY register ignored: session missing"
3679
+ );
3680
+ break;
3681
+ }
3682
+ sessionManager.setPid(msg.sessionId, msg.pid);
3683
+ terminalSockets.set(msg.sessionId, socket);
3684
+ socket.write(
3685
+ serializeIpc({
3686
+ type: "bridge_status",
3687
+ connected: relayConnection.getStatus().connected
3688
+ })
3689
+ );
3690
+ const session = sessionManager.getSession(msg.sessionId);
3691
+ if (session) {
3692
+ broadcastSessionSync(relayConnection, session);
3693
+ }
3694
+ broadcastSessionList(relayConnection, sessionManager);
3695
+ serviceLogger.info({ sessionId: msg.sessionId }, "PTY session registered");
3696
+ break;
3697
+ }
3698
+ case "pty_deregister": {
3699
+ relayConnection.sendRaw(
3700
+ JSON.stringify({
3701
+ type: "pty_state",
3702
+ sessionId: msg.sessionId,
3703
+ payload: { state: "turn_complete" }
3704
+ })
3705
+ );
3706
+ sessionManager.terminateSession(msg.sessionId);
3707
+ terminalSockets.delete(msg.sessionId);
3708
+ controlHandlers.cleanup(msg.sessionId);
3709
+ agentStatusRegistry.delete(msg.sessionId);
3710
+ broadcastSessionList(relayConnection, sessionManager);
3711
+ serviceLogger.info({ sessionId: msg.sessionId }, "PTY session deregistered");
3712
+ break;
3713
+ }
3714
+ case "pty_input": {
3715
+ if (!sessionManager.getSession(msg.sessionId)) break;
3716
+ const targetSocket = terminalSockets.get(msg.sessionId);
3717
+ if (hostedPtyRegistry.write(msg.sessionId, msg.data)) {
3718
+ break;
3719
+ }
3720
+ if (targetSocket?.writable) {
3721
+ targetSocket.write(
3722
+ serializeIpc({
3723
+ type: "pty_input",
3724
+ sessionId: msg.sessionId,
3725
+ data: msg.data
3726
+ })
3727
+ );
3728
+ }
3729
+ break;
3730
+ }
3731
+ case "session_status_update": {
3732
+ changeSessionState(sessionManager, relayConnection, msg.sessionId, msg.state);
3733
+ break;
3734
+ }
3735
+ case "pty_snapshot": {
3736
+ if (!sessionManager.getSession(msg.sessionId)) break;
3737
+ relayConnection.sendRaw(
3738
+ JSON.stringify({
3739
+ type: "session_snapshot",
3740
+ sessionId: msg.sessionId,
3741
+ cols: msg.cols,
3742
+ rows: msg.rows,
3743
+ data: msg.data,
3744
+ outputSeq: msg.outputSeq,
3745
+ requestId: msg.requestId
3746
+ })
3747
+ );
3748
+ serviceLogger.info(
3749
+ { sessionId: msg.sessionId, cols: msg.cols, rows: msg.rows },
3750
+ "Session snapshot forwarded to relay"
3751
+ );
3752
+ break;
3753
+ }
3754
+ default: {
3755
+ serviceLogger.warn({ type: msg.type }, "Unhandled IPC message type");
3756
+ }
3757
+ }
3758
+ },
3759
+ (sessionId, data, outputSeq) => {
3760
+ if (!sessionManager.getSession(sessionId)) return;
3761
+ const sessionIdBuf = Buffer.from(sessionId, "utf-8");
3762
+ const wsFrame = Buffer.alloc(1 + sessionIdBuf.length + 4 + data.length);
3763
+ wsFrame[0] = sessionIdBuf.length;
3764
+ sessionIdBuf.copy(wsFrame, 1);
3765
+ wsFrame.writeUInt32LE(outputSeq, 1 + sessionIdBuf.length);
3766
+ data.copy(wsFrame, 1 + sessionIdBuf.length + 4);
3767
+ relayConnection.sendBinary(wsFrame);
3768
+ }
3769
+ );
3770
+ socket.on("close", () => {
3771
+ for (const [sessionId, terminalSocket] of terminalSockets) {
3772
+ if (terminalSocket === socket) {
3773
+ terminalSockets.delete(sessionId);
3774
+ const session = sessionManager.getSession(sessionId);
3775
+ if (!session) {
3776
+ serviceLogger.info({ sessionId }, "Terminal socket closed, session already cleaned");
3777
+ continue;
3778
+ }
3779
+ if (session.mode === "pty" && session.pid && isProcessAlive(session.pid)) {
3780
+ serviceLogger.info(
3781
+ { sessionId, pid: session.pid },
3782
+ "Terminal socket closed but process alive, skipping cleanup"
3783
+ );
3784
+ continue;
3785
+ }
3786
+ relayConnection.sendRaw(
3787
+ JSON.stringify({
3788
+ type: "pty_state",
3789
+ sessionId,
3790
+ payload: { state: "turn_complete" }
3791
+ })
3792
+ );
3793
+ sessionManager.terminateSession(sessionId);
3794
+ controlHandlers.cleanup(sessionId);
3795
+ agentStatusRegistry.delete(sessionId);
3796
+ broadcastSessionList(relayConnection, sessionManager);
3797
+ serviceLogger.info(
3798
+ { sessionId },
3799
+ "PTY session cleaned up on socket close (crash fallback)"
3800
+ );
3801
+ }
3802
+ }
3803
+ });
3804
+ socket.on("error", (err) => {
3805
+ serviceLogger.warn({ error: String(err) }, "Client socket error");
3806
+ });
3807
+ }
3808
+
3809
+ // src/serve/hook-registry.ts
3810
+ import { createHash, randomBytes } from "crypto";
3811
+ function hashToken(token) {
3812
+ return createHash("sha256").update(token).digest("hex");
3813
+ }
3814
+ function randomSecret() {
3815
+ return randomBytes(32).toString("base64url");
3816
+ }
3817
+ var HookRegistry = class {
3818
+ bindingsBySession = /* @__PURE__ */ new Map();
3819
+ registerSession(sessionId, provider, options = {}) {
3820
+ const now = options.now ?? Date.now();
3821
+ const token = randomSecret();
3822
+ const marker = randomSecret();
3823
+ this.bindingsBySession.set(sessionId, {
3824
+ sessionId,
3825
+ provider,
3826
+ marker,
3827
+ tokenHash: hashToken(token),
3828
+ createdAt: now,
3829
+ ...options.ttlMs ? { expiresAt: now + options.ttlMs } : {}
3830
+ });
3831
+ return { sessionId, provider, marker, token };
3832
+ }
3833
+ verify(options) {
3834
+ const binding = this.bindingsBySession.get(options.sessionId);
3835
+ if (!binding) return null;
3836
+ if (options.provider && binding.provider !== options.provider) return null;
3837
+ if (binding.marker !== options.marker) return null;
3838
+ if (binding.tokenHash !== hashToken(options.token)) return null;
3839
+ if (binding.expiresAt && (options.now ?? Date.now()) > binding.expiresAt) return null;
3840
+ return binding;
3841
+ }
3842
+ getSession(sessionId) {
3843
+ return this.bindingsBySession.get(sessionId) ?? null;
3844
+ }
3845
+ unregisterSession(sessionId) {
3846
+ this.bindingsBySession.delete(sessionId);
3847
+ }
3848
+ };
3849
+
3850
+ // src/serve/hook-server.ts
3851
+ import { createServer } from "http";
3852
+ function getBearerToken(req) {
3853
+ const header = req.headers.authorization;
3854
+ if (!header?.startsWith("Bearer ")) return null;
3855
+ return header.slice("Bearer ".length).trim() || null;
3856
+ }
3857
+ function asProvider(value) {
3858
+ return value === "claude" || value === "codex" ? value : null;
3859
+ }
3860
+ function asRecord(value) {
3861
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3862
+ }
3863
+ var HookServer = class {
3864
+ constructor(options) {
3865
+ this.options = options;
3866
+ this.host = options.host ?? "127.0.0.1";
3867
+ this.maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
3868
+ }
3869
+ options;
3870
+ server = null;
3871
+ host;
3872
+ maxBodyBytes;
3873
+ start() {
3874
+ if (this.server) return Promise.resolve();
3875
+ this.server = createServer((req, res) => {
3876
+ this.handle(req, res).catch((err) => {
3877
+ serviceLogger.error({ err: String(err) }, "Hook request failed");
3878
+ this.writeJson(res, 500, { error: "internal_error" });
3879
+ });
3880
+ });
3881
+ return new Promise((resolve, reject) => {
3882
+ const onError = (err) => {
3883
+ this.server?.off("listening", onListening);
3884
+ reject(err);
3885
+ };
3886
+ const onListening = () => {
3887
+ this.server?.off("error", onError);
3888
+ serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
3889
+ resolve();
3890
+ };
3891
+ this.server.once("error", onError);
3892
+ this.server.once("listening", onListening);
3893
+ this.server.listen(this.options.port, this.host);
3894
+ });
3895
+ }
3896
+ close() {
3897
+ if (!this.server) return Promise.resolve();
3898
+ const server = this.server;
3899
+ this.server = null;
3900
+ return new Promise((resolve, reject) => {
3901
+ server.close((err) => err ? reject(err) : resolve());
3902
+ });
3903
+ }
3904
+ getListeningPort() {
3905
+ const address = this.server?.address();
3906
+ if (!address || typeof address === "string") return null;
3907
+ return address.port;
3908
+ }
3909
+ async handle(req, res) {
3910
+ if (req.method !== "POST" || req.url !== "/hook") {
3911
+ this.writeJson(res, 404, { error: "not_found" });
3912
+ return;
3913
+ }
3914
+ const token = getBearerToken(req);
3915
+ if (!token) {
3916
+ this.writeJson(res, 401, { error: "missing_token" });
3917
+ return;
3918
+ }
3919
+ const body = await this.readBody(req);
3920
+ const parsed = JSON.parse(body);
3921
+ const provider = asProvider(parsed.provider);
3922
+ if (typeof parsed.sessionId !== "string" || typeof parsed.marker !== "string" || typeof parsed.event !== "string" || !provider) {
3923
+ this.writeJson(res, 400, { error: "invalid_hook_payload" });
3924
+ return;
3925
+ }
3926
+ const binding = this.options.registry.verify({
3927
+ sessionId: parsed.sessionId,
3928
+ marker: parsed.marker,
3929
+ token,
3930
+ provider
3931
+ });
3932
+ if (!binding) {
3933
+ this.writeJson(res, 403, { error: "invalid_hook_credentials" });
3934
+ return;
3935
+ }
3936
+ if (this.options.isSessionActive && !this.options.isSessionActive(binding.sessionId)) {
3937
+ serviceLogger.info(
3938
+ { sessionId: binding.sessionId, provider: binding.provider, event: parsed.event },
3939
+ "Provider hook ignored for inactive session"
3940
+ );
3941
+ this.writeProviderResponse(res, this.toNeutralProviderResponse(provider, parsed.event));
3942
+ return;
3943
+ }
3944
+ const payload = asRecord(parsed.payload);
3945
+ const requestId = typeof parsed.requestId === "string" ? parsed.requestId : typeof payload.tool_use_id === "string" ? payload.tool_use_id : void 0;
3946
+ const event = {
3947
+ sessionId: binding.sessionId,
3948
+ provider: binding.provider,
3949
+ event: parsed.event,
3950
+ ...requestId !== void 0 ? { requestId } : {},
3951
+ payload
3952
+ };
3953
+ if (event.event === "PermissionRequest") {
3954
+ await this.handlePermissionRequest(event, res);
3955
+ return;
3956
+ }
3957
+ this.options.onEvent?.(event);
3958
+ this.writeProviderResponse(res, this.toNeutralProviderResponse(event.provider, event.event));
3959
+ }
3960
+ async handlePermissionRequest(event, res) {
3961
+ const requestId = event.requestId ?? (typeof event.payload.tool_use_id === "string" ? event.payload.tool_use_id : void 0) ?? `${event.sessionId}:${Date.now()}`;
3962
+ const toolName = typeof event.payload.toolName === "string" ? event.payload.toolName : typeof event.payload.tool_name === "string" ? event.payload.tool_name : "unknown";
3963
+ const input = asRecord(event.payload.input ?? event.payload.tool_input);
3964
+ this.options.onEvent?.({ ...event, requestId });
3965
+ const decision = await this.options.permissionBroker.request({
3966
+ requestId,
3967
+ sessionId: event.sessionId,
3968
+ provider: event.provider,
3969
+ toolName,
3970
+ input
3971
+ });
3972
+ this.writeJson(res, 200, this.toProviderDecision(event.event, decision));
3973
+ }
3974
+ toProviderDecision(eventName, decision) {
3975
+ if (eventName === "PreToolUse") {
3976
+ return {
3977
+ hookSpecificOutput: {
3978
+ hookEventName: "PreToolUse",
3979
+ permissionDecision: decision.behavior,
3980
+ ...decision.message ? { permissionDecisionReason: decision.message } : {}
3981
+ }
3982
+ };
3983
+ }
3984
+ return {
3985
+ hookSpecificOutput: {
3986
+ hookEventName: "PermissionRequest",
3987
+ decision
3988
+ }
3989
+ };
3990
+ }
3991
+ toNeutralProviderResponse(provider, eventName) {
3992
+ if (eventName === "PreToolUse") {
3993
+ if (provider === "codex") {
3994
+ return null;
3995
+ }
3996
+ return {
3997
+ hookSpecificOutput: {
3998
+ hookEventName: "PreToolUse",
3999
+ permissionDecision: "defer"
4000
+ }
4001
+ };
4002
+ }
4003
+ return null;
4004
+ }
4005
+ writeProviderResponse(res, payload) {
4006
+ if (res.headersSent) return;
4007
+ if (payload === null) {
4008
+ res.writeHead(200);
4009
+ res.end();
4010
+ return;
4011
+ }
4012
+ this.writeJson(res, 200, payload);
4013
+ }
4014
+ readBody(req) {
4015
+ return new Promise((resolve, reject) => {
4016
+ let body = "";
4017
+ let size = 0;
4018
+ req.setEncoding("utf8");
4019
+ req.on("data", (chunk) => {
4020
+ size += Buffer.byteLength(chunk);
4021
+ if (size > this.maxBodyBytes) {
4022
+ reject(new Error("hook body too large"));
4023
+ req.destroy();
4024
+ return;
4025
+ }
4026
+ body += chunk;
4027
+ });
4028
+ req.on("end", () => resolve(body));
4029
+ req.on("error", reject);
4030
+ });
4031
+ }
4032
+ writeJson(res, statusCode, payload) {
4033
+ if (res.headersSent) return;
4034
+ res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
4035
+ res.end(JSON.stringify(payload));
4036
+ }
4037
+ };
4038
+
4039
+ // src/serve/provider-hook-runtime.ts
4040
+ async function createProviderHookRuntime(options) {
4041
+ const hookRegistry = new HookRegistry();
4042
+ const hookEventRouter = new HookEventRouter({
4043
+ relayConnection: options.relayConnection,
4044
+ agentStatusRegistry: options.agentStatusRegistry,
4045
+ changeSessionState: options.changeSessionState
4046
+ });
4047
+ const port = options.hookPort ?? 17654;
4048
+ const hookServer = new HookServer({
4049
+ port,
4050
+ registry: hookRegistry,
4051
+ permissionBroker: options.permissionBroker,
4052
+ isSessionActive: (sessionId) => !!options.sessionManager.getSession(sessionId),
4053
+ onEvent: (event) => {
4054
+ serviceLogger.info(
4055
+ {
4056
+ sessionId: event.sessionId,
4057
+ provider: event.provider,
4058
+ event: event.event,
4059
+ requestId: event.requestId
4060
+ },
4061
+ "Provider hook event received"
4062
+ );
4063
+ hookEventRouter.handle(event);
4064
+ }
4065
+ });
4066
+ try {
4067
+ await hookServer.start();
4068
+ } catch (err) {
4069
+ const msg = `Failed to start hook server on 127.0.0.1:${port}: ${String(err)}`;
4070
+ serviceLogger.error(msg);
4071
+ console.error(msg);
4072
+ process.exit(1);
4073
+ }
4074
+ const hookUrl = `http://127.0.0.1:${hookServer.getListeningPort() ?? port}/hook`;
4075
+ const createHookContext = (sessionId, provider) => {
4076
+ const credentials = hookRegistry.registerSession(sessionId, provider);
4077
+ return {
4078
+ provider,
4079
+ sessionId,
4080
+ hookUrl,
4081
+ marker: credentials.marker,
4082
+ token: credentials.token
4083
+ };
4084
+ };
4085
+ return {
4086
+ hookRegistry,
4087
+ hookEventRouter,
4088
+ hookServer,
4089
+ createHookContext
4090
+ };
4091
+ }
4092
+
4093
+ // src/serve.ts
4094
+ function resolveInterruptedApprovals(permissionBroker, hookEventRouter, relay, sessionId) {
4095
+ const approvals = permissionBroker.listSession(sessionId);
4096
+ if (approvals.length === 0) return;
4097
+ const message = "Permission request was interrupted in the PTY.";
4098
+ for (const approval of approvals) {
4099
+ if (!permissionBroker.resolve(approval.requestId, { behavior: "deny", message })) continue;
4100
+ hookEventRouter.onPermissionResolved(
4101
+ approval.sessionId,
4102
+ approval.provider,
4103
+ approval.requestId,
4104
+ "deny",
4105
+ { toolName: approval.toolName, toolInput: approval.input }
4106
+ );
4107
+ relay.sendRaw(
4108
+ JSON.stringify({
4109
+ type: "permission_decision_result",
4110
+ sessionId: approval.sessionId,
4111
+ requestId: approval.requestId,
4112
+ outcome: "deny",
4113
+ delivered: true,
4114
+ message
4115
+ })
4116
+ );
4117
+ }
4118
+ serviceLogger.info(
4119
+ { sessionId, count: approvals.length },
4120
+ "Pending approvals cleared after PTY interruption"
4121
+ );
4122
+ }
4123
+ function parseServiceOptions(argv) {
4124
+ const options = {};
4125
+ for (let i = 0; i < argv.length; i++) {
4126
+ const arg = argv[i];
4127
+ if (arg === "--env") {
4128
+ const envName = argv[i + 1];
4129
+ if (!envName || envName.startsWith("-")) {
4130
+ throw new Error("Missing value for --env");
4131
+ }
4132
+ options.envName = envName;
4133
+ i++;
4134
+ continue;
4135
+ }
4136
+ if (arg.startsWith("--env=")) {
4137
+ const envName = arg.slice("--env=".length);
4138
+ if (!envName) throw new Error("Missing value for --env");
4139
+ options.envName = envName;
4140
+ }
4141
+ }
4142
+ return options;
4143
+ }
4144
+ async function startService(options) {
4145
+ await cleanupStaleResources();
4146
+ try {
4147
+ unlinkSync3(STOPPED_PATH);
4148
+ } catch {
4149
+ }
4150
+ const permissionBroker = new PermissionBroker();
4151
+ const agentStatusRegistry = new AgentStatusRegistry();
4152
+ let unregisterHookSession = () => {
4153
+ };
4154
+ const sessionManager = new SessionManager({
4155
+ persistPath: SESSIONS_PATH,
4156
+ onSessionRemoved: (id, context) => {
4157
+ if (!context?.preserveProviderHooks) {
4158
+ unregisterHookSession(id);
4159
+ }
4160
+ permissionBroker.cleanupSession(id, "session removed");
4161
+ agentStatusRegistry.delete(id);
4162
+ const paths = sessionPaths(id);
4163
+ try {
4164
+ rmSync2(paths.dir, { recursive: true, force: true });
4165
+ } catch {
4166
+ }
4167
+ }
4168
+ });
4169
+ sessionManager.startReaper();
4170
+ const terminalSockets = /* @__PURE__ */ new Map();
4171
+ const proxyName = getProxyName();
4172
+ let proxyConfig = loadConfig({ envName: options?.envName });
4173
+ const getProviderEnv = () => buildProviderEnv(proxyConfig, process.env);
4174
+ const getAgentCliSuggestions = () => proxyConfig.agentCliSuggestions;
4175
+ const setAgentCliPath = (provider, path) => {
4176
+ const field = provider === "claude" ? "claudeBin" : "codexBin";
4177
+ const existing = proxyConfig.agentCliSuggestions[provider] ?? [];
4178
+ proxyConfig = {
4179
+ ...proxyConfig,
4180
+ [field]: path,
4181
+ agentCliSuggestions: {
4182
+ ...proxyConfig.agentCliSuggestions,
4183
+ [provider]: [path, ...existing.filter((candidate) => candidate !== path)]
4184
+ },
4185
+ sources: {
4186
+ ...proxyConfig.sources,
4187
+ [field]: "file"
4188
+ }
4189
+ };
4190
+ };
4191
+ const relayUrl = options?.relayUrl ?? proxyConfig.relayUrl;
4192
+ const relayToken = proxyConfig.relayToken;
4193
+ const statusConfig = {
4194
+ envName: proxyConfig.envName,
4195
+ envNameSource: proxyConfig.sources.envName,
4196
+ relayUrl,
4197
+ relayUrlSource: proxyConfig.sources.relayUrl,
4198
+ relayTokenSource: proxyConfig.sources.relayToken,
4199
+ hookPort: proxyConfig.hookPort ?? 17654,
4200
+ hookPortSource: proxyConfig.sources.hookPort
4201
+ };
4202
+ if (!relayUrl) {
4203
+ const msg = 'Relay URL is required. Set it via RELAY_URL or ~/.dev-anywhere/config.json {"defaultEnv":"local","envs":{"local":{"relayUrl":"ws://..."}}}.';
4204
+ serviceLogger.error(msg);
4205
+ console.error(msg);
4206
+ process.exit(1);
4207
+ }
4208
+ const relayConnection = new RelayConnection(relayUrl, { name: proxyName, token: relayToken });
4209
+ const relaySend = (data) => relayConnection.sendRaw(data);
4210
+ const controlHandlers = createControlMessageHandlers(relaySend, sessionManager);
4211
+ const observerChangeState = (sessionId, next) => changeSessionState(sessionManager, relayConnection, sessionId, next);
4212
+ const emitAgentStatus = (sessionId, phase) => {
4213
+ const session = sessionManager.getSession(sessionId);
4214
+ if (!session) return;
4215
+ const payload = {
4216
+ provider: session.provider,
4217
+ phase,
4218
+ seq: new SeqCounter(sessionId).next(),
4219
+ updatedAt: Date.now()
4220
+ };
4221
+ agentStatusRegistry.set(sessionId, payload);
4222
+ relayConnection.sendRaw(JSON.stringify({ type: "agent_status", sessionId, payload }));
4223
+ };
4224
+ const jsonObserver = new JsonObserver({
4225
+ changeSessionState: observerChangeState,
4226
+ emitAgentStatus
4227
+ });
4228
+ const hookRuntime = await createProviderHookRuntime({
4229
+ hookPort: proxyConfig.hookPort,
4230
+ permissionBroker,
4231
+ sessionManager,
4232
+ relayConnection,
4233
+ agentStatusRegistry,
4234
+ changeSessionState: observerChangeState
4235
+ });
4236
+ unregisterHookSession = (sessionId) => hookRuntime.hookRegistry.unregisterSession(sessionId);
4237
+ const workerRegistry = new WorkerRegistry({
4238
+ sessionManager,
4239
+ permissionBroker,
4240
+ relayConnection,
4241
+ jsonObserver,
4242
+ getProviderEnv
4243
+ });
4244
+ const hostedPtyRegistry = new HostedPtyRegistry({
4245
+ sessionManager,
4246
+ relayConnection,
4247
+ getProviderEnv,
4248
+ changeSessionState: observerChangeState,
4249
+ onTurnComplete: (sessionId) => {
4250
+ resolveInterruptedApprovals(
4251
+ permissionBroker,
4252
+ hookRuntime.hookEventRouter,
4253
+ relayConnection,
4254
+ sessionId
4255
+ );
4256
+ emitAgentStatus(sessionId, "idle");
4257
+ },
4258
+ onSessionClosed: (sessionId) => {
4259
+ controlHandlers.cleanup(sessionId);
4260
+ agentStatusRegistry.delete(sessionId);
4261
+ broadcastSessionList(relayConnection, sessionManager);
4262
+ }
4263
+ });
4264
+ relayConnection.connect();
4265
+ serviceLogger.info(
4266
+ {
4267
+ envName: proxyConfig.envName ?? "(single)",
4268
+ relayUrl,
4269
+ proxyName,
4270
+ tokenSet: !!relayToken,
4271
+ relayUrlSource: proxyConfig.sources.relayUrl
4272
+ },
4273
+ "Connecting to relay server"
4274
+ );
4275
+ const relayRouter = new RelayRouter({
4276
+ sessionManager,
4277
+ workerRegistry,
4278
+ controlHandlers,
4279
+ relayConnection,
4280
+ relaySend,
4281
+ terminalSockets,
4282
+ hostedPtyRegistry,
4283
+ broadcastSessionList: () => broadcastSessionList(relayConnection, sessionManager),
4284
+ broadcastSessionSync: (session) => broadcastSessionSync(relayConnection, session),
4285
+ jsonObserver,
4286
+ createHookContext: hookRuntime.createHookContext,
4287
+ cleanupHookContext: (sessionId) => hookRuntime.hookRegistry.unregisterSession(sessionId),
4288
+ permissionBroker,
4289
+ hookEventRouter: hookRuntime.hookEventRouter,
4290
+ agentStatusRegistry,
4291
+ envName: proxyConfig.envName,
4292
+ getProviderEnv,
4293
+ getAgentCliSuggestions,
4294
+ setAgentCliPath
4295
+ });
4296
+ relayConnection.on("message", (msg) => relayRouter.handle(msg));
4297
+ relayConnection.on("connected", () => {
4298
+ controlHandlers.reinitializeOnReconnect();
4299
+ broadcastBridgeStatus(true);
4300
+ });
4301
+ relayConnection.on("disconnected", () => {
4302
+ broadcastBridgeStatus(false);
4303
+ });
4304
+ function broadcastBridgeStatus(connected) {
4305
+ const msg = serializeIpc({ type: "bridge_status", connected });
4306
+ for (const [, sock] of terminalSockets) {
4307
+ if (sock.writable) sock.write(msg);
4308
+ }
4309
+ }
4310
+ await workerRegistry.reconnectAll();
4311
+ const server = createServer2((socket) => {
4312
+ handleTerminalConnection(socket, {
4313
+ sessionManager,
4314
+ workerRegistry,
4315
+ terminalSockets,
4316
+ hostedPtyRegistry,
4317
+ relayConnection,
4318
+ controlHandlers,
4319
+ agentStatusRegistry,
4320
+ permissionBroker,
4321
+ hookEventRouter: hookRuntime.hookEventRouter,
4322
+ createHookContext: hookRuntime.createHookContext,
4323
+ emitAgentStatus,
4324
+ config: statusConfig,
4325
+ resolveInterruptedApprovals: (sessionId) => resolveInterruptedApprovals(
4326
+ permissionBroker,
4327
+ hookRuntime.hookEventRouter,
4328
+ relayConnection,
4329
+ sessionId
4330
+ )
4331
+ });
4332
+ });
4333
+ server.listen(SOCK_PATH, () => {
4334
+ writeFileSync4(PID_PATH, String(process.pid));
4335
+ chmodSync(SOCK_PATH, 384);
4336
+ serviceLogger.info({ pid: process.pid, sock: SOCK_PATH }, "Service started");
4337
+ });
4338
+ async function shutdown() {
4339
+ serviceLogger.info("Shutting down service");
4340
+ sessionManager.stopReaper();
4341
+ await hookRuntime.hookServer.close();
4342
+ relayConnection.close();
4343
+ workerRegistry.destroyAll();
4344
+ hostedPtyRegistry.destroyAll();
4345
+ server.close();
4346
+ try {
4347
+ unlinkSync3(SOCK_PATH);
4348
+ } catch {
4349
+ }
4350
+ try {
4351
+ unlinkSync3(PID_PATH);
4352
+ } catch {
4353
+ }
4354
+ process.exit(0);
4355
+ }
4356
+ process.on("SIGTERM", () => {
4357
+ shutdown();
4358
+ });
4359
+ process.on("SIGINT", () => {
4360
+ shutdown();
4361
+ });
4362
+ }
4363
+ var isMainModule = process.argv[1] && (process.argv[1].endsWith("serve.js") || process.argv[1].endsWith("serve.ts"));
4364
+ if (isMainModule) {
4365
+ startService(parseServiceOptions(process.argv.slice(2))).catch((err) => {
4366
+ const message = err instanceof Error ? err.message : String(err);
4367
+ serviceLogger.error({ err: message }, "Service failed to start");
4368
+ console.error(message);
4369
+ process.exit(1);
4370
+ });
4371
+ }
4372
+ export {
4373
+ startService
4374
+ };
4375
+ //# sourceMappingURL=serve.js.map