@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,661 @@
1
+ /**
2
+ * OpenCode Session Watcher
3
+ *
4
+ * Polls the OpenCode SQLite database for new messages in a session and
5
+ * streams them as ConversationMessage objects that the session handler
6
+ * can consume directly for both history and live updates.
7
+ *
8
+ * Unlike the JSONL-based SessionWatcher (which uses chokidar for file
9
+ * change detection), this module polls SQLite because OpenCode holds a
10
+ * write lock on the database and we must open/close quickly in read-only
11
+ * mode to avoid contention.
12
+ */
13
+
14
+ import Database from 'better-sqlite3';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+
18
+ import type { ConversationMessage, MessagePart } from '../sessions/types';
19
+
20
+ // ── Constants ────────────────────────────────────────────────────────
21
+
22
+ const OPENCODE_DB_PATH = (() => {
23
+ if (process.platform === 'darwin') {
24
+ return path.join(process.env.HOME || '', 'Library', 'Application Support', 'opencode', 'opencode.db');
25
+ }
26
+ const xdgData = process.env.XDG_DATA_HOME || path.join(process.env.HOME || '', '.local', 'share');
27
+ return path.join(xdgData, 'opencode', 'opencode.db');
28
+ })();
29
+
30
+ /** Poll interval in milliseconds. */
31
+ const POLL_INTERVAL_MS = 2000;
32
+
33
+ /** Maximum parameters per SQLite IN clause (SQLite limit is 999). */
34
+ const SQLITE_MAX_PARAMS = 500;
35
+
36
+ /**
37
+ * Normalise a timestamp from OpenCode's SQLite database to milliseconds.
38
+ *
39
+ * OpenCode currently stores `time_created` / `time_updated` as Unix
40
+ * **milliseconds**, but this is not formally documented and could change.
41
+ * A simple heuristic distinguishes seconds from milliseconds:
42
+ * - Values < 1e12 (~2001-09-09 in ms, ~33658 AD in seconds) are seconds.
43
+ * - Values >= 1e12 are already milliseconds.
44
+ */
45
+ function toMillis(timestamp: number): number {
46
+ return timestamp < 1e12 ? timestamp * 1000 : timestamp;
47
+ }
48
+
49
+ // ── SQLite Row Types ─────────────────────────────────────────────────
50
+
51
+ interface OpenCodeMessage {
52
+ data: string; // JSON
53
+ id: string;
54
+ session_id: string;
55
+ time_created: number;
56
+ time_updated: number;
57
+ }
58
+
59
+ interface OpenCodePart {
60
+ data: string; // JSON
61
+ id: string;
62
+ message_id: string;
63
+ session_id: string;
64
+ time_created: number;
65
+ time_updated: number;
66
+ }
67
+
68
+ /** Raw part data as stored in OpenCode's SQLite `part.data` JSON column. */
69
+ interface OpenCodePartData {
70
+ callID?: string;
71
+ id?: string;
72
+ state?: { input?: Record<string, unknown> };
73
+ text?: string;
74
+ tool?: string;
75
+ type: string;
76
+ }
77
+
78
+ // ── Per-session Watcher State ────────────────────────────────────────
79
+
80
+ interface OpenCodeSession {
81
+ directory: string;
82
+ id: string;
83
+ time_created: number;
84
+ time_updated: number;
85
+ }
86
+
87
+ interface WatchState {
88
+ callbacks: Set<(messages: ConversationMessage[]) => void>;
89
+ /** Set of message IDs we have already emitted, to avoid duplicates. */
90
+ emittedMessageIds: Set<string>;
91
+ /** Set of part IDs we have already emitted, to avoid duplicates on update. */
92
+ emittedPartIds: Set<string>;
93
+ intervalHandle: ReturnType<typeof setInterval>;
94
+ /** Highest time_created we have seen for messages (milliseconds). */
95
+ lastMessageTime: number;
96
+ /** Highest time_updated we have seen for parts (milliseconds). */
97
+ lastPartTime: number;
98
+ sessionId: string;
99
+ }
100
+
101
+ // ── OpenCodeWatcher Class ────────────────────────────────────────────
102
+
103
+ class OpenCodeWatcher {
104
+ private watchers = new Map<string, WatchState>();
105
+
106
+ /**
107
+ * Find the most recent non-archived OpenCode session that matches
108
+ * the given working directory. Checks session.directory equals or
109
+ * starts with `cwd`.
110
+ *
111
+ * Returns the session ID or null if none found.
112
+ */
113
+ findSessionId(cwd: string, createdAfter?: number): null | string {
114
+ const db = openDb();
115
+ if (!db) {
116
+ return null;
117
+ }
118
+
119
+ try {
120
+ // Match sessions that were active (updated) after the terminal was launched.
121
+ // OpenCode resumes existing sessions rather than always creating new ones,
122
+ // so we filter on time_updated (not time_created) to find the session
123
+ // that's being actively used by this terminal instance.
124
+ //
125
+ // createdAfter is JS milliseconds (Date.getTime()), but OpenCode may
126
+ // store time_updated in seconds. Use the smaller of the two
127
+ // representations so the filter works regardless of DB unit.
128
+ const timeFilter = createdAfter
129
+ ? Math.min(createdAfter, Math.floor(createdAfter / 1000))
130
+ : 0;
131
+ const row = db
132
+ .prepare(
133
+ `
134
+ SELECT id
135
+ FROM session
136
+ WHERE (time_archived IS NULL OR time_archived = 0)
137
+ AND (directory = ? OR directory LIKE ? || '/%')
138
+ AND time_updated > ?
139
+ ORDER BY time_updated DESC
140
+ LIMIT 1
141
+ `
142
+ )
143
+ .get(cwd, cwd, timeFilter) as OpenCodeSession | undefined;
144
+
145
+ return row?.id ?? null;
146
+ } catch (error) {
147
+ console.error('[opencode-watcher] Failed to find session:', error);
148
+ return null;
149
+ } finally {
150
+ db.close();
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Read all messages and parts for a session from SQLite, converting
156
+ * them to ConversationMessage format.
157
+ */
158
+ getHistory(sessionId: string): ConversationMessage[] {
159
+ const db = openDb();
160
+ if (!db) {
161
+ return [];
162
+ }
163
+
164
+ try {
165
+ // Fetch all messages for this session, ordered chronologically.
166
+ const messages = db
167
+ .prepare(
168
+ `
169
+ SELECT id, session_id, time_created, time_updated, data
170
+ FROM message
171
+ WHERE session_id = ?
172
+ ORDER BY time_created ASC
173
+ `
174
+ )
175
+ .all(sessionId) as OpenCodeMessage[];
176
+
177
+ if (messages.length === 0) {
178
+ return [];
179
+ }
180
+
181
+ // Fetch all parts for these messages (batched to avoid SQLite param limit).
182
+ const messageIds = messages.map((m) => m.id);
183
+ const parts = batchInQuery<OpenCodePart>(
184
+ db,
185
+ `SELECT id, message_id, session_id, time_created, time_updated, data
186
+ FROM part
187
+ WHERE message_id IN (__PLACEHOLDERS__)
188
+ ORDER BY time_created ASC`,
189
+ messageIds
190
+ );
191
+
192
+ // Group parts by message ID.
193
+ const partsByMessage = new Map<string, OpenCodePart[]>();
194
+ for (const part of parts) {
195
+ if (!partsByMessage.has(part.message_id)) {
196
+ partsByMessage.set(part.message_id, []);
197
+ }
198
+ partsByMessage.get(part.message_id)!.push(part);
199
+ }
200
+
201
+ return this.buildMessages(messages, partsByMessage);
202
+ } catch (error) {
203
+ console.error('[opencode-watcher] Failed to read history:', error);
204
+ return [];
205
+ } finally {
206
+ db.close();
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Stop watching a specific session. If a callback is provided, only that
212
+ * subscriber is removed — the interval keeps running while other subscribers
213
+ * remain. If no callback is provided, all subscribers and the interval are
214
+ * cleared (backward compat).
215
+ */
216
+ stop(sessionId: string, callback?: (messages: ConversationMessage[]) => void): void {
217
+ const state = this.watchers.get(sessionId);
218
+ if (!state) {
219
+ return;
220
+ }
221
+
222
+ if (callback) {
223
+ state.callbacks.delete(callback);
224
+ console.log(
225
+ `[opencode-watcher] Removed subscriber from session: ${sessionId} ` +
226
+ `(remaining=${state.callbacks.size})`
227
+ );
228
+
229
+ // Only tear down the interval when no subscribers remain.
230
+ if (state.callbacks.size > 0) {
231
+ return;
232
+ }
233
+ }
234
+
235
+ clearInterval(state.intervalHandle);
236
+ this.watchers.delete(sessionId);
237
+ console.log(`[opencode-watcher] Stopped watching session: ${sessionId}`);
238
+ }
239
+
240
+ /**
241
+ * Stop all active watchers.
242
+ */
243
+ stopAll(): void {
244
+ for (const [sessionId] of this.watchers) {
245
+ this.stop(sessionId);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Start polling the SQLite DB every 2 seconds for new messages/parts
251
+ * in the given session. Converts new data to ConversationMessage
252
+ * format and invokes the callback.
253
+ */
254
+ watch(sessionId: string, callback: (messages: ConversationMessage[]) => void): void {
255
+ const existing = this.watchers.get(sessionId);
256
+ if (existing) {
257
+ // Already watching — just add the new callback, don't create a new interval.
258
+ existing.callbacks.add(callback);
259
+ console.log(
260
+ `[opencode-watcher] Added subscriber to session: ${sessionId} ` +
261
+ `(total=${existing.callbacks.size})`
262
+ );
263
+ return;
264
+ }
265
+
266
+ // Determine the initial high-water marks by scanning existing data.
267
+ const { emittedMessageIds, emittedPartIds, lastMessageTime, lastPartTime } =
268
+ this.getHighWaterMarks(sessionId);
269
+
270
+ const intervalHandle = setInterval(() => {
271
+ this.poll(sessionId);
272
+ }, POLL_INTERVAL_MS);
273
+
274
+ const state: WatchState = {
275
+ callbacks: new Set([callback]),
276
+ emittedMessageIds,
277
+ emittedPartIds,
278
+ intervalHandle,
279
+ lastMessageTime,
280
+ lastPartTime,
281
+ sessionId
282
+ };
283
+
284
+ this.watchers.set(sessionId, state);
285
+ console.log(
286
+ `[opencode-watcher] Watching session: ${sessionId} ` +
287
+ `(lastMsg=${lastMessageTime}, lastPart=${lastPartTime})`
288
+ );
289
+ }
290
+
291
+ // ── Private Helpers ────────────────────────────────────────────────
292
+
293
+ /**
294
+ * Convert OpenCode messages + their parts into ConversationMessage
295
+ * objects for consumption by the session handler.
296
+ */
297
+ private buildMessages(
298
+ messages: OpenCodeMessage[],
299
+ partsByMessage: Map<string, OpenCodePart[]>
300
+ ): ConversationMessage[] {
301
+ const result: ConversationMessage[] = [];
302
+
303
+ for (const msg of messages) {
304
+ // Parse message data to determine role.
305
+ let msgData: { agent?: string; role?: string } = {};
306
+ try {
307
+ msgData = JSON.parse(msg.data) as typeof msgData;
308
+ } catch {
309
+ // Skip unparseable message data.
310
+ continue;
311
+ }
312
+
313
+ const role = msgData.role === 'user' ? 'user' : 'assistant';
314
+ const msgParts = partsByMessage.get(msg.id) || [];
315
+
316
+ // Convert each part to a MessagePart.
317
+ const parts: MessagePart[] = [];
318
+
319
+ for (const part of msgParts) {
320
+ let partData: OpenCodePartData;
321
+ try {
322
+ partData = JSON.parse(part.data) as OpenCodePartData;
323
+ } catch {
324
+ continue;
325
+ }
326
+
327
+ const converted = convertPartToMessagePart(partData);
328
+ if (converted) {
329
+ parts.push(converted);
330
+ }
331
+ }
332
+
333
+ // Skip messages with no usable content.
334
+ if (parts.length === 0) {
335
+ console.debug(`[opencode-watcher] Skipping message ${msg.id} (no usable parts)`);
336
+ continue;
337
+ }
338
+
339
+ result.push({
340
+ id: msg.id,
341
+ parts,
342
+ role,
343
+ timestamp: new Date(toMillis(msg.time_created)).toISOString(),
344
+ });
345
+ }
346
+
347
+ return result;
348
+ }
349
+
350
+ /**
351
+ * Scan existing messages/parts to determine the starting high-water
352
+ * marks for time-based polling. Also collects the set of already-seen
353
+ * message IDs so we do not re-emit them.
354
+ */
355
+ private getHighWaterMarks(sessionId: string): {
356
+ emittedMessageIds: Set<string>;
357
+ emittedPartIds: Set<string>;
358
+ lastMessageTime: number;
359
+ lastPartTime: number;
360
+ } {
361
+ const db = openDb();
362
+ if (!db) {
363
+ return { emittedMessageIds: new Set(), emittedPartIds: new Set(), lastMessageTime: 0, lastPartTime: 0 };
364
+ }
365
+
366
+ try {
367
+ const msgRow = db
368
+ .prepare(
369
+ `
370
+ SELECT MAX(time_created) as maxTime
371
+ FROM message
372
+ WHERE session_id = ?
373
+ `
374
+ )
375
+ .get(sessionId) as undefined | { maxTime: null | number };
376
+
377
+ const partRow = db
378
+ .prepare(
379
+ `
380
+ SELECT MAX(time_updated) as maxTime
381
+ FROM part
382
+ WHERE session_id = ?
383
+ `
384
+ )
385
+ .get(sessionId) as undefined | { maxTime: null | number };
386
+
387
+ // Collect all existing message IDs.
388
+ const existingIds = db
389
+ .prepare(
390
+ `
391
+ SELECT id FROM message WHERE session_id = ?
392
+ `
393
+ )
394
+ .all(sessionId) as { id: string }[];
395
+
396
+ const emittedMessageIds = new Set(existingIds.map((r) => r.id));
397
+
398
+ // Collect all existing part IDs.
399
+ const existingPartIds = db
400
+ .prepare(
401
+ `
402
+ SELECT id FROM part WHERE session_id = ?
403
+ `
404
+ )
405
+ .all(sessionId) as { id: string }[];
406
+
407
+ const emittedPartIds = new Set(existingPartIds.map((r) => r.id));
408
+
409
+ return {
410
+ emittedMessageIds,
411
+ emittedPartIds,
412
+ lastMessageTime: msgRow?.maxTime ?? 0,
413
+ lastPartTime: partRow?.maxTime ?? 0
414
+ };
415
+ } catch (error) {
416
+ console.error('[opencode-watcher] Failed to get high-water marks:', error);
417
+ return { emittedMessageIds: new Set(), emittedPartIds: new Set(), lastMessageTime: 0, lastPartTime: 0 };
418
+ } finally {
419
+ db.close();
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Single poll iteration. Opens the DB, queries for new messages and
425
+ * updated parts, converts them to ConversationMessage[], and invokes
426
+ * the watcher callbacks.
427
+ */
428
+ private poll(sessionId: string): void {
429
+ const state = this.watchers.get(sessionId);
430
+ if (!state) {
431
+ return;
432
+ }
433
+
434
+ const db = openDb();
435
+ if (!db) {
436
+ return;
437
+ }
438
+
439
+ try {
440
+ const results: ConversationMessage[] = [];
441
+
442
+ // ── 1. Check for brand-new messages ──────────────────────────
443
+ const newMessages = db
444
+ .prepare(
445
+ `
446
+ SELECT id, session_id, time_created, time_updated, data
447
+ FROM message
448
+ WHERE session_id = ? AND time_created > ?
449
+ ORDER BY time_created ASC
450
+ `
451
+ )
452
+ .all(sessionId, state.lastMessageTime) as OpenCodeMessage[];
453
+
454
+ if (newMessages.length > 0) {
455
+ // Deduplicate: skip messages we have already emitted (guards
456
+ // against clock-tie edge cases where time_created equals the
457
+ // high-water mark and the row slips through the > filter again).
458
+ const dedupedMessages = newMessages.filter(
459
+ (m) => !state.emittedMessageIds.has(m.id)
460
+ );
461
+
462
+ if (dedupedMessages.length > 0) {
463
+ // Fetch parts for these new messages (batched to avoid SQLite param limit).
464
+ const newMsgIds = dedupedMessages.map((m) => m.id);
465
+ const newParts = batchInQuery<OpenCodePart>(
466
+ db,
467
+ `SELECT id, message_id, session_id, time_created, time_updated, data
468
+ FROM part
469
+ WHERE message_id IN (__PLACEHOLDERS__)
470
+ ORDER BY time_created ASC`,
471
+ newMsgIds
472
+ );
473
+
474
+ // Group parts by message.
475
+ const partsByMessage = new Map<string, OpenCodePart[]>();
476
+ for (const part of newParts) {
477
+ if (!partsByMessage.has(part.message_id)) {
478
+ partsByMessage.set(part.message_id, []);
479
+ }
480
+ partsByMessage.get(part.message_id)!.push(part);
481
+ }
482
+
483
+ const newEntries = this.buildMessages(dedupedMessages, partsByMessage);
484
+ results.push(...newEntries);
485
+
486
+ // Track emitted part IDs and update part high-water mark.
487
+ for (const part of newParts) {
488
+ if (part.time_updated > state.lastPartTime) {
489
+ state.lastPartTime = part.time_updated;
490
+ }
491
+ state.emittedPartIds.add(part.id);
492
+ }
493
+ }
494
+
495
+ // Always update message high-water marks (even for already-emitted messages).
496
+ for (const msg of newMessages) {
497
+ if (msg.time_created > state.lastMessageTime) {
498
+ state.lastMessageTime = msg.time_created;
499
+ }
500
+ state.emittedMessageIds.add(msg.id);
501
+ }
502
+ }
503
+
504
+ // ── 2. Check for updated parts on existing messages ──────────
505
+ // Parts may be added or updated after the initial message row is
506
+ // created (e.g., streaming assistant response). Look for parts
507
+ // whose time_updated exceeds our last-seen mark, but whose parent
508
+ // message is NOT in the new-messages set (those were handled above).
509
+ const updatedParts = db
510
+ .prepare(
511
+ `
512
+ SELECT id, message_id, session_id, time_created, time_updated, data
513
+ FROM part
514
+ WHERE session_id = ? AND time_updated > ?
515
+ ORDER BY time_created ASC
516
+ `
517
+ )
518
+ .all(sessionId, state.lastPartTime) as OpenCodePart[];
519
+
520
+ if (updatedParts.length > 0) {
521
+ // Filter out parts that belong to messages we just processed,
522
+ // AND parts we have already emitted (avoid duplicates).
523
+ const newMsgIdSet = new Set(newMessages.map((m) => m.id));
524
+ const newParts = updatedParts.filter(
525
+ (p) => !newMsgIdSet.has(p.message_id) && !state.emittedPartIds.has(p.id)
526
+ );
527
+
528
+ if (newParts.length > 0) {
529
+ // Group the NEW parts by message ID.
530
+ const partsByMessage = new Map<string, OpenCodePart[]>();
531
+ for (const part of newParts) {
532
+ if (!partsByMessage.has(part.message_id)) {
533
+ partsByMessage.set(part.message_id, []);
534
+ }
535
+ partsByMessage.get(part.message_id)!.push(part);
536
+ }
537
+
538
+ // Fetch the parent messages for context (batched to avoid SQLite param limit).
539
+ const affectedMsgIds = [...partsByMessage.keys()];
540
+ const affectedMessages = batchInQuery<OpenCodeMessage>(
541
+ db,
542
+ `SELECT id, session_id, time_created, time_updated, data
543
+ FROM message
544
+ WHERE id IN (__PLACEHOLDERS__)
545
+ ORDER BY time_created ASC`,
546
+ affectedMsgIds
547
+ );
548
+
549
+ // Build entries from ONLY the new parts (delta), not all
550
+ // parts for the message. This prevents re-emitting content
551
+ // the session handler has already seen.
552
+ const updatedEntries = this.buildMessages(affectedMessages, partsByMessage);
553
+ results.push(...updatedEntries);
554
+
555
+ // Track emitted part IDs.
556
+ for (const part of newParts) {
557
+ state.emittedPartIds.add(part.id);
558
+ }
559
+ }
560
+
561
+ // Update part high-water mark.
562
+ for (const part of updatedParts) {
563
+ if (part.time_updated > state.lastPartTime) {
564
+ state.lastPartTime = part.time_updated;
565
+ }
566
+ }
567
+ }
568
+
569
+ // ── 3. Invoke all callbacks if there are new entries ─────────
570
+ if (results.length > 0) {
571
+ for (const cb of state.callbacks) {
572
+ try {
573
+ cb(results);
574
+ } catch (cbError) {
575
+ console.error('[opencode-watcher] Callback error:', cbError);
576
+ }
577
+ }
578
+ }
579
+ } catch (error) {
580
+ // Log but do not crash — the next poll will retry.
581
+ console.error('[opencode-watcher] Poll error:', error);
582
+ } finally {
583
+ db.close();
584
+ }
585
+ }
586
+ }
587
+
588
+ // ── Database Helpers ─────────────────────────────────────────────────
589
+
590
+ /**
591
+ * Execute a SELECT query with an IN clause, batching in chunks of
592
+ * SQLITE_MAX_PARAMS to stay within SQLite's 999-parameter limit.
593
+ */
594
+ function batchInQuery<T>(
595
+ db: Database.Database,
596
+ sql: string,
597
+ ids: string[]
598
+ ): T[] {
599
+ if (ids.length === 0) return [];
600
+
601
+ const results: T[] = [];
602
+ for (let i = 0; i < ids.length; i += SQLITE_MAX_PARAMS) {
603
+ const chunk = ids.slice(i, i + SQLITE_MAX_PARAMS);
604
+ const placeholders = chunk.map(() => '?').join(',');
605
+ const query = sql.replace('__PLACEHOLDERS__', placeholders);
606
+ const rows = db.prepare(query).all(...chunk) as T[];
607
+ results.push(...rows);
608
+ }
609
+ return results;
610
+ }
611
+
612
+ // ── Part Conversion ──────────────────────────────────────────────────
613
+ // Maps OpenCode part types to MessagePart directly, skipping the
614
+ // intermediate Record<string, unknown> stage.
615
+
616
+ function convertPartToMessagePart(data: OpenCodePartData): MessagePart | null {
617
+ switch (data.type) {
618
+ case 'reasoning':
619
+ return { content: data.text || '', type: 'thinking' };
620
+
621
+ case 'text':
622
+ return { content: data.text || '', type: 'text' };
623
+
624
+ case 'tool':
625
+ return {
626
+ id: data.callID || data.id || '',
627
+ input: data.state?.input || {},
628
+ toolName: data.tool || 'Unknown',
629
+ type: 'tool_use'
630
+ };
631
+
632
+ default:
633
+ // Skip snapshot, patch, step-start, step-finish, subtask, retry, compaction
634
+ return null;
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Open the OpenCode SQLite database in read-only mode.
640
+ * Returns null if the file does not exist or cannot be opened.
641
+ */
642
+ function openDb(): Database.Database | null {
643
+ if (!fs.existsSync(OPENCODE_DB_PATH)) {
644
+ return null;
645
+ }
646
+ try {
647
+ return new Database(OPENCODE_DB_PATH, { readonly: true });
648
+ } catch {
649
+ return null;
650
+ }
651
+ }
652
+
653
+ // ── Singleton ────────────────────────────────────────────────────────
654
+ // Use globalThis to ensure a single shared instance across module
655
+ // loaders (same pattern as pty-manager and session-watcher).
656
+
657
+ const OW_GLOBAL_KEY = '__shooter_opencode_watcher';
658
+ export const openCodeWatcher: OpenCodeWatcher =
659
+ ((globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] as OpenCodeWatcher) ||
660
+ new OpenCodeWatcher();
661
+ (globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] = openCodeWatcher;