@juspay/shooter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/.claude/hooks/notifier.cjs +1431 -0
  2. package/.claude/settings.json +162 -0
  3. package/README.md +515 -0
  4. package/bin/shooter.cjs +141 -0
  5. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css +1 -0
  6. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.br +0 -0
  7. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css +1 -0
  9. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.br +1 -0
  10. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/3.C0uFg0IS.css +1 -0
  12. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.br +0 -0
  13. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.gz +0 -0
  14. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css +1 -0
  15. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.br +0 -0
  16. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.gz +0 -0
  17. package/build/client/_app/immutable/assets/5.DRjApZQW.css +1 -0
  18. package/build/client/_app/immutable/assets/5.DRjApZQW.css.br +0 -0
  19. package/build/client/_app/immutable/assets/5.DRjApZQW.css.gz +0 -0
  20. package/build/client/_app/immutable/assets/6.AraZrY8I.css +1 -0
  21. package/build/client/_app/immutable/assets/6.AraZrY8I.css.br +0 -0
  22. package/build/client/_app/immutable/assets/6.AraZrY8I.css.gz +0 -0
  23. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css +1 -0
  24. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.br +0 -0
  25. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.gz +0 -0
  26. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css +1 -0
  27. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.br +0 -0
  28. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.gz +0 -0
  29. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css +1 -0
  30. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.br +0 -0
  31. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.gz +0 -0
  32. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css +1 -0
  33. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.br +0 -0
  34. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.gz +0 -0
  35. package/build/client/_app/immutable/chunks/BNJphC1q.js +56 -0
  36. package/build/client/_app/immutable/chunks/BNJphC1q.js.br +0 -0
  37. package/build/client/_app/immutable/chunks/BNJphC1q.js.gz +0 -0
  38. package/build/client/_app/immutable/chunks/BTGVxaYV.js +9 -0
  39. package/build/client/_app/immutable/chunks/BTGVxaYV.js.br +0 -0
  40. package/build/client/_app/immutable/chunks/BTGVxaYV.js.gz +0 -0
  41. package/build/client/_app/immutable/chunks/BlxrFPDK.js +1 -0
  42. package/build/client/_app/immutable/chunks/BlxrFPDK.js.br +0 -0
  43. package/build/client/_app/immutable/chunks/BlxrFPDK.js.gz +0 -0
  44. package/build/client/_app/immutable/chunks/Bvk7mfPM.js +1 -0
  45. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.br +0 -0
  46. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.gz +0 -0
  47. package/build/client/_app/immutable/chunks/CAokzuPQ.js +1 -0
  48. package/build/client/_app/immutable/chunks/CAokzuPQ.js.br +0 -0
  49. package/build/client/_app/immutable/chunks/CAokzuPQ.js.gz +0 -0
  50. package/build/client/_app/immutable/chunks/CGLrx-H5.js +1 -0
  51. package/build/client/_app/immutable/chunks/CGLrx-H5.js.br +0 -0
  52. package/build/client/_app/immutable/chunks/CGLrx-H5.js.gz +0 -0
  53. package/build/client/_app/immutable/chunks/CgCpWzEA.js +1 -0
  54. package/build/client/_app/immutable/chunks/CgCpWzEA.js.br +0 -0
  55. package/build/client/_app/immutable/chunks/CgCpWzEA.js.gz +0 -0
  56. package/build/client/_app/immutable/chunks/Cjwk_cGO.js +6 -0
  57. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.br +0 -0
  58. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.gz +0 -0
  59. package/build/client/_app/immutable/chunks/CtQ8EED1.js +11 -0
  60. package/build/client/_app/immutable/chunks/CtQ8EED1.js.br +0 -0
  61. package/build/client/_app/immutable/chunks/CtQ8EED1.js.gz +0 -0
  62. package/build/client/_app/immutable/chunks/DERQCisl.js +1 -0
  63. package/build/client/_app/immutable/chunks/DERQCisl.js.br +0 -0
  64. package/build/client/_app/immutable/chunks/DERQCisl.js.gz +0 -0
  65. package/build/client/_app/immutable/chunks/DKrg8TQs.js +1 -0
  66. package/build/client/_app/immutable/chunks/DKrg8TQs.js.br +0 -0
  67. package/build/client/_app/immutable/chunks/DKrg8TQs.js.gz +0 -0
  68. package/build/client/_app/immutable/chunks/DLu6yJIZ.js +1 -0
  69. package/build/client/_app/immutable/chunks/DLu6yJIZ.js.br +0 -0
  70. package/build/client/_app/immutable/chunks/DLu6yJIZ.js.gz +0 -0
  71. package/build/client/_app/immutable/chunks/Dkkpz_4D.js +126 -0
  72. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.br +0 -0
  73. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.gz +0 -0
  74. package/build/client/_app/immutable/chunks/DoczjQhA.js +1 -0
  75. package/build/client/_app/immutable/chunks/DoczjQhA.js.br +0 -0
  76. package/build/client/_app/immutable/chunks/DoczjQhA.js.gz +0 -0
  77. package/build/client/_app/immutable/chunks/PPVm8Dsz.js +1 -0
  78. package/build/client/_app/immutable/chunks/PPVm8Dsz.js.br +0 -0
  79. package/build/client/_app/immutable/chunks/PPVm8Dsz.js.gz +0 -0
  80. package/build/client/_app/immutable/chunks/RpcNruLP.js +2 -0
  81. package/build/client/_app/immutable/chunks/RpcNruLP.js.br +0 -0
  82. package/build/client/_app/immutable/chunks/RpcNruLP.js.gz +0 -0
  83. package/build/client/_app/immutable/chunks/a-St0Zwo.js +1 -0
  84. package/build/client/_app/immutable/chunks/a-St0Zwo.js.br +0 -0
  85. package/build/client/_app/immutable/chunks/a-St0Zwo.js.gz +0 -0
  86. package/build/client/_app/immutable/chunks/bo70OQUZ.js +1 -0
  87. package/build/client/_app/immutable/chunks/bo70OQUZ.js.br +0 -0
  88. package/build/client/_app/immutable/chunks/bo70OQUZ.js.gz +0 -0
  89. package/build/client/_app/immutable/entry/app.QvGgdvTI.js +2 -0
  90. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.br +0 -0
  91. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.gz +0 -0
  92. package/build/client/_app/immutable/entry/start.BntDNRMC.js +1 -0
  93. package/build/client/_app/immutable/entry/start.BntDNRMC.js.br +0 -0
  94. package/build/client/_app/immutable/entry/start.BntDNRMC.js.gz +0 -0
  95. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js +1 -0
  96. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.br +0 -0
  97. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.gz +0 -0
  98. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js +1 -0
  99. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.br +0 -0
  100. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.gz +0 -0
  101. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js +1 -0
  102. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.br +0 -0
  103. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.gz +0 -0
  104. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js +3 -0
  105. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.br +0 -0
  106. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.gz +0 -0
  107. package/build/client/_app/immutable/nodes/4.D-cIe70D.js +1 -0
  108. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.br +0 -0
  109. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.gz +0 -0
  110. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js +1 -0
  111. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.br +0 -0
  112. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.gz +0 -0
  113. package/build/client/_app/immutable/nodes/6.BB7QE48r.js +2 -0
  114. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.br +0 -0
  115. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.gz +0 -0
  116. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js +2 -0
  117. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.br +0 -0
  118. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.gz +0 -0
  119. package/build/client/_app/version.json +1 -0
  120. package/build/client/_app/version.json.br +0 -0
  121. package/build/client/_app/version.json.gz +0 -0
  122. package/build/client/app-icon.png +0 -0
  123. package/build/client/apple-touch-icon.png +0 -0
  124. package/build/client/favicon.png +0 -0
  125. package/build/client/favicon.svg +10 -0
  126. package/build/client/favicon.svg.br +0 -0
  127. package/build/client/favicon.svg.gz +0 -0
  128. package/build/client/manifest.webmanifest +1 -0
  129. package/build/client/pwa-192x192.png +0 -0
  130. package/build/client/pwa-512x512.png +0 -0
  131. package/build/client/registerSW.js +1 -0
  132. package/build/client/registerSW.js.br +0 -0
  133. package/build/client/registerSW.js.gz +0 -0
  134. package/build/client/sw.js +222 -0
  135. package/build/client/sw.js.br +0 -0
  136. package/build/client/sw.js.gz +0 -0
  137. package/build/client/workbox-5119daf5.js +3395 -0
  138. package/build/client/workbox-5119daf5.js.br +0 -0
  139. package/build/client/workbox-5119daf5.js.gz +0 -0
  140. package/build/env.js +94 -0
  141. package/build/handler.js +1494 -0
  142. package/build/index.js +345 -0
  143. package/build/pty-holder.cjs +510 -0
  144. package/build/server/chunks/0-q2IUp76Y.js +9 -0
  145. package/build/server/chunks/0-q2IUp76Y.js.map +1 -0
  146. package/build/server/chunks/1-CU50G5wZ.js +9 -0
  147. package/build/server/chunks/1-CU50G5wZ.js.map +1 -0
  148. package/build/server/chunks/2-D01t9s8T.js +9 -0
  149. package/build/server/chunks/2-D01t9s8T.js.map +1 -0
  150. package/build/server/chunks/3-5PUQ04wC.js +9 -0
  151. package/build/server/chunks/3-5PUQ04wC.js.map +1 -0
  152. package/build/server/chunks/4-e7gywnSG.js +9 -0
  153. package/build/server/chunks/4-e7gywnSG.js.map +1 -0
  154. package/build/server/chunks/5-CA1SA6KZ.js +9 -0
  155. package/build/server/chunks/5-CA1SA6KZ.js.map +1 -0
  156. package/build/server/chunks/6-71H221sV.js +9 -0
  157. package/build/server/chunks/6-71H221sV.js.map +1 -0
  158. package/build/server/chunks/7-Bo-vmdyz.js +9 -0
  159. package/build/server/chunks/7-Bo-vmdyz.js.map +1 -0
  160. package/build/server/chunks/_layout.svelte-SFHOxs74.js +132 -0
  161. package/build/server/chunks/_layout.svelte-SFHOxs74.js.map +1 -0
  162. package/build/server/chunks/_page.svelte-B4w-2wD-.js +120 -0
  163. package/build/server/chunks/_page.svelte-B4w-2wD-.js.map +1 -0
  164. package/build/server/chunks/_page.svelte-B_qAXjkh.js +213 -0
  165. package/build/server/chunks/_page.svelte-B_qAXjkh.js.map +1 -0
  166. package/build/server/chunks/_page.svelte-CsF1_TRG.js +50 -0
  167. package/build/server/chunks/_page.svelte-CsF1_TRG.js.map +1 -0
  168. package/build/server/chunks/_page.svelte-DJC6U-P0.js +68 -0
  169. package/build/server/chunks/_page.svelte-DJC6U-P0.js.map +1 -0
  170. package/build/server/chunks/_page.svelte-DQ6HBtsz.js +407 -0
  171. package/build/server/chunks/_page.svelte-DQ6HBtsz.js.map +1 -0
  172. package/build/server/chunks/_page.svelte-LbhhjP21.js +148 -0
  173. package/build/server/chunks/_page.svelte-LbhhjP21.js.map +1 -0
  174. package/build/server/chunks/_server.ts-BL2FGb5Z.js +387 -0
  175. package/build/server/chunks/_server.ts-BL2FGb5Z.js.map +1 -0
  176. package/build/server/chunks/_server.ts-BgdjBZco.js +47 -0
  177. package/build/server/chunks/_server.ts-BgdjBZco.js.map +1 -0
  178. package/build/server/chunks/_server.ts-BihKSdj_.js +59 -0
  179. package/build/server/chunks/_server.ts-BihKSdj_.js.map +1 -0
  180. package/build/server/chunks/_server.ts-BjOJsoy4.js +63 -0
  181. package/build/server/chunks/_server.ts-BjOJsoy4.js.map +1 -0
  182. package/build/server/chunks/_server.ts-C29xzfaw.js +77 -0
  183. package/build/server/chunks/_server.ts-C29xzfaw.js.map +1 -0
  184. package/build/server/chunks/_server.ts-CPa6DgIt.js +71 -0
  185. package/build/server/chunks/_server.ts-CPa6DgIt.js.map +1 -0
  186. package/build/server/chunks/_server.ts-CbDRDIoP.js +36 -0
  187. package/build/server/chunks/_server.ts-CbDRDIoP.js.map +1 -0
  188. package/build/server/chunks/_server.ts-Cl1OEWL4.js +54 -0
  189. package/build/server/chunks/_server.ts-Cl1OEWL4.js.map +1 -0
  190. package/build/server/chunks/_server.ts-ColfDHW8.js +60 -0
  191. package/build/server/chunks/_server.ts-ColfDHW8.js.map +1 -0
  192. package/build/server/chunks/_server.ts-Cv_OrRuL.js +494 -0
  193. package/build/server/chunks/_server.ts-Cv_OrRuL.js.map +1 -0
  194. package/build/server/chunks/_server.ts-D4MNi4cD.js +25 -0
  195. package/build/server/chunks/_server.ts-D4MNi4cD.js.map +1 -0
  196. package/build/server/chunks/_server.ts-DRVbgm6k.js +125 -0
  197. package/build/server/chunks/_server.ts-DRVbgm6k.js.map +1 -0
  198. package/build/server/chunks/_server.ts-DfajWaqh.js +39 -0
  199. package/build/server/chunks/_server.ts-DfajWaqh.js.map +1 -0
  200. package/build/server/chunks/_server.ts-y9-WYDMa.js +35 -0
  201. package/build/server/chunks/_server.ts-y9-WYDMa.js.map +1 -0
  202. package/build/server/chunks/auth-CEgFis71.js +32 -0
  203. package/build/server/chunks/auth-CEgFis71.js.map +1 -0
  204. package/build/server/chunks/client-CxCatAKr.js +255 -0
  205. package/build/server/chunks/client-CxCatAKr.js.map +1 -0
  206. package/build/server/chunks/error.svelte-BqdwMWdK.js +26 -0
  207. package/build/server/chunks/error.svelte-BqdwMWdK.js.map +1 -0
  208. package/build/server/chunks/exports-CJ0Q5XmL.js +4081 -0
  209. package/build/server/chunks/exports-CJ0Q5XmL.js.map +1 -0
  210. package/build/server/chunks/index2-DAxIoAO-.js +36 -0
  211. package/build/server/chunks/index2-DAxIoAO-.js.map +1 -0
  212. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js +137 -0
  213. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js.map +1 -0
  214. package/build/server/chunks/library-apns-BHxLmuIx.js +104 -0
  215. package/build/server/chunks/library-apns-BHxLmuIx.js.map +1 -0
  216. package/build/server/chunks/markdown-Bxrl3cCF.js +1241 -0
  217. package/build/server/chunks/markdown-Bxrl3cCF.js.map +1 -0
  218. package/build/server/chunks/pending-requests-D8UiTw7L.js +44 -0
  219. package/build/server/chunks/pending-requests-D8UiTw7L.js.map +1 -0
  220. package/build/server/chunks/pty-manager-C0FhBiVq.js +1697 -0
  221. package/build/server/chunks/pty-manager-C0FhBiVq.js.map +1 -0
  222. package/build/server/chunks/shared-server-BDY8jh20.js +200 -0
  223. package/build/server/chunks/shared-server-BDY8jh20.js.map +1 -0
  224. package/build/server/chunks/stores-D0HorpgL.js +36 -0
  225. package/build/server/chunks/stores-D0HorpgL.js.map +1 -0
  226. package/build/server/index.js +6466 -0
  227. package/build/server/index.js.map +1 -0
  228. package/build/server/manifest.js +184 -0
  229. package/build/server/manifest.js.map +1 -0
  230. package/build/shims.js +32 -0
  231. package/package.json +94 -0
  232. package/scripts/clipboard-shims/wl-paste +48 -0
  233. package/scripts/clipboard-shims/xclip +31 -0
  234. package/scripts/install.sh +477 -0
  235. package/scripts/setup-node-pty.sh +63 -0
  236. package/scripts/setup.cjs +571 -0
  237. package/scripts/test-runner.ts +243 -0
  238. package/scripts/vercel-env-commands.sh +60 -0
  239. package/server.ts +139 -0
  240. package/src/app.css +1835 -0
  241. package/src/app.d.ts +31 -0
  242. package/src/app.html +24 -0
  243. package/src/generated/types/APN.ts +305 -0
  244. package/src/generated/types/CLI.ts +52 -0
  245. package/src/generated/types/JWT.ts +92 -0
  246. package/src/generated/types/Terminal.ts +2736 -0
  247. package/src/generated/types/index.ts +6 -0
  248. package/src/lib/assets/icons/alert-triangle.svg +5 -0
  249. package/src/lib/assets/icons/bell.svg +4 -0
  250. package/src/lib/assets/icons/check-circle.svg +4 -0
  251. package/src/lib/assets/icons/file.svg +4 -0
  252. package/src/lib/assets/icons/folder.svg +3 -0
  253. package/src/lib/assets/icons/play.svg +3 -0
  254. package/src/lib/assets/icons/refresh.svg +4 -0
  255. package/src/lib/assets/icons/settings.svg +4 -0
  256. package/src/lib/assets/icons/terminal.svg +1 -0
  257. package/src/lib/assets/icons/tool.svg +3 -0
  258. package/src/lib/assets/icons/x-circle.svg +5 -0
  259. package/src/lib/modules/client/common/Card.svelte +26 -0
  260. package/src/lib/modules/client/common/EmptyState.svelte +36 -0
  261. package/src/lib/modules/client/common/Icon.svelte +61 -0
  262. package/src/lib/modules/client/common/StatusBadge.svelte +38 -0
  263. package/src/lib/modules/client/common/cache.ts +31 -0
  264. package/src/lib/modules/client/common/config-guard.ts +18 -0
  265. package/src/lib/modules/client/common/index.ts +12 -0
  266. package/src/lib/modules/client/common/markdown.ts +23 -0
  267. package/src/lib/modules/client/common/native-bridge.ts +50 -0
  268. package/src/lib/modules/client/common/time.ts +22 -0
  269. package/src/lib/modules/client/common/tool-title.ts +28 -0
  270. package/src/lib/modules/client/terminal/ChatView.svelte +400 -0
  271. package/src/lib/modules/client/terminal/CommandPalette.svelte +60 -0
  272. package/src/lib/modules/client/terminal/ConnectionStatus.svelte +99 -0
  273. package/src/lib/modules/client/terminal/LaunchSheet.svelte +294 -0
  274. package/src/lib/modules/client/terminal/QuickKeys.svelte +71 -0
  275. package/src/lib/modules/client/terminal/ShortcutsHelp.svelte +79 -0
  276. package/src/lib/modules/client/terminal/keyboard-shortcuts.ts +70 -0
  277. package/src/lib/modules/client/terminal/xterm-wrapper.ts +243 -0
  278. package/src/lib/modules/server/apn/library-apns.ts +137 -0
  279. package/src/lib/modules/server/apn/notification-history.ts +35 -0
  280. package/src/lib/modules/server/apn/notification-sessions.ts +117 -0
  281. package/src/lib/modules/server/apn/pending-requests.ts +65 -0
  282. package/src/lib/modules/server/apn/types.ts +51 -0
  283. package/src/lib/modules/server/auth.ts +34 -0
  284. package/src/lib/modules/server/cli/index.ts +79 -0
  285. package/src/lib/modules/server/cli/runner.ts +162 -0
  286. package/src/lib/modules/server/fcm/fcm-service.ts +72 -0
  287. package/src/lib/modules/server/sessions/jsonl-parser.ts +197 -0
  288. package/src/lib/modules/server/sessions/jsonl-reader.ts +301 -0
  289. package/src/lib/modules/server/sessions/opencode-reader.ts +264 -0
  290. package/src/lib/modules/server/sessions/types.ts +53 -0
  291. package/src/lib/modules/server/terminal/holder-client.ts +273 -0
  292. package/src/lib/modules/server/terminal/opencode-watcher.ts +661 -0
  293. package/src/lib/modules/server/terminal/pty-holder.cjs +510 -0
  294. package/src/lib/modules/server/terminal/pty-manager.ts +1012 -0
  295. package/src/lib/modules/server/terminal/session-watcher.ts +320 -0
  296. package/src/lib/modules/server/terminal/terminal-store.ts +198 -0
  297. package/src/lib/modules/server/ws/events-handler.ts +73 -0
  298. package/src/lib/modules/server/ws/keepalive.ts +108 -0
  299. package/src/lib/modules/server/ws/server.ts +93 -0
  300. package/src/lib/modules/server/ws/session-handler.ts +462 -0
  301. package/src/lib/modules/server/ws/terminal-handler.ts +197 -0
  302. package/src/lib/modules/server/ws/ticket-store.ts +58 -0
  303. package/src/lib/theme.css +529 -0
  304. package/src/lib/types/config.ts +6 -0
  305. package/src/routes/+layout.svelte +218 -0
  306. package/src/routes/+page.svelte +261 -0
  307. package/src/routes/api/debug/+server.ts +33 -0
  308. package/src/routes/api/device-token/+server.ts +85 -0
  309. package/src/routes/api/health/+server.ts +100 -0
  310. package/src/routes/api/notify/+server.ts +418 -0
  311. package/src/routes/api/qr-config/+server.ts +45 -0
  312. package/src/routes/api/response/+server.ts +73 -0
  313. package/src/routes/api/sessions/+server.ts +120 -0
  314. package/src/routes/api/terminals/+server.ts +141 -0
  315. package/src/routes/api/terminals/[id]/+server.ts +75 -0
  316. package/src/routes/api/terminals/[id]/paste-image/+server.ts +61 -0
  317. package/src/routes/api/terminals/[id]/resize/+server.ts +60 -0
  318. package/src/routes/api/webhook/+server.ts +42 -0
  319. package/src/routes/api/ws-status/+server.ts +23 -0
  320. package/src/routes/api/ws-ticket/+server.ts +86 -0
  321. package/src/routes/config/+page.svelte +600 -0
  322. package/src/routes/project/+page.svelte +274 -0
  323. package/src/routes/session/[id]/+page.svelte +434 -0
  324. package/src/routes/terminals/+page.svelte +618 -0
  325. package/src/routes/terminals/[id]/+page.svelte +968 -0
  326. package/svelte.config.js +18 -0
  327. package/tsconfig.json +14 -0
@@ -0,0 +1,1697 @@
1
+ import { fork } from 'child_process';
2
+ import { randomBytes } from 'crypto';
3
+ import * as fs from 'fs';
4
+ import { existsSync, readFileSync, unlinkSync, readdirSync, statSync } from 'fs';
5
+ import * as path from 'path';
6
+ import path__default from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import * as net from 'net';
9
+ import Database from 'better-sqlite3';
10
+ import { watch } from 'chokidar';
11
+ import { p as parseJsonlText } from './jsonl-parser-dmZU_Hyu.js';
12
+
13
+ class HolderClient {
14
+ connected = false;
15
+ pid = 0;
16
+ activityCb = null;
17
+ cwdCb = null;
18
+ disconnectCb = null;
19
+ exitCb = null;
20
+ lineBuf = "";
21
+ outputCb = null;
22
+ socket = null;
23
+ /**
24
+ * Connect to a holder process via its Unix domain socket.
25
+ * Resolves once the initial `info` and `scrollback` handshake messages
26
+ * have been received.
27
+ */
28
+ connect(socketPath) {
29
+ return new Promise((resolve, reject) => {
30
+ let settled = false;
31
+ let info = null;
32
+ let scrollback = "";
33
+ const pendingMessages = [];
34
+ const socket = net.createConnection(socketPath);
35
+ this.socket = socket;
36
+ socket.setEncoding("utf8");
37
+ socket.on("connect", () => {
38
+ this.connected = true;
39
+ });
40
+ socket.on("data", (chunk) => {
41
+ this.lineBuf += chunk;
42
+ const lines = this.lineBuf.split("\n");
43
+ this.lineBuf = lines.pop();
44
+ for (const line of lines) {
45
+ if (line.length === 0) {
46
+ continue;
47
+ }
48
+ let msg;
49
+ try {
50
+ msg = JSON.parse(line);
51
+ } catch {
52
+ continue;
53
+ }
54
+ if (!settled) {
55
+ if (msg.type === "info") {
56
+ info = { exitCode: msg.exitCode, exited: msg.exited, pid: msg.pid };
57
+ this.pid = msg.pid;
58
+ } else if (msg.type === "scrollback") {
59
+ scrollback = msg.data;
60
+ } else {
61
+ pendingMessages.push(msg);
62
+ }
63
+ if (info !== null) {
64
+ const settle = () => {
65
+ if (settled) return;
66
+ settled = true;
67
+ resolve({
68
+ exitCode: info.exitCode,
69
+ exited: info.exited,
70
+ pid: info.pid,
71
+ scrollback
72
+ });
73
+ const queued = [...pendingMessages];
74
+ pendingMessages.length = 0;
75
+ setTimeout(() => {
76
+ for (const pending of queued) {
77
+ this.handleMessage(pending);
78
+ }
79
+ }, 0);
80
+ };
81
+ if (msg.type === "scrollback" || info.exited) {
82
+ settle();
83
+ } else if (msg.type === "info") {
84
+ setTimeout(settle, 100);
85
+ }
86
+ }
87
+ } else {
88
+ this.handleMessage(msg);
89
+ }
90
+ }
91
+ });
92
+ socket.on("error", (err) => {
93
+ if (!settled) {
94
+ settled = true;
95
+ this.connected = false;
96
+ this.socket = null;
97
+ reject(err);
98
+ }
99
+ });
100
+ socket.on("close", () => {
101
+ const wasConnected = this.connected;
102
+ this.connected = false;
103
+ this.socket = null;
104
+ this.lineBuf = "";
105
+ if (!settled) {
106
+ settled = true;
107
+ reject(new Error("Socket closed before handshake completed"));
108
+ return;
109
+ }
110
+ if (wasConnected && this.disconnectCb) {
111
+ this.disconnectCb();
112
+ }
113
+ });
114
+ });
115
+ }
116
+ /** Gracefully disconnect from the holder (does NOT kill the holder). */
117
+ disconnect() {
118
+ this.connected = false;
119
+ if (this.socket) {
120
+ this.socket.destroy();
121
+ this.socket = null;
122
+ }
123
+ this.lineBuf = "";
124
+ }
125
+ /** Send a signal to the PTY process (default SIGTERM). */
126
+ kill(signal) {
127
+ const msg = { type: "kill" };
128
+ if (signal) {
129
+ msg.signal = signal;
130
+ }
131
+ this.send(msg);
132
+ }
133
+ /** Register callback for activity state changes. */
134
+ onActivity(cb) {
135
+ this.activityCb = cb;
136
+ }
137
+ /** Register callback for CWD changes. */
138
+ onCwd(cb) {
139
+ this.cwdCb = cb;
140
+ }
141
+ /** Register callback for unexpected disconnect from holder. */
142
+ onDisconnect(cb) {
143
+ this.disconnectCb = cb;
144
+ }
145
+ /** Register callback for PTY exit. */
146
+ onExit(cb) {
147
+ this.exitCb = cb;
148
+ }
149
+ /** Register callback for PTY output data. */
150
+ onOutput(cb) {
151
+ this.outputCb = cb;
152
+ }
153
+ /** Resize the PTY. */
154
+ resize(cols, rows) {
155
+ this.send({ cols, rows, type: "resize" });
156
+ }
157
+ /** Write data to the PTY stdin. */
158
+ write(data) {
159
+ this.send({ data, type: "input" });
160
+ }
161
+ // ── Private Helpers ────────────────────────────────────────────────
162
+ /** Dispatch a post-handshake message from the holder. */
163
+ handleMessage(msg) {
164
+ switch (msg.type) {
165
+ case "activity":
166
+ if (this.activityCb) {
167
+ this.activityCb(msg.active);
168
+ }
169
+ break;
170
+ case "cwd":
171
+ if (this.cwdCb) {
172
+ this.cwdCb(msg.path);
173
+ }
174
+ break;
175
+ case "exit":
176
+ if (this.exitCb) {
177
+ this.exitCb(msg.code);
178
+ }
179
+ break;
180
+ case "output":
181
+ if (this.outputCb) {
182
+ this.outputCb(msg.data);
183
+ }
184
+ break;
185
+ }
186
+ }
187
+ /** Send an ndjson message to the holder. */
188
+ send(msg) {
189
+ if (!this.socket || !this.connected) {
190
+ return;
191
+ }
192
+ this.socket.write(`${JSON.stringify(msg)}
193
+ `);
194
+ }
195
+ }
196
+ const OPENCODE_DB_PATH = (() => {
197
+ if (process.platform === "darwin") {
198
+ return path.join(process.env.HOME || "", "Library", "Application Support", "opencode", "opencode.db");
199
+ }
200
+ const xdgData = process.env.XDG_DATA_HOME || path.join(process.env.HOME || "", ".local", "share");
201
+ return path.join(xdgData, "opencode", "opencode.db");
202
+ })();
203
+ const POLL_INTERVAL_MS = 2e3;
204
+ const SQLITE_MAX_PARAMS = 500;
205
+ function toMillis(timestamp) {
206
+ return timestamp < 1e12 ? timestamp * 1e3 : timestamp;
207
+ }
208
+ class OpenCodeWatcher {
209
+ watchers = /* @__PURE__ */ new Map();
210
+ /**
211
+ * Find the most recent non-archived OpenCode session that matches
212
+ * the given working directory. Checks session.directory equals or
213
+ * starts with `cwd`.
214
+ *
215
+ * Returns the session ID or null if none found.
216
+ */
217
+ findSessionId(cwd, createdAfter) {
218
+ const db = openDb();
219
+ if (!db) {
220
+ return null;
221
+ }
222
+ try {
223
+ const timeFilter = createdAfter ? Math.min(createdAfter, Math.floor(createdAfter / 1e3)) : 0;
224
+ const row = db.prepare(
225
+ `
226
+ SELECT id
227
+ FROM session
228
+ WHERE (time_archived IS NULL OR time_archived = 0)
229
+ AND (directory = ? OR directory LIKE ? || '/%')
230
+ AND time_updated > ?
231
+ ORDER BY time_updated DESC
232
+ LIMIT 1
233
+ `
234
+ ).get(cwd, cwd, timeFilter);
235
+ return row?.id ?? null;
236
+ } catch (error) {
237
+ console.error("[opencode-watcher] Failed to find session:", error);
238
+ return null;
239
+ } finally {
240
+ db.close();
241
+ }
242
+ }
243
+ /**
244
+ * Read all messages and parts for a session from SQLite, converting
245
+ * them to ConversationMessage format.
246
+ */
247
+ getHistory(sessionId) {
248
+ const db = openDb();
249
+ if (!db) {
250
+ return [];
251
+ }
252
+ try {
253
+ const messages = db.prepare(
254
+ `
255
+ SELECT id, session_id, time_created, time_updated, data
256
+ FROM message
257
+ WHERE session_id = ?
258
+ ORDER BY time_created ASC
259
+ `
260
+ ).all(sessionId);
261
+ if (messages.length === 0) {
262
+ return [];
263
+ }
264
+ const messageIds = messages.map((m) => m.id);
265
+ const parts = batchInQuery(
266
+ db,
267
+ `SELECT id, message_id, session_id, time_created, time_updated, data
268
+ FROM part
269
+ WHERE message_id IN (__PLACEHOLDERS__)
270
+ ORDER BY time_created ASC`,
271
+ messageIds
272
+ );
273
+ const partsByMessage = /* @__PURE__ */ new Map();
274
+ for (const part of parts) {
275
+ if (!partsByMessage.has(part.message_id)) {
276
+ partsByMessage.set(part.message_id, []);
277
+ }
278
+ partsByMessage.get(part.message_id).push(part);
279
+ }
280
+ return this.buildMessages(messages, partsByMessage);
281
+ } catch (error) {
282
+ console.error("[opencode-watcher] Failed to read history:", error);
283
+ return [];
284
+ } finally {
285
+ db.close();
286
+ }
287
+ }
288
+ /**
289
+ * Stop watching a specific session. If a callback is provided, only that
290
+ * subscriber is removed — the interval keeps running while other subscribers
291
+ * remain. If no callback is provided, all subscribers and the interval are
292
+ * cleared (backward compat).
293
+ */
294
+ stop(sessionId, callback) {
295
+ const state = this.watchers.get(sessionId);
296
+ if (!state) {
297
+ return;
298
+ }
299
+ if (callback) {
300
+ state.callbacks.delete(callback);
301
+ console.log(
302
+ `[opencode-watcher] Removed subscriber from session: ${sessionId} (remaining=${state.callbacks.size})`
303
+ );
304
+ if (state.callbacks.size > 0) {
305
+ return;
306
+ }
307
+ }
308
+ clearInterval(state.intervalHandle);
309
+ this.watchers.delete(sessionId);
310
+ console.log(`[opencode-watcher] Stopped watching session: ${sessionId}`);
311
+ }
312
+ /**
313
+ * Stop all active watchers.
314
+ */
315
+ stopAll() {
316
+ for (const [sessionId] of this.watchers) {
317
+ this.stop(sessionId);
318
+ }
319
+ }
320
+ /**
321
+ * Start polling the SQLite DB every 2 seconds for new messages/parts
322
+ * in the given session. Converts new data to ConversationMessage
323
+ * format and invokes the callback.
324
+ */
325
+ watch(sessionId, callback) {
326
+ const existing = this.watchers.get(sessionId);
327
+ if (existing) {
328
+ existing.callbacks.add(callback);
329
+ console.log(
330
+ `[opencode-watcher] Added subscriber to session: ${sessionId} (total=${existing.callbacks.size})`
331
+ );
332
+ return;
333
+ }
334
+ const { emittedMessageIds, emittedPartIds, lastMessageTime, lastPartTime } = this.getHighWaterMarks(sessionId);
335
+ const intervalHandle = setInterval(() => {
336
+ this.poll(sessionId);
337
+ }, POLL_INTERVAL_MS);
338
+ const state = {
339
+ callbacks: /* @__PURE__ */ new Set([callback]),
340
+ emittedMessageIds,
341
+ emittedPartIds,
342
+ intervalHandle,
343
+ lastMessageTime,
344
+ lastPartTime,
345
+ sessionId
346
+ };
347
+ this.watchers.set(sessionId, state);
348
+ console.log(
349
+ `[opencode-watcher] Watching session: ${sessionId} (lastMsg=${lastMessageTime}, lastPart=${lastPartTime})`
350
+ );
351
+ }
352
+ // ── Private Helpers ────────────────────────────────────────────────
353
+ /**
354
+ * Convert OpenCode messages + their parts into ConversationMessage
355
+ * objects for consumption by the session handler.
356
+ */
357
+ buildMessages(messages, partsByMessage) {
358
+ const result = [];
359
+ for (const msg of messages) {
360
+ let msgData = {};
361
+ try {
362
+ msgData = JSON.parse(msg.data);
363
+ } catch {
364
+ continue;
365
+ }
366
+ const role = msgData.role === "user" ? "user" : "assistant";
367
+ const msgParts = partsByMessage.get(msg.id) || [];
368
+ const parts = [];
369
+ for (const part of msgParts) {
370
+ let partData;
371
+ try {
372
+ partData = JSON.parse(part.data);
373
+ } catch {
374
+ continue;
375
+ }
376
+ const converted = convertPartToMessagePart(partData);
377
+ if (converted) {
378
+ parts.push(converted);
379
+ }
380
+ }
381
+ if (parts.length === 0) {
382
+ console.debug(`[opencode-watcher] Skipping message ${msg.id} (no usable parts)`);
383
+ continue;
384
+ }
385
+ result.push({
386
+ id: msg.id,
387
+ parts,
388
+ role,
389
+ timestamp: new Date(toMillis(msg.time_created)).toISOString()
390
+ });
391
+ }
392
+ return result;
393
+ }
394
+ /**
395
+ * Scan existing messages/parts to determine the starting high-water
396
+ * marks for time-based polling. Also collects the set of already-seen
397
+ * message IDs so we do not re-emit them.
398
+ */
399
+ getHighWaterMarks(sessionId) {
400
+ const db = openDb();
401
+ if (!db) {
402
+ return { emittedMessageIds: /* @__PURE__ */ new Set(), emittedPartIds: /* @__PURE__ */ new Set(), lastMessageTime: 0, lastPartTime: 0 };
403
+ }
404
+ try {
405
+ const msgRow = db.prepare(
406
+ `
407
+ SELECT MAX(time_created) as maxTime
408
+ FROM message
409
+ WHERE session_id = ?
410
+ `
411
+ ).get(sessionId);
412
+ const partRow = db.prepare(
413
+ `
414
+ SELECT MAX(time_updated) as maxTime
415
+ FROM part
416
+ WHERE session_id = ?
417
+ `
418
+ ).get(sessionId);
419
+ const existingIds = db.prepare(
420
+ `
421
+ SELECT id FROM message WHERE session_id = ?
422
+ `
423
+ ).all(sessionId);
424
+ const emittedMessageIds = new Set(existingIds.map((r) => r.id));
425
+ const existingPartIds = db.prepare(
426
+ `
427
+ SELECT id FROM part WHERE session_id = ?
428
+ `
429
+ ).all(sessionId);
430
+ const emittedPartIds = new Set(existingPartIds.map((r) => r.id));
431
+ return {
432
+ emittedMessageIds,
433
+ emittedPartIds,
434
+ lastMessageTime: msgRow?.maxTime ?? 0,
435
+ lastPartTime: partRow?.maxTime ?? 0
436
+ };
437
+ } catch (error) {
438
+ console.error("[opencode-watcher] Failed to get high-water marks:", error);
439
+ return { emittedMessageIds: /* @__PURE__ */ new Set(), emittedPartIds: /* @__PURE__ */ new Set(), lastMessageTime: 0, lastPartTime: 0 };
440
+ } finally {
441
+ db.close();
442
+ }
443
+ }
444
+ /**
445
+ * Single poll iteration. Opens the DB, queries for new messages and
446
+ * updated parts, converts them to ConversationMessage[], and invokes
447
+ * the watcher callbacks.
448
+ */
449
+ poll(sessionId) {
450
+ const state = this.watchers.get(sessionId);
451
+ if (!state) {
452
+ return;
453
+ }
454
+ const db = openDb();
455
+ if (!db) {
456
+ return;
457
+ }
458
+ try {
459
+ const results = [];
460
+ const newMessages = db.prepare(
461
+ `
462
+ SELECT id, session_id, time_created, time_updated, data
463
+ FROM message
464
+ WHERE session_id = ? AND time_created > ?
465
+ ORDER BY time_created ASC
466
+ `
467
+ ).all(sessionId, state.lastMessageTime);
468
+ if (newMessages.length > 0) {
469
+ const dedupedMessages = newMessages.filter(
470
+ (m) => !state.emittedMessageIds.has(m.id)
471
+ );
472
+ if (dedupedMessages.length > 0) {
473
+ const newMsgIds = dedupedMessages.map((m) => m.id);
474
+ const newParts = batchInQuery(
475
+ db,
476
+ `SELECT id, message_id, session_id, time_created, time_updated, data
477
+ FROM part
478
+ WHERE message_id IN (__PLACEHOLDERS__)
479
+ ORDER BY time_created ASC`,
480
+ newMsgIds
481
+ );
482
+ const partsByMessage = /* @__PURE__ */ new Map();
483
+ for (const part of newParts) {
484
+ if (!partsByMessage.has(part.message_id)) {
485
+ partsByMessage.set(part.message_id, []);
486
+ }
487
+ partsByMessage.get(part.message_id).push(part);
488
+ }
489
+ const newEntries = this.buildMessages(dedupedMessages, partsByMessage);
490
+ results.push(...newEntries);
491
+ for (const part of newParts) {
492
+ if (part.time_updated > state.lastPartTime) {
493
+ state.lastPartTime = part.time_updated;
494
+ }
495
+ state.emittedPartIds.add(part.id);
496
+ }
497
+ }
498
+ for (const msg of newMessages) {
499
+ if (msg.time_created > state.lastMessageTime) {
500
+ state.lastMessageTime = msg.time_created;
501
+ }
502
+ state.emittedMessageIds.add(msg.id);
503
+ }
504
+ }
505
+ const updatedParts = db.prepare(
506
+ `
507
+ SELECT id, message_id, session_id, time_created, time_updated, data
508
+ FROM part
509
+ WHERE session_id = ? AND time_updated > ?
510
+ ORDER BY time_created ASC
511
+ `
512
+ ).all(sessionId, state.lastPartTime);
513
+ if (updatedParts.length > 0) {
514
+ const newMsgIdSet = new Set(newMessages.map((m) => m.id));
515
+ const newParts = updatedParts.filter(
516
+ (p) => !newMsgIdSet.has(p.message_id) && !state.emittedPartIds.has(p.id)
517
+ );
518
+ if (newParts.length > 0) {
519
+ const partsByMessage = /* @__PURE__ */ new Map();
520
+ for (const part of newParts) {
521
+ if (!partsByMessage.has(part.message_id)) {
522
+ partsByMessage.set(part.message_id, []);
523
+ }
524
+ partsByMessage.get(part.message_id).push(part);
525
+ }
526
+ const affectedMsgIds = [...partsByMessage.keys()];
527
+ const affectedMessages = batchInQuery(
528
+ db,
529
+ `SELECT id, session_id, time_created, time_updated, data
530
+ FROM message
531
+ WHERE id IN (__PLACEHOLDERS__)
532
+ ORDER BY time_created ASC`,
533
+ affectedMsgIds
534
+ );
535
+ const updatedEntries = this.buildMessages(affectedMessages, partsByMessage);
536
+ results.push(...updatedEntries);
537
+ for (const part of newParts) {
538
+ state.emittedPartIds.add(part.id);
539
+ }
540
+ }
541
+ for (const part of updatedParts) {
542
+ if (part.time_updated > state.lastPartTime) {
543
+ state.lastPartTime = part.time_updated;
544
+ }
545
+ }
546
+ }
547
+ if (results.length > 0) {
548
+ for (const cb of state.callbacks) {
549
+ try {
550
+ cb(results);
551
+ } catch (cbError) {
552
+ console.error("[opencode-watcher] Callback error:", cbError);
553
+ }
554
+ }
555
+ }
556
+ } catch (error) {
557
+ console.error("[opencode-watcher] Poll error:", error);
558
+ } finally {
559
+ db.close();
560
+ }
561
+ }
562
+ }
563
+ function batchInQuery(db, sql, ids) {
564
+ if (ids.length === 0) return [];
565
+ const results = [];
566
+ for (let i = 0; i < ids.length; i += SQLITE_MAX_PARAMS) {
567
+ const chunk = ids.slice(i, i + SQLITE_MAX_PARAMS);
568
+ const placeholders = chunk.map(() => "?").join(",");
569
+ const query = sql.replace("__PLACEHOLDERS__", placeholders);
570
+ const rows = db.prepare(query).all(...chunk);
571
+ results.push(...rows);
572
+ }
573
+ return results;
574
+ }
575
+ function convertPartToMessagePart(data) {
576
+ switch (data.type) {
577
+ case "reasoning":
578
+ return { content: data.text || "", type: "thinking" };
579
+ case "text":
580
+ return { content: data.text || "", type: "text" };
581
+ case "tool":
582
+ return {
583
+ id: data.callID || data.id || "",
584
+ input: data.state?.input || {},
585
+ toolName: data.tool || "Unknown",
586
+ type: "tool_use"
587
+ };
588
+ default:
589
+ return null;
590
+ }
591
+ }
592
+ function openDb() {
593
+ if (!fs.existsSync(OPENCODE_DB_PATH)) {
594
+ return null;
595
+ }
596
+ try {
597
+ return new Database(OPENCODE_DB_PATH, { readonly: true });
598
+ } catch {
599
+ return null;
600
+ }
601
+ }
602
+ const OW_GLOBAL_KEY = "__shooter_opencode_watcher";
603
+ const openCodeWatcher = globalThis[OW_GLOBAL_KEY] || new OpenCodeWatcher();
604
+ globalThis[OW_GLOBAL_KEY] = openCodeWatcher;
605
+ path.join(process.env.HOME || "", ".claude", "projects");
606
+ class SessionWatcher {
607
+ // Track assistant turns that span multiple JSONL lines, keyed by filePath
608
+ assistantTurnsPerFile = /* @__PURE__ */ new Map();
609
+ // Buffer for incomplete trailing lines (no terminating newline yet)
610
+ lineBufferPerFile = /* @__PURE__ */ new Map();
611
+ // Track message index per file for generating fallback IDs
612
+ messageIndexPerFile = /* @__PURE__ */ new Map();
613
+ watchedFiles = /* @__PURE__ */ new Map();
614
+ /**
615
+ * Read all entries from a JSONL file from the beginning.
616
+ * Used for catch-up replay when a new client connects mid-session.
617
+ */
618
+ getHistory(filePath) {
619
+ if (!fs.existsSync(filePath)) {
620
+ return [];
621
+ }
622
+ try {
623
+ const raw = fs.readFileSync(filePath, "utf-8");
624
+ const assistantTurns = /* @__PURE__ */ new Map();
625
+ const messages = parseJsonlText(raw, assistantTurns, 0);
626
+ for (const [msgId, turn] of assistantTurns) {
627
+ if (turn.parts.length > 0) {
628
+ messages.push({
629
+ id: msgId,
630
+ parts: turn.parts,
631
+ role: "assistant",
632
+ timestamp: turn.timestamp
633
+ });
634
+ }
635
+ }
636
+ return messages;
637
+ } catch (error) {
638
+ console.error(`[session-watcher] Failed to read history for ${filePath}:`, error);
639
+ return [];
640
+ }
641
+ }
642
+ /**
643
+ * Get raw JSONL entries from a session file (unparsed objects).
644
+ */
645
+ getRawEntries(filePath) {
646
+ if (!fs.existsSync(filePath)) {
647
+ return [];
648
+ }
649
+ try {
650
+ const raw = fs.readFileSync(filePath, "utf-8");
651
+ const entries = [];
652
+ for (const line of raw.split("\n")) {
653
+ const trimmed = line.trim();
654
+ if (!trimmed) {
655
+ continue;
656
+ }
657
+ try {
658
+ entries.push(JSON.parse(trimmed));
659
+ } catch {
660
+ }
661
+ }
662
+ return entries;
663
+ } catch (error) {
664
+ console.error(`[session-watcher] Failed to read raw entries for ${filePath}:`, error);
665
+ return [];
666
+ }
667
+ }
668
+ /**
669
+ * Stop watching a specific file and clean up resources.
670
+ */
671
+ stop(filePath) {
672
+ const watched = this.watchedFiles.get(filePath);
673
+ if (!watched) {
674
+ return;
675
+ }
676
+ watched.watcher.close();
677
+ this.watchedFiles.delete(filePath);
678
+ this.assistantTurnsPerFile.delete(filePath);
679
+ this.messageIndexPerFile.delete(filePath);
680
+ this.lineBufferPerFile.delete(filePath);
681
+ console.log(`[session-watcher] Stopped watching: ${filePath}`);
682
+ }
683
+ /**
684
+ * Stop watching all files and clean up all resources.
685
+ */
686
+ stopAll() {
687
+ for (const [filePath] of this.watchedFiles) {
688
+ this.stop(filePath);
689
+ }
690
+ }
691
+ /**
692
+ * Subscribe to new JSONL entries for a file. If the file is not yet
693
+ * being watched, starts watching it. Returns an unsubscribe function
694
+ * that removes the callback (and stops the watcher when no subscribers
695
+ * remain). Matches the multi-subscriber pattern used by OpenCodeWatcher.
696
+ */
697
+ subscribe(filePath, onNewEntries) {
698
+ const existing = this.watchedFiles.get(filePath);
699
+ if (existing) {
700
+ existing.callbacks.add(onNewEntries);
701
+ console.log(
702
+ `[session-watcher] Added subscriber to: ${filePath} (total=${existing.callbacks.size})`
703
+ );
704
+ return () => {
705
+ existing.callbacks.delete(onNewEntries);
706
+ console.log(
707
+ `[session-watcher] Removed subscriber from: ${filePath} (remaining=${existing.callbacks.size})`
708
+ );
709
+ if (existing.callbacks.size === 0) {
710
+ this.stop(filePath);
711
+ }
712
+ };
713
+ }
714
+ const initialOffset = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
715
+ this.assistantTurnsPerFile.set(filePath, /* @__PURE__ */ new Map());
716
+ this.messageIndexPerFile.set(filePath, 0);
717
+ this.lineBufferPerFile.set(filePath, "");
718
+ const watcher = watch(filePath, {
719
+ // Debounce rapid successive writes
720
+ awaitWriteFinish: {
721
+ pollInterval: 100,
722
+ stabilityThreshold: 200
723
+ },
724
+ // Don't emit 'add' event on initial scan — we handle catch-up via getHistory
725
+ ignoreInitial: true,
726
+ // Use polling as a fallback for network filesystems
727
+ usePolling: false
728
+ });
729
+ const watched = {
730
+ callbacks: /* @__PURE__ */ new Set([onNewEntries]),
731
+ filePath,
732
+ offset: initialOffset,
733
+ watcher
734
+ };
735
+ watcher.on("change", () => {
736
+ this.readNewEntries(watched);
737
+ });
738
+ watcher.on("add", () => {
739
+ this.readNewEntries(watched);
740
+ });
741
+ watcher.on("error", (error) => {
742
+ console.error(`[session-watcher] Error watching ${filePath}:`, error);
743
+ });
744
+ this.watchedFiles.set(filePath, watched);
745
+ console.log(`[session-watcher] Watching: ${filePath} (offset: ${initialOffset})`);
746
+ return () => {
747
+ watched.callbacks.delete(onNewEntries);
748
+ console.log(
749
+ `[session-watcher] Removed subscriber from: ${filePath} (remaining=${watched.callbacks.size})`
750
+ );
751
+ if (watched.callbacks.size === 0) {
752
+ this.stop(filePath);
753
+ }
754
+ };
755
+ }
756
+ /**
757
+ * Start watching a JSONL file for new entries (legacy API).
758
+ * Delegates to subscribe() internally. Callers that need to
759
+ * unsubscribe should use subscribe() directly instead.
760
+ */
761
+ watch(filePath, onNewEntries) {
762
+ this.subscribe(filePath, onNewEntries);
763
+ }
764
+ /**
765
+ * Read bytes appended since last offset, parse new JSONL lines,
766
+ * and invoke the callback with any new messages.
767
+ */
768
+ readNewEntries(watched) {
769
+ const { filePath } = watched;
770
+ let stat;
771
+ try {
772
+ stat = fs.statSync(filePath);
773
+ } catch {
774
+ return;
775
+ }
776
+ const currentSize = stat.size;
777
+ if (currentSize <= watched.offset) {
778
+ if (currentSize < watched.offset) {
779
+ console.warn(`[session-watcher] File truncated, resetting offset: ${filePath}`);
780
+ watched.offset = 0;
781
+ this.assistantTurnsPerFile.set(filePath, /* @__PURE__ */ new Map());
782
+ this.messageIndexPerFile.set(filePath, 0);
783
+ this.lineBufferPerFile.set(filePath, "");
784
+ }
785
+ return;
786
+ }
787
+ const fd = fs.openSync(filePath, "r");
788
+ try {
789
+ const bytesToRead = currentSize - watched.offset;
790
+ const buffer = Buffer.alloc(bytesToRead);
791
+ fs.readSync(fd, buffer, 0, bytesToRead, watched.offset);
792
+ watched.offset = currentSize;
793
+ const chunk = buffer.toString("utf-8");
794
+ const previousBuffer = this.lineBufferPerFile.get(filePath) || "";
795
+ const combined = previousBuffer + chunk;
796
+ const segments = combined.split("\n");
797
+ if (!combined.endsWith("\n")) {
798
+ this.lineBufferPerFile.set(filePath, segments.pop() || "");
799
+ } else {
800
+ this.lineBufferPerFile.set(filePath, "");
801
+ if (segments.length > 0 && segments[segments.length - 1] === "") {
802
+ segments.pop();
803
+ }
804
+ }
805
+ const completeLines = segments.filter((line) => line.trim());
806
+ if (completeLines.length === 0) {
807
+ return;
808
+ }
809
+ const assistantTurns = this.assistantTurnsPerFile.get(filePath) || /* @__PURE__ */ new Map();
810
+ const startIndex = this.messageIndexPerFile.get(filePath) || 0;
811
+ const newText = completeLines.join("\n");
812
+ const newMessages = parseJsonlText(newText, assistantTurns, startIndex);
813
+ this.messageIndexPerFile.set(filePath, startIndex + completeLines.length);
814
+ if (newMessages.length > 0) {
815
+ for (const cb of watched.callbacks) {
816
+ try {
817
+ cb(newMessages);
818
+ } catch (cbError) {
819
+ console.error("[session-watcher] Callback error:", cbError);
820
+ }
821
+ }
822
+ }
823
+ } finally {
824
+ fs.closeSync(fd);
825
+ }
826
+ }
827
+ }
828
+ const SW_GLOBAL_KEY = "__shooter_session_watcher";
829
+ const sessionWatcher = globalThis[SW_GLOBAL_KEY] || new SessionWatcher();
830
+ globalThis[SW_GLOBAL_KEY] = sessionWatcher;
831
+ const DB_DIR = path.join(process.env.HOME || "", ".shooter");
832
+ const DB_PATH = path.join(DB_DIR, "shooter.db");
833
+ const COLUMNS = [
834
+ "id",
835
+ "command",
836
+ "args",
837
+ "cwd",
838
+ "cols",
839
+ "rows",
840
+ "pid",
841
+ "holder_pid",
842
+ "socket_path",
843
+ "session_file",
844
+ "opencode_session_id",
845
+ "status",
846
+ "exit_code",
847
+ "created_at",
848
+ "exited_at"
849
+ ];
850
+ function rowToRecord(row) {
851
+ return {
852
+ args: row.args,
853
+ cols: row.cols,
854
+ command: row.command,
855
+ createdAt: row.created_at,
856
+ cwd: row.cwd,
857
+ exitCode: row.exit_code ?? null,
858
+ exitedAt: row.exited_at ?? null,
859
+ holderPid: row.holder_pid ?? null,
860
+ id: row.id,
861
+ opencodeSessionId: row.opencode_session_id ?? null,
862
+ pid: row.pid ?? null,
863
+ rows: row.rows,
864
+ sessionFile: row.session_file ?? null,
865
+ socketPath: row.socket_path ?? null,
866
+ status: row.status
867
+ };
868
+ }
869
+ const CAMEL_TO_SNAKE = {
870
+ createdAt: "created_at",
871
+ exitCode: "exit_code",
872
+ exitedAt: "exited_at",
873
+ holderPid: "holder_pid",
874
+ opencodeSessionId: "opencode_session_id",
875
+ sessionFile: "session_file",
876
+ socketPath: "socket_path"
877
+ };
878
+ class TerminalStore {
879
+ db;
880
+ constructor() {
881
+ fs.mkdirSync(DB_DIR, { recursive: true });
882
+ this.db = new Database(DB_PATH);
883
+ this.db.pragma("journal_mode = WAL");
884
+ this.db.exec(`
885
+ CREATE TABLE IF NOT EXISTS terminals (
886
+ id TEXT PRIMARY KEY,
887
+ command TEXT NOT NULL,
888
+ args TEXT NOT NULL DEFAULT '[]',
889
+ cwd TEXT NOT NULL,
890
+ cols INTEGER NOT NULL DEFAULT 80,
891
+ rows INTEGER NOT NULL DEFAULT 24,
892
+ pid INTEGER,
893
+ holder_pid INTEGER,
894
+ socket_path TEXT,
895
+ session_file TEXT,
896
+ opencode_session_id TEXT,
897
+ status TEXT NOT NULL DEFAULT 'running',
898
+ exit_code INTEGER,
899
+ created_at TEXT NOT NULL,
900
+ exited_at TEXT
901
+ )
902
+ `);
903
+ }
904
+ deleteOlderThan(ms) {
905
+ const cutoff = new Date(Date.now() - ms).toISOString();
906
+ const result = this.db.prepare(
907
+ "DELETE FROM terminals WHERE status IN ('exited', 'orphaned') AND COALESCE(exited_at, created_at) < ?"
908
+ ).run(cutoff);
909
+ return result.changes;
910
+ }
911
+ get(id) {
912
+ const row = this.db.prepare("SELECT * FROM terminals WHERE id = ?").get(id);
913
+ return row ? rowToRecord(row) : null;
914
+ }
915
+ insert(terminal) {
916
+ const placeholders = COLUMNS.map(() => "?").join(", ");
917
+ const stmt = this.db.prepare(
918
+ `INSERT INTO terminals (${COLUMNS.join(", ")}) VALUES (${placeholders})`
919
+ );
920
+ stmt.run(
921
+ terminal.id,
922
+ terminal.command,
923
+ terminal.args,
924
+ terminal.cwd,
925
+ terminal.cols,
926
+ terminal.rows,
927
+ terminal.pid,
928
+ terminal.holderPid,
929
+ terminal.socketPath,
930
+ terminal.sessionFile,
931
+ terminal.opencodeSessionId,
932
+ terminal.status,
933
+ terminal.exitCode,
934
+ terminal.createdAt,
935
+ terminal.exitedAt
936
+ );
937
+ }
938
+ listAll() {
939
+ const rows = this.db.prepare("SELECT * FROM terminals ORDER BY created_at DESC").all();
940
+ return rows.map(rowToRecord);
941
+ }
942
+ listRunning() {
943
+ const rows = this.db.prepare("SELECT * FROM terminals WHERE status = 'running' ORDER BY created_at DESC").all();
944
+ return rows.map(rowToRecord);
945
+ }
946
+ markExited(id, exitCode) {
947
+ this.db.prepare(
948
+ "UPDATE terminals SET status = 'exited', exit_code = ?, exited_at = ? WHERE id = ?"
949
+ ).run(exitCode, (/* @__PURE__ */ new Date()).toISOString(), id);
950
+ }
951
+ markOrphaned(id) {
952
+ this.db.prepare("UPDATE terminals SET status = 'orphaned' WHERE id = ?").run(id);
953
+ }
954
+ update(id, fields) {
955
+ const entries = Object.entries(fields).filter(([key]) => key !== "id");
956
+ if (entries.length === 0) {
957
+ return;
958
+ }
959
+ const sets = entries.map(([key]) => `${toSnake(key)} = ?`).join(", ");
960
+ const values = entries.map(([, val]) => val ?? null);
961
+ this.db.prepare(`UPDATE terminals SET ${sets} WHERE id = ?`).run(...values, id);
962
+ }
963
+ }
964
+ function toSnake(key) {
965
+ return CAMEL_TO_SNAKE[key] || key;
966
+ }
967
+ const TS_GLOBAL_KEY = "__shooter_terminal_store";
968
+ const terminalStore = globalThis[TS_GLOBAL_KEY] || new TerminalStore();
969
+ globalThis[TS_GLOBAL_KEY] = terminalStore;
970
+ const MAX_SCROLLBACK_BYTES = 512 * 1024;
971
+ const MAX_OUTPUT_BUFFER_BYTES = 1024 * 1024;
972
+ const SCROLLBACK_CHUNK_SIZE = 50 * 1024;
973
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
974
+ const EXITED_TTL_MS = 60 * 60 * 1e3;
975
+ const MAX_EXITED_TERMINALS = 10;
976
+ const SIGKILL_DELAY_MS = 5e3;
977
+ const HOLDER_READY_TIMEOUT_MS = 5e3;
978
+ const DB_CLEANUP_TTL_MS = 24 * 60 * 60 * 1e3;
979
+ const __filename$1 = fileURLToPath(import.meta.url);
980
+ const __dirname$1 = path__default.dirname(__filename$1);
981
+ class PtyManager {
982
+ cleanupTimer = null;
983
+ terminals = /* @__PURE__ */ new Map();
984
+ constructor() {
985
+ this.cleanupTimer = setInterval(() => {
986
+ this.cleanup();
987
+ }, CLEANUP_INTERVAL_MS);
988
+ }
989
+ // -----------------------------------------------------------------------
990
+ // create — now async: forks a holder process, connects via HolderClient,
991
+ // persists to SQLite
992
+ // -----------------------------------------------------------------------
993
+ attach(id, ws) {
994
+ const terminal = this.terminals.get(id);
995
+ if (!terminal) {
996
+ return false;
997
+ }
998
+ terminal.clients.add(ws);
999
+ terminal.outputBuffers.set(ws, { data: [], size: 0 });
1000
+ this.sendScrollback(terminal, ws);
1001
+ return true;
1002
+ }
1003
+ // -----------------------------------------------------------------------
1004
+ // reconnectAll — recover persisted terminals on server startup
1005
+ // -----------------------------------------------------------------------
1006
+ cleanup() {
1007
+ const now = Date.now();
1008
+ const exited = [];
1009
+ for (const [id, terminal] of this.terminals) {
1010
+ if (terminal.status !== "exited") {
1011
+ continue;
1012
+ }
1013
+ const exitTime = terminal.exitedAt?.getTime() ?? terminal.createdAt.getTime();
1014
+ if (now - exitTime > EXITED_TTL_MS) {
1015
+ this.evict(id);
1016
+ continue;
1017
+ }
1018
+ exited.push({ exitedAt: exitTime, id });
1019
+ }
1020
+ if (exited.length > MAX_EXITED_TERMINALS) {
1021
+ exited.sort((a, b) => a.exitedAt - b.exitedAt);
1022
+ const toEvict = exited.slice(0, exited.length - MAX_EXITED_TERMINALS);
1023
+ for (const { id } of toEvict) {
1024
+ this.evict(id);
1025
+ }
1026
+ }
1027
+ try {
1028
+ const deleted = terminalStore.deleteOlderThan(DB_CLEANUP_TTL_MS);
1029
+ if (deleted > 0) {
1030
+ console.log(`[pty-manager] Cleaned up ${deleted} old terminal record(s) from SQLite`);
1031
+ }
1032
+ } catch {
1033
+ }
1034
+ }
1035
+ // -----------------------------------------------------------------------
1036
+ // disconnectAll — graceful shutdown: disconnect clients, keep holders alive
1037
+ // -----------------------------------------------------------------------
1038
+ async create(command, args, cwd, cols, rows) {
1039
+ const id = randomBytes(4).toString("hex");
1040
+ const socketPath = `/tmp/shooter-term-${id}.sock`;
1041
+ const holderScript = resolveHolderPath();
1042
+ const holderArgs = [id, socketPath, cwd, String(cols), String(rows), command, ...args];
1043
+ const holder = fork(holderScript, holderArgs, {
1044
+ detached: true,
1045
+ stdio: ["ignore", "ignore", "ignore", "ipc"]
1046
+ });
1047
+ holder.unref();
1048
+ await new Promise((resolve, reject) => {
1049
+ const timeout = setTimeout(() => {
1050
+ holder.kill();
1051
+ reject(new Error("Holder ready timeout"));
1052
+ }, HOLDER_READY_TIMEOUT_MS);
1053
+ holder.on("message", (msg) => {
1054
+ if (msg.type === "ready") {
1055
+ clearTimeout(timeout);
1056
+ holder.disconnect();
1057
+ resolve();
1058
+ }
1059
+ });
1060
+ holder.on("error", (err) => {
1061
+ clearTimeout(timeout);
1062
+ reject(new Error(`Holder process error: ${err.message}`));
1063
+ });
1064
+ holder.on("exit", (code) => {
1065
+ clearTimeout(timeout);
1066
+ reject(new Error(`Holder process exited with code ${code} before ready`));
1067
+ });
1068
+ });
1069
+ const holderPid = holder.pid;
1070
+ const client = new HolderClient();
1071
+ const connectResult = await client.connect(socketPath);
1072
+ const now = /* @__PURE__ */ new Date();
1073
+ const terminal = {
1074
+ args,
1075
+ clients: /* @__PURE__ */ new Set(),
1076
+ cols,
1077
+ command,
1078
+ createdAt: now,
1079
+ cwd,
1080
+ currentCwd: null,
1081
+ exitCode: connectResult.exitCode,
1082
+ exitedAt: null,
1083
+ holderPid,
1084
+ id,
1085
+ isActive: false,
1086
+ openCodeNoopCb: null,
1087
+ openCodeSessionId: null,
1088
+ outputBuffers: /* @__PURE__ */ new Map(),
1089
+ pid: connectResult.pid,
1090
+ pollTimer: null,
1091
+ pty: client,
1092
+ rows,
1093
+ scrollback: connectResult.scrollback,
1094
+ sessionFile: null,
1095
+ socketPath,
1096
+ status: connectResult.exited ? "exited" : "running",
1097
+ watcherOffset: 0
1098
+ };
1099
+ this.wireHolderCallbacks(client, terminal);
1100
+ terminalStore.insert({
1101
+ args: JSON.stringify(args),
1102
+ cols,
1103
+ command,
1104
+ createdAt: now.toISOString(),
1105
+ cwd,
1106
+ exitCode: null,
1107
+ exitedAt: null,
1108
+ holderPid,
1109
+ id,
1110
+ opencodeSessionId: null,
1111
+ pid: connectResult.pid,
1112
+ rows,
1113
+ sessionFile: null,
1114
+ socketPath,
1115
+ status: "running"
1116
+ });
1117
+ this.startSessionDiscovery(terminal);
1118
+ this.terminals.set(id, terminal);
1119
+ return terminal;
1120
+ }
1121
+ // -----------------------------------------------------------------------
1122
+ // get
1123
+ // -----------------------------------------------------------------------
1124
+ destroy() {
1125
+ if (this.cleanupTimer) {
1126
+ clearInterval(this.cleanupTimer);
1127
+ this.cleanupTimer = null;
1128
+ }
1129
+ for (const [id, terminal] of this.terminals) {
1130
+ if (terminal.pollTimer) {
1131
+ clearInterval(terminal.pollTimer);
1132
+ terminal.pollTimer = null;
1133
+ }
1134
+ if (terminal.status === "running") {
1135
+ try {
1136
+ terminal.pty.kill("SIGTERM");
1137
+ } catch {
1138
+ }
1139
+ try {
1140
+ process.kill(terminal.holderPid, "SIGKILL");
1141
+ } catch {
1142
+ }
1143
+ }
1144
+ terminal.pty.disconnect();
1145
+ for (const ws of terminal.clients) {
1146
+ try {
1147
+ ws.close();
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ this.terminals.delete(id);
1152
+ }
1153
+ }
1154
+ // -----------------------------------------------------------------------
1155
+ // list — running first, then recently exited, each group sorted by
1156
+ // createdAt descending
1157
+ // -----------------------------------------------------------------------
1158
+ detach(id, ws) {
1159
+ const terminal = this.terminals.get(id);
1160
+ if (!terminal) {
1161
+ return false;
1162
+ }
1163
+ terminal.clients.delete(ws);
1164
+ terminal.outputBuffers.delete(ws);
1165
+ return true;
1166
+ }
1167
+ // -----------------------------------------------------------------------
1168
+ // kill — route through holder: SIGTERM, then SIGKILL after 5 s
1169
+ // -----------------------------------------------------------------------
1170
+ disconnectAll() {
1171
+ if (this.cleanupTimer) {
1172
+ clearInterval(this.cleanupTimer);
1173
+ this.cleanupTimer = null;
1174
+ }
1175
+ for (const [, terminal] of this.terminals) {
1176
+ if (terminal.pollTimer) {
1177
+ clearInterval(terminal.pollTimer);
1178
+ terminal.pollTimer = null;
1179
+ }
1180
+ terminal.pty.disconnect();
1181
+ for (const ws of terminal.clients) {
1182
+ try {
1183
+ ws.close();
1184
+ } catch {
1185
+ }
1186
+ }
1187
+ terminal.clients.clear();
1188
+ terminal.outputBuffers.clear();
1189
+ }
1190
+ this.terminals.clear();
1191
+ }
1192
+ // -----------------------------------------------------------------------
1193
+ // remove — remove an exited terminal from the map
1194
+ // -----------------------------------------------------------------------
1195
+ get(id) {
1196
+ return this.terminals.get(id) ?? null;
1197
+ }
1198
+ // -----------------------------------------------------------------------
1199
+ // resize
1200
+ // -----------------------------------------------------------------------
1201
+ getScrollback(id) {
1202
+ const terminal = this.terminals.get(id);
1203
+ if (!terminal) {
1204
+ return null;
1205
+ }
1206
+ return terminal.scrollback;
1207
+ }
1208
+ // -----------------------------------------------------------------------
1209
+ // attach — register a WebSocket client and replay scrollback
1210
+ // -----------------------------------------------------------------------
1211
+ kill(id) {
1212
+ const terminal = this.terminals.get(id);
1213
+ if (!terminal) {
1214
+ return false;
1215
+ }
1216
+ if (terminal.status === "exited") {
1217
+ return true;
1218
+ }
1219
+ try {
1220
+ terminal.pty.kill("SIGTERM");
1221
+ } catch {
1222
+ terminal.status = "exited";
1223
+ terminal.exitedAt = /* @__PURE__ */ new Date();
1224
+ terminalStore.markExited(id, null);
1225
+ return true;
1226
+ }
1227
+ setTimeout(() => {
1228
+ if (terminal.status === "running") {
1229
+ try {
1230
+ terminal.pty.kill("SIGKILL");
1231
+ } catch {
1232
+ }
1233
+ terminal.status = "exited";
1234
+ terminal.exitedAt = terminal.exitedAt ?? /* @__PURE__ */ new Date();
1235
+ terminalStore.markExited(id, null);
1236
+ }
1237
+ }, SIGKILL_DELAY_MS);
1238
+ return true;
1239
+ }
1240
+ // -----------------------------------------------------------------------
1241
+ // detach — remove a WebSocket client
1242
+ // -----------------------------------------------------------------------
1243
+ list() {
1244
+ const all = Array.from(this.terminals.values());
1245
+ const running = all.filter((t) => t.status === "running").sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1246
+ const exited = all.filter((t) => t.status === "exited").sort((a, b) => {
1247
+ const aTime = a.exitedAt?.getTime() ?? a.createdAt.getTime();
1248
+ const bTime = b.exitedAt?.getTime() ?? b.createdAt.getTime();
1249
+ return bTime - aTime;
1250
+ });
1251
+ return [...running, ...exited];
1252
+ }
1253
+ // -----------------------------------------------------------------------
1254
+ // getScrollback — return raw scrollback data for replay
1255
+ // -----------------------------------------------------------------------
1256
+ async reconnectAll() {
1257
+ const running = terminalStore.listRunning();
1258
+ if (running.length === 0) {
1259
+ console.log("[pty-manager] No persisted terminals to reconnect");
1260
+ return;
1261
+ }
1262
+ console.log(`[pty-manager] Reconnecting to ${running.length} persisted terminal(s)...`);
1263
+ for (const record of running) {
1264
+ try {
1265
+ await this.reconnectOne(record);
1266
+ } catch (err) {
1267
+ const errMsg = err instanceof Error ? err.message : String(err);
1268
+ console.warn(`[pty-manager] Failed to reconnect terminal ${record.id}: ${errMsg}`);
1269
+ this.handleReconnectFailure(record);
1270
+ }
1271
+ }
1272
+ }
1273
+ // -----------------------------------------------------------------------
1274
+ // cleanup — evict exited terminals older than 1 hour, cap at 10 exited;
1275
+ // also clean up old SQLite records
1276
+ // -----------------------------------------------------------------------
1277
+ remove(id) {
1278
+ const terminal = this.terminals.get(id);
1279
+ if (!terminal) {
1280
+ return false;
1281
+ }
1282
+ if (terminal.status === "running") {
1283
+ return false;
1284
+ }
1285
+ this.evict(id);
1286
+ return true;
1287
+ }
1288
+ // -----------------------------------------------------------------------
1289
+ // destroy — emergency forced kill (kills holder processes too)
1290
+ // -----------------------------------------------------------------------
1291
+ resize(id, cols, rows) {
1292
+ const terminal = this.terminals.get(id);
1293
+ if (!terminal || terminal.status === "exited") {
1294
+ return false;
1295
+ }
1296
+ try {
1297
+ terminal.pty.resize(cols, rows);
1298
+ terminal.cols = cols;
1299
+ terminal.rows = rows;
1300
+ return true;
1301
+ } catch {
1302
+ return false;
1303
+ }
1304
+ }
1305
+ // -----------------------------------------------------------------------
1306
+ // Private: reconnectOne — reconnect to a single persisted terminal
1307
+ // -----------------------------------------------------------------------
1308
+ appendScrollback(terminal, data) {
1309
+ terminal.scrollback += data;
1310
+ if (Buffer.byteLength(terminal.scrollback, "utf8") > MAX_SCROLLBACK_BYTES) {
1311
+ const mid = Math.floor(terminal.scrollback.length / 2);
1312
+ const newlineIdx = terminal.scrollback.indexOf("\n", mid);
1313
+ if (newlineIdx !== -1) {
1314
+ terminal.scrollback = terminal.scrollback.slice(newlineIdx + 1);
1315
+ } else {
1316
+ terminal.scrollback = terminal.scrollback.slice(mid);
1317
+ }
1318
+ }
1319
+ }
1320
+ // -----------------------------------------------------------------------
1321
+ // Private: handleReconnectFailure — handle failed reconnection
1322
+ // -----------------------------------------------------------------------
1323
+ broadcastOutput(terminal, data) {
1324
+ const msg = JSON.stringify({ data, type: "output" });
1325
+ for (const ws of terminal.clients) {
1326
+ if (ws.bufferedAmount > MAX_OUTPUT_BUFFER_BYTES) {
1327
+ this.safeSend(ws, JSON.stringify({ bytes: data.length, type: "output-dropped" }));
1328
+ continue;
1329
+ }
1330
+ const buffer = terminal.outputBuffers.get(ws);
1331
+ if (!buffer) {
1332
+ continue;
1333
+ }
1334
+ const msgSize = Buffer.byteLength(msg, "utf8");
1335
+ if (buffer.size + msgSize > MAX_OUTPUT_BUFFER_BYTES) {
1336
+ let droppedBytes = 0;
1337
+ while (buffer.size + msgSize > MAX_OUTPUT_BUFFER_BYTES && buffer.data.length > 0) {
1338
+ const dropped = buffer.data.shift();
1339
+ if (dropped) {
1340
+ const droppedSize = Buffer.byteLength(dropped, "utf8");
1341
+ buffer.size -= droppedSize;
1342
+ droppedBytes += droppedSize;
1343
+ }
1344
+ }
1345
+ if (droppedBytes > 0) {
1346
+ const dropMsg = JSON.stringify({
1347
+ bytes: droppedBytes,
1348
+ type: "output-dropped"
1349
+ });
1350
+ this.safeSend(ws, dropMsg);
1351
+ }
1352
+ }
1353
+ buffer.data.push(msg);
1354
+ buffer.size += msgSize;
1355
+ this.flushOutputBuffer(ws, buffer);
1356
+ }
1357
+ }
1358
+ // -----------------------------------------------------------------------
1359
+ // Private: startSessionDiscovery — polling for session files
1360
+ // -----------------------------------------------------------------------
1361
+ /** Evict a terminal, freeing all resources. */
1362
+ evict(id) {
1363
+ const terminal = this.terminals.get(id);
1364
+ if (!terminal) {
1365
+ return;
1366
+ }
1367
+ if (terminal.pollTimer) {
1368
+ clearInterval(terminal.pollTimer);
1369
+ terminal.pollTimer = null;
1370
+ }
1371
+ if (terminal.openCodeSessionId && terminal.openCodeNoopCb) {
1372
+ openCodeWatcher.stop(terminal.openCodeSessionId, terminal.openCodeNoopCb);
1373
+ terminal.openCodeNoopCb = null;
1374
+ }
1375
+ terminal.pty.disconnect();
1376
+ for (const ws of terminal.clients) {
1377
+ try {
1378
+ ws.close();
1379
+ } catch {
1380
+ }
1381
+ }
1382
+ terminal.clients.clear();
1383
+ terminal.outputBuffers.clear();
1384
+ terminal.scrollback = "";
1385
+ this.terminals.delete(id);
1386
+ }
1387
+ // -----------------------------------------------------------------------
1388
+ // Private: appendScrollback — append to cached scrollback string,
1389
+ // trim from midpoint when cap exceeded
1390
+ // -----------------------------------------------------------------------
1391
+ /** Attempt to flush buffered messages to a WebSocket client. */
1392
+ flushOutputBuffer(ws, buffer) {
1393
+ while (buffer.data.length > 0) {
1394
+ const msg = buffer.data[0];
1395
+ if (!this.safeSend(ws, msg)) {
1396
+ break;
1397
+ }
1398
+ buffer.data.shift();
1399
+ buffer.size -= Buffer.byteLength(msg, "utf8");
1400
+ }
1401
+ }
1402
+ // -----------------------------------------------------------------------
1403
+ // Private: broadcastOutput — send output to all connected WS clients
1404
+ // with backpressure management
1405
+ // -----------------------------------------------------------------------
1406
+ handleReconnectFailure(record) {
1407
+ if (record.holderPid) {
1408
+ try {
1409
+ process.kill(record.holderPid, 0);
1410
+ console.warn(
1411
+ `[pty-manager] Holder PID ${record.holderPid} alive but socket dead for ${record.id}`
1412
+ );
1413
+ } catch {
1414
+ }
1415
+ }
1416
+ if (record.socketPath) {
1417
+ const exitFilePath = `${record.socketPath}.exit`;
1418
+ if (existsSync(exitFilePath)) {
1419
+ try {
1420
+ const exitData = JSON.parse(readFileSync(exitFilePath, "utf8"));
1421
+ unlinkSync(exitFilePath);
1422
+ terminalStore.markExited(record.id, exitData.code);
1423
+ console.log(
1424
+ `[pty-manager] Terminal ${record.id} exited while disconnected (code=${exitData.code})`
1425
+ );
1426
+ return;
1427
+ } catch {
1428
+ }
1429
+ }
1430
+ }
1431
+ terminalStore.markOrphaned(record.id);
1432
+ console.log(`[pty-manager] Marked terminal ${record.id} as orphaned`);
1433
+ }
1434
+ async reconnectOne(record) {
1435
+ if (!record.socketPath) {
1436
+ throw new Error("No socket path stored");
1437
+ }
1438
+ const exitFilePath = `${record.socketPath}.exit`;
1439
+ if (existsSync(exitFilePath)) {
1440
+ try {
1441
+ const exitData = JSON.parse(readFileSync(exitFilePath, "utf8"));
1442
+ unlinkSync(exitFilePath);
1443
+ terminalStore.markExited(record.id, exitData.code);
1444
+ console.log(
1445
+ `[pty-manager] Terminal ${record.id} exited while disconnected (code=${exitData.code})`
1446
+ );
1447
+ return;
1448
+ } catch {
1449
+ }
1450
+ }
1451
+ const client = new HolderClient();
1452
+ const connectResult = await client.connect(record.socketPath);
1453
+ let parsedArgs = [];
1454
+ try {
1455
+ parsedArgs = JSON.parse(record.args);
1456
+ } catch {
1457
+ }
1458
+ const terminal = {
1459
+ args: parsedArgs,
1460
+ clients: /* @__PURE__ */ new Set(),
1461
+ cols: record.cols,
1462
+ command: record.command,
1463
+ createdAt: new Date(record.createdAt),
1464
+ currentCwd: null,
1465
+ cwd: record.cwd,
1466
+ exitCode: connectResult.exitCode,
1467
+ exitedAt: record.exitedAt ? new Date(record.exitedAt) : null,
1468
+ holderPid: record.holderPid ?? 0,
1469
+ id: record.id,
1470
+ isActive: false,
1471
+ openCodeNoopCb: null,
1472
+ openCodeSessionId: record.opencodeSessionId ?? null,
1473
+ outputBuffers: /* @__PURE__ */ new Map(),
1474
+ pid: connectResult.pid,
1475
+ pollTimer: null,
1476
+ pty: client,
1477
+ rows: record.rows,
1478
+ scrollback: connectResult.scrollback,
1479
+ sessionFile: record.sessionFile ?? null,
1480
+ socketPath: record.socketPath,
1481
+ status: connectResult.exited ? "exited" : "running",
1482
+ watcherOffset: 0
1483
+ };
1484
+ if (connectResult.exited) {
1485
+ terminal.exitedAt = terminal.exitedAt ?? /* @__PURE__ */ new Date();
1486
+ terminalStore.markExited(record.id, connectResult.exitCode);
1487
+ }
1488
+ this.wireHolderCallbacks(client, terminal);
1489
+ if (terminal.sessionFile) ;
1490
+ if (terminal.openCodeSessionId) {
1491
+ const noopCb = () => {
1492
+ };
1493
+ terminal.openCodeNoopCb = noopCb;
1494
+ openCodeWatcher.watch(terminal.openCodeSessionId, noopCb);
1495
+ }
1496
+ if (!terminal.sessionFile && !terminal.openCodeSessionId && terminal.status === "running") {
1497
+ this.startSessionDiscovery(terminal);
1498
+ }
1499
+ if (record.pid !== connectResult.pid) {
1500
+ terminalStore.update(record.id, { pid: connectResult.pid });
1501
+ }
1502
+ this.terminals.set(record.id, terminal);
1503
+ console.log(
1504
+ `[pty-manager] Reconnected terminal ${record.id} (pid=${connectResult.pid}, holder=${record.holderPid}, status=${terminal.status})`
1505
+ );
1506
+ }
1507
+ /** Wire up all HolderClient callbacks (activity, CWD, output, exit, disconnect). */
1508
+ wireHolderCallbacks(client, terminal) {
1509
+ client.onActivity((active) => {
1510
+ terminal.isActive = active;
1511
+ const msg = JSON.stringify({ active, type: "activity" });
1512
+ for (const ws of terminal.clients) {
1513
+ this.safeSend(ws, msg);
1514
+ }
1515
+ });
1516
+ client.onCwd((path2) => {
1517
+ terminal.currentCwd = path2;
1518
+ const msg = JSON.stringify({ path: path2, type: "cwd" });
1519
+ for (const ws of terminal.clients) {
1520
+ this.safeSend(ws, msg);
1521
+ }
1522
+ });
1523
+ client.onOutput((data) => {
1524
+ this.appendScrollback(terminal, data);
1525
+ this.broadcastOutput(terminal, data);
1526
+ });
1527
+ client.onExit((exitCode) => {
1528
+ terminal.status = "exited";
1529
+ terminal.exitCode = exitCode;
1530
+ terminal.exitedAt = /* @__PURE__ */ new Date();
1531
+ terminalStore.markExited(terminal.id, exitCode);
1532
+ const exitMsg = JSON.stringify({
1533
+ code: exitCode,
1534
+ signal: null,
1535
+ type: "exit"
1536
+ });
1537
+ for (const ws of terminal.clients) {
1538
+ this.safeSend(ws, exitMsg);
1539
+ }
1540
+ });
1541
+ client.onDisconnect(() => {
1542
+ if (terminal.status === "running") {
1543
+ console.warn(`[pty-manager] Holder disconnected unexpectedly for terminal ${terminal.id}`);
1544
+ terminal.status = "exited";
1545
+ terminal.exitedAt = /* @__PURE__ */ new Date();
1546
+ terminalStore.markOrphaned(terminal.id);
1547
+ }
1548
+ });
1549
+ }
1550
+ /** Safely send a message to a WebSocket, returning false on failure. */
1551
+ safeSend(ws, msg) {
1552
+ try {
1553
+ if (ws.readyState !== 1) {
1554
+ return false;
1555
+ }
1556
+ ws.send(msg);
1557
+ return true;
1558
+ } catch {
1559
+ return false;
1560
+ }
1561
+ }
1562
+ /** Send cached scrollback data to a newly connected client in 50 KB chunks. */
1563
+ async sendScrollback(terminal, ws) {
1564
+ const fullData = terminal.scrollback;
1565
+ if (fullData.length === 0) {
1566
+ return;
1567
+ }
1568
+ const totalBytes = Buffer.byteLength(fullData, "utf8");
1569
+ const totalChunks = Math.ceil(totalBytes / SCROLLBACK_CHUNK_SIZE);
1570
+ if (totalChunks <= 1) {
1571
+ const msg = JSON.stringify({
1572
+ chunk: 1,
1573
+ data: fullData,
1574
+ total: 1,
1575
+ type: "scrollback"
1576
+ });
1577
+ this.safeSend(ws, msg);
1578
+ return;
1579
+ }
1580
+ const buf = Buffer.from(fullData, "utf8");
1581
+ let offset = 0;
1582
+ let chunkIndex = 1;
1583
+ while (offset < buf.length) {
1584
+ if (ws.bufferedAmount > SCROLLBACK_CHUNK_SIZE * 2) {
1585
+ await new Promise((resolve) => setTimeout(resolve, 50));
1586
+ }
1587
+ const end = Math.min(offset + SCROLLBACK_CHUNK_SIZE, buf.length);
1588
+ const chunkData = buf.subarray(offset, end).toString("utf8");
1589
+ const msg = JSON.stringify({
1590
+ chunk: chunkIndex,
1591
+ data: chunkData,
1592
+ total: totalChunks,
1593
+ type: "scrollback"
1594
+ });
1595
+ this.safeSend(ws, msg);
1596
+ offset = end;
1597
+ chunkIndex++;
1598
+ }
1599
+ }
1600
+ startSessionDiscovery(terminal) {
1601
+ const { command, cwd, id } = terminal;
1602
+ if (command === "claude") {
1603
+ const projectDir = path__default.join(
1604
+ process.env.HOME || "",
1605
+ ".claude",
1606
+ "projects",
1607
+ cwd.replace(/\//g, "-")
1608
+ );
1609
+ const launchTime = terminal.createdAt.getTime();
1610
+ terminal.pollTimer = setInterval(() => {
1611
+ if (terminal.status === "exited" || terminal.sessionFile) {
1612
+ if (terminal.pollTimer) {
1613
+ clearInterval(terminal.pollTimer);
1614
+ terminal.pollTimer = null;
1615
+ }
1616
+ if (terminal.sessionFile) ;
1617
+ return;
1618
+ }
1619
+ try {
1620
+ if (!existsSync(projectDir)) {
1621
+ return;
1622
+ }
1623
+ const files = readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
1624
+ const stat = statSync(path__default.join(projectDir, f));
1625
+ return {
1626
+ birthtime: stat.birthtimeMs,
1627
+ fullPath: path__default.join(projectDir, f),
1628
+ mtime: stat.mtimeMs,
1629
+ name: f
1630
+ };
1631
+ }).filter((f) => f.birthtime > launchTime).sort((a, b) => b.birthtime - a.birthtime);
1632
+ if (files.length > 0) {
1633
+ terminal.sessionFile = files[0].fullPath;
1634
+ if (terminal.pollTimer) {
1635
+ clearInterval(terminal.pollTimer);
1636
+ terminal.pollTimer = null;
1637
+ }
1638
+ terminalStore.update(id, { sessionFile: terminal.sessionFile });
1639
+ }
1640
+ } catch {
1641
+ }
1642
+ }, 1500);
1643
+ setTimeout(() => {
1644
+ if (terminal.pollTimer) {
1645
+ clearInterval(terminal.pollTimer);
1646
+ terminal.pollTimer = null;
1647
+ }
1648
+ }, 5 * 60 * 1e3);
1649
+ }
1650
+ if (command === "opencode") {
1651
+ const launchTime = terminal.createdAt.getTime();
1652
+ const pollInterval = setInterval(() => {
1653
+ if (terminal.status === "exited" || terminal.openCodeSessionId) {
1654
+ clearInterval(pollInterval);
1655
+ if (terminal.openCodeSessionId) {
1656
+ const noopCb = () => {
1657
+ };
1658
+ terminal.openCodeNoopCb = noopCb;
1659
+ openCodeWatcher.watch(terminal.openCodeSessionId, noopCb);
1660
+ }
1661
+ return;
1662
+ }
1663
+ const sessionId = openCodeWatcher.findSessionId(cwd, launchTime);
1664
+ if (sessionId) {
1665
+ terminal.openCodeSessionId = sessionId;
1666
+ clearInterval(pollInterval);
1667
+ const noopCb = () => {
1668
+ };
1669
+ terminal.openCodeNoopCb = noopCb;
1670
+ openCodeWatcher.watch(sessionId, noopCb);
1671
+ terminalStore.update(id, { opencodeSessionId: sessionId });
1672
+ }
1673
+ }, 2e3);
1674
+ terminal.pollTimer = pollInterval;
1675
+ setTimeout(() => {
1676
+ clearInterval(pollInterval);
1677
+ terminal.pollTimer = null;
1678
+ }, 5 * 60 * 1e3);
1679
+ }
1680
+ }
1681
+ }
1682
+ function resolveHolderPath() {
1683
+ if (process.env.SHOOTER_HOLDER_PATH) {
1684
+ return process.env.SHOOTER_HOLDER_PATH;
1685
+ }
1686
+ const colocated = path__default.join(__dirname$1, "pty-holder.cjs");
1687
+ if (existsSync(colocated)) {
1688
+ return colocated;
1689
+ }
1690
+ return path__default.resolve(__dirname$1, "..", "..", "pty-holder.cjs");
1691
+ }
1692
+ const PTY_GLOBAL_KEY = "__shooter_pty_manager";
1693
+ const ptyManager = globalThis[PTY_GLOBAL_KEY] || new PtyManager();
1694
+ globalThis[PTY_GLOBAL_KEY] = ptyManager;
1695
+
1696
+ export { ptyManager as p };
1697
+ //# sourceMappingURL=pty-manager-C0FhBiVq.js.map