@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,1431 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Unified Shooter Notifier v3.0
5
+ * Works with both Claude Code and OpenCode
6
+ *
7
+ * Claude Code: Called via CLI with event type argument + JSON on stdin
8
+ * node notifier.cjs PreToolUse (stdin: { tool_name, tool_input, ... })
9
+ * node notifier.cjs Stop (stdin: { session_id, ... })
10
+ * node notifier.cjs Notification (stdin: { notification_type, message, title, ... })
11
+ * node notifier.cjs PermissionRequest (stdin: { tool_name, tool_input, ... })
12
+ *
13
+ * OpenCode: Import as plugin module
14
+ * Place in ~/.config/opencode/plugins/ or .opencode/plugins/
15
+ */
16
+
17
+ const https = require('https');
18
+ const http = require('http');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // ============================================
23
+ // SECTION 1: Configuration & Runtime Detection
24
+ // ============================================
25
+
26
+ // Detect runtime environment
27
+ const IS_OPENCODE =
28
+ typeof process.env.OPENCODE_VERSION !== 'undefined' ||
29
+ require.main !== module ||
30
+ process.argv[1]?.includes('opencode');
31
+ const IS_CLAUDE_CODE = !IS_OPENCODE && require.main === module;
32
+ const RUNTIME = IS_OPENCODE ? 'opencode' : 'claude-code';
33
+
34
+ // Environment configuration
35
+ const USE_LOCAL = process.env.SHOOTER_USE_LOCAL === 'true';
36
+ const LOCAL_PORT = process.env.SHOOTER_LOCAL_PORT || '3000';
37
+ const REMOTE_BASE_URL = process.env.SHOOTER_API_URL || '';
38
+ const LOCAL_BASE_URL = `http://localhost:${LOCAL_PORT}`;
39
+ const BASE_URL = USE_LOCAL ? LOCAL_BASE_URL : REMOTE_BASE_URL;
40
+ const API_URL = `${BASE_URL}/api/notify`;
41
+
42
+ // Authentication
43
+ const API_KEY = process.env.API_KEY || process.env.SHOOTER_API_KEY;
44
+ const DEVICE_TOKEN = process.env.SHOOTER_DEVICE_TOKEN || null;
45
+ const AUTH_KEY = API_KEY || '';
46
+
47
+ // Validate required environment variables ONLY for Claude Code CLI mode
48
+ if (IS_CLAUDE_CODE && !API_KEY) {
49
+ console.error('API_KEY environment variable is required');
50
+ process.exit(1);
51
+ }
52
+
53
+ // Completion detection timeout
54
+ const COMPLETION_TIMEOUT = 45000; // 45 seconds
55
+
56
+ // Bidirectional permission response polling
57
+ const PERMISSION_TIMEOUT = parseInt(process.env.SHOOTER_PERMISSION_TIMEOUT || '120') * 1000;
58
+ const POLL_INTERVAL = 2000; // 2 seconds between polls
59
+ const RESPONSE_URL = `${BASE_URL}/api/response`;
60
+ const STATE_DIR = `/tmp/claude_session_tracker`;
61
+
62
+ // Global timeout tracker per project (for OpenCode)
63
+ const completionTimers = new Map();
64
+
65
+ // Debug logging flag
66
+ const DEBUG_ENABLED = process.env.SHOOTER_DEBUG === 'true';
67
+ const DEBUG_LOG_FILE = '/tmp/shooter-debug.log';
68
+
69
+ // ============================================
70
+ // SECTION 1.5: WebSocket Client Detection
71
+ // ============================================
72
+
73
+ /**
74
+ * Check if any WebSocket clients are connected to the events channel.
75
+ * When clients are connected, the WebSocket events broadcast handles
76
+ * permission-requested notifications, so we can skip push notifications.
77
+ */
78
+ async function hasWebSocketClients() {
79
+ try {
80
+ const url = `${BASE_URL}/api/ws-status`;
81
+ const protocol = url.startsWith('https') ? https : http;
82
+ return new Promise((resolve) => {
83
+ const req = protocol.request(url, {
84
+ method: 'GET',
85
+ headers: { Authorization: `Bearer ${AUTH_KEY}` },
86
+ timeout: 3000,
87
+ }, (res) => {
88
+ let data = '';
89
+ res.on('data', (chunk) => (data += chunk));
90
+ res.on('end', () => {
91
+ if (res.statusCode === 200) {
92
+ try {
93
+ const parsed = JSON.parse(data);
94
+ resolve(parsed.connectedClients > 0);
95
+ } catch (e) {
96
+ resolve(false);
97
+ }
98
+ } else {
99
+ resolve(false);
100
+ }
101
+ });
102
+ });
103
+ req.on('error', () => resolve(false));
104
+ req.on('timeout', () => { req.destroy(); resolve(false); });
105
+ req.end();
106
+ });
107
+ } catch (e) {
108
+ // If we can't reach the server, fall back to push
109
+ return false;
110
+ }
111
+ }
112
+
113
+ // ============================================
114
+ // SECTION 2: Stdin Reader (Claude Code)
115
+ // ============================================
116
+
117
+ /**
118
+ * Read JSON data from stdin (Claude Code passes event data this way)
119
+ * Returns parsed JSON or null if stdin is empty/not JSON
120
+ */
121
+ function readStdin() {
122
+ return new Promise((resolve) => {
123
+ // If stdin is a TTY (interactive terminal), no data to read
124
+ if (process.stdin.isTTY) {
125
+ resolve(null);
126
+ return;
127
+ }
128
+
129
+ let data = '';
130
+ const timeout = setTimeout(() => {
131
+ // Timeout after 1 second - stdin may not have data
132
+ process.stdin.removeAllListeners('data');
133
+ process.stdin.removeAllListeners('end');
134
+ resolve(null);
135
+ }, 1000);
136
+
137
+ process.stdin.setEncoding('utf8');
138
+ process.stdin.on('data', (chunk) => {
139
+ data += chunk;
140
+ });
141
+ process.stdin.on('end', () => {
142
+ clearTimeout(timeout);
143
+ if (data.trim()) {
144
+ try {
145
+ resolve(JSON.parse(data.trim()));
146
+ } catch (e) {
147
+ debugLog(`Failed to parse stdin JSON: ${e.message}`);
148
+ resolve(null);
149
+ }
150
+ } else {
151
+ resolve(null);
152
+ }
153
+ });
154
+ process.stdin.resume();
155
+ });
156
+ }
157
+
158
+ // ============================================
159
+ // SECTION 3: Common Event Format
160
+ // ============================================
161
+
162
+ /**
163
+ * Common Event Format - all events normalized to this structure
164
+ *
165
+ * eventType values:
166
+ * 'tool.before' - Tool is about to execute (activity tracking)
167
+ * 'tool.after' - Tool finished executing (activity tracking)
168
+ * 'session.idle' - Agent finished responding (completion timer)
169
+ * 'session.start' - New session started
170
+ * 'permission' - Agent needs permission to run a tool
171
+ * 'question' - Agent is asking user a question / presenting options
172
+ * 'idle_input' - Agent is idle, waiting for user to type
173
+ * 'intervention' - Generic intervention needed (fallback)
174
+ * 'error' - An error occurred
175
+ * 'check.completion' - Manual completion check
176
+ * 'session.status' - Internal status update (ignored)
177
+ */
178
+ function createCommonEvent(source, eventType, data = {}) {
179
+ return {
180
+ source, // 'claude-code' | 'opencode'
181
+ eventType,
182
+ timestamp: new Date().toISOString(),
183
+ projectName: getProjectName(),
184
+ data,
185
+ };
186
+ }
187
+
188
+ // ============================================
189
+ // SECTION 4: Event Adapters
190
+ // ============================================
191
+
192
+ /**
193
+ * Adapter: Claude Code CLI + stdin JSON -> Common Event Format
194
+ *
195
+ * Claude Code passes rich JSON on stdin with fields like:
196
+ * - tool_name, tool_input (for PreToolUse, PermissionRequest)
197
+ * - notification_type, message, title (for Notification)
198
+ * - session_id, cwd, hook_event_name (common to all)
199
+ */
200
+ function adaptClaudeCodeEvent(cliArg, stdinData) {
201
+ const data = {};
202
+
203
+ // --- PermissionRequest: Agent needs user permission to run a tool ---
204
+ if (cliArg === 'PermissionRequest') {
205
+ data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
206
+ data.toolInput = stdinData?.tool_input || {};
207
+ // Extract meaningful details from tool input
208
+ data.command = data.toolInput.command || '';
209
+ data.filePath = data.toolInput.file_path || '';
210
+ data.description = data.toolInput.description || '';
211
+ data.sessionId = stdinData?.session_id || '';
212
+ return createCommonEvent('claude-code', 'permission', data);
213
+ }
214
+
215
+ // --- Notification: Different subtypes based on notification_type ---
216
+ if (cliArg === 'Notification') {
217
+ const notificationType = stdinData?.notification_type || '';
218
+ data.message = stdinData?.message || '';
219
+ data.title = stdinData?.title || '';
220
+ data.notificationType = notificationType;
221
+
222
+ switch (notificationType) {
223
+ case 'permission_prompt':
224
+ // Permission prompt notification - agent waiting for permission approval.
225
+ // Use 'permission_notification' (not 'permission') to avoid triggering
226
+ // the blocking bidirectional poll flow in handlePermission().
227
+ data.tool = ''; // Not available in notification event, just message
228
+ return createCommonEvent('claude-code', 'permission_notification', data);
229
+
230
+ case 'elicitation_dialog':
231
+ // Agent is presenting a question/dialog to the user
232
+ return createCommonEvent('claude-code', 'question', data);
233
+
234
+ case 'idle_prompt':
235
+ // Agent is idle, waiting for user to type something
236
+ return createCommonEvent('claude-code', 'idle_input', data);
237
+
238
+ case 'auth_success':
239
+ // Auth completed - not actionable, ignore
240
+ debugLog('auth_success notification - ignoring');
241
+ return createCommonEvent('claude-code', 'session.status', data);
242
+
243
+ default:
244
+ // Unknown notification type - send with whatever info we have
245
+ return createCommonEvent('claude-code', 'intervention', data);
246
+ }
247
+ }
248
+
249
+ // --- PreToolUse: Tool is about to execute (activity tracking only) ---
250
+ if (cliArg === 'PreToolUse') {
251
+ data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
252
+ data.files = process.env.CLAUDE_FILE_PATHS || '';
253
+ data.command = stdinData?.tool_input?.command || process.env.CLAUDE_COMMAND_LINE || '';
254
+ return createCommonEvent('claude-code', 'tool.before', data);
255
+ }
256
+
257
+ // --- PostToolUse: Tool finished executing (activity tracking) ---
258
+ if (cliArg === 'PostToolUse') {
259
+ data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
260
+ data.files = process.env.CLAUDE_FILE_PATHS || '';
261
+ data.command = stdinData?.tool_input?.command || process.env.CLAUDE_COMMAND_LINE || '';
262
+ return createCommonEvent('claude-code', 'tool.after', data);
263
+ }
264
+
265
+ // --- PostToolUseFailure: Tool execution failed ---
266
+ if (cliArg === 'PostToolUseFailure') {
267
+ data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
268
+ data.message = stdinData?.error || 'Tool execution failed';
269
+ data.files = process.env.CLAUDE_FILE_PATHS || '';
270
+ data.command = stdinData?.tool_input?.command || process.env.CLAUDE_COMMAND_LINE || '';
271
+ return createCommonEvent('claude-code', 'error', data);
272
+ }
273
+
274
+ // --- Stop: Agent finished responding ---
275
+ if (cliArg === 'Stop') {
276
+ return createCommonEvent('claude-code', 'session.idle', data);
277
+ }
278
+
279
+ // --- SessionStart: New session started ---
280
+ if (cliArg === 'SessionStart') {
281
+ return createCommonEvent('claude-code', 'session.start', data);
282
+ }
283
+
284
+ // --- SessionEnd: Session terminated ---
285
+ if (cliArg === 'SessionEnd') {
286
+ return createCommonEvent('claude-code', 'session.end', data);
287
+ }
288
+
289
+ // --- SubagentStart: Subagent spawned ---
290
+ if (cliArg === 'SubagentStart') {
291
+ data.agentType = stdinData?.agent_type || 'unknown';
292
+ return createCommonEvent('claude-code', 'subagent.start', data);
293
+ }
294
+
295
+ // --- SubagentStop: Subagent finished ---
296
+ if (cliArg === 'SubagentStop') {
297
+ data.agentType = stdinData?.agent_type || 'unknown';
298
+ return createCommonEvent('claude-code', 'subagent.stop', data);
299
+ }
300
+
301
+ // --- UserPromptSubmit: User submitted a prompt ---
302
+ if (cliArg === 'UserPromptSubmit') {
303
+ data.message = stdinData?.message || '';
304
+ return createCommonEvent('claude-code', 'user.prompt', data);
305
+ }
306
+
307
+ // --- TeammateIdle: Agent teammate went idle ---
308
+ if (cliArg === 'TeammateIdle') {
309
+ data.teammate = stdinData?.agent_name || stdinData?.name || 'unknown';
310
+ return createCommonEvent('claude-code', 'teammate.idle', data);
311
+ }
312
+
313
+ // --- TaskCompleted: A task was marked complete ---
314
+ if (cliArg === 'TaskCompleted') {
315
+ data.taskId = stdinData?.task_id || '';
316
+ data.message = stdinData?.subject || '';
317
+ return createCommonEvent('claude-code', 'task.completed', data);
318
+ }
319
+
320
+ // --- PreCompact: Context about to be compacted ---
321
+ if (cliArg === 'PreCompact') {
322
+ return createCommonEvent('claude-code', 'context.compact', data);
323
+ }
324
+
325
+ // --- CheckCompletion: Manual check ---
326
+ if (cliArg === 'CheckCompletion') {
327
+ return createCommonEvent('claude-code', 'check.completion', data);
328
+ }
329
+
330
+ // --- Unknown event type ---
331
+ data.rawArg = cliArg;
332
+ return createCommonEvent('claude-code', 'unknown', data);
333
+ }
334
+
335
+ /**
336
+ * Adapter: OpenCode Hook Events -> Common Event Format
337
+ */
338
+ function adaptOpenCodeEvent(hookEventType, hookData = {}) {
339
+ const eventTypeMap = {
340
+ 'tool.execute.before': 'tool.before',
341
+ 'tool.execute.after': 'tool.after',
342
+ 'session.idle': 'session.idle',
343
+ 'session.created': 'session.start',
344
+ 'session.error': 'error',
345
+ 'session.status': 'session.status',
346
+ 'session.updated': 'session.status',
347
+ 'session.diff': 'session.status',
348
+ 'message.updated': 'session.status',
349
+ 'message.part.updated': 'session.status',
350
+ 'message.removed': 'session.status',
351
+ 'message.part.removed': 'session.status',
352
+ 'lsp.client.diagnostics': 'session.status',
353
+ 'lsp.updated': 'session.status',
354
+ 'permission.asked': 'permission',
355
+ 'permission.replied': 'session.status',
356
+ 'question.asked': 'question',
357
+ 'question.replied': 'session.status',
358
+ 'question.rejected': 'session.status',
359
+ 'server.instance.disposed': 'session.status',
360
+ 'server.connected': 'session.status',
361
+ 'todo.updated': 'session.status',
362
+ 'file.edited': 'session.status',
363
+ 'file.watcher.updated': 'session.status',
364
+ 'installation.updated': 'session.status',
365
+ 'command.executed': 'session.status',
366
+ 'shell.env': 'session.status',
367
+ };
368
+
369
+ const eventType = eventTypeMap[hookEventType] || 'unknown';
370
+ const data = {
371
+ tool: hookData.tool || 'unknown',
372
+ toolInput: hookData.toolInput || {},
373
+ command: hookData.command || '',
374
+ filePath: hookData.filePath || '',
375
+ files: hookData.files || [],
376
+ message: hookData.message || hookData.error || '',
377
+ questions: hookData.questions || [],
378
+ };
379
+
380
+ return createCommonEvent('opencode', eventType, data);
381
+ }
382
+
383
+ // ============================================
384
+ // SECTION 5: Session State Management
385
+ // ============================================
386
+
387
+ function ensureStateDir() {
388
+ if (!fs.existsSync(STATE_DIR)) {
389
+ fs.mkdirSync(STATE_DIR, { recursive: true });
390
+ }
391
+ }
392
+
393
+ function getProjectName() {
394
+ return path.basename(process.cwd()) || 'unknown';
395
+ }
396
+
397
+ function getSessionIdentifier() {
398
+ const projectName = getProjectName();
399
+ const runtime = RUNTIME;
400
+ const pid = process.pid;
401
+ return `${projectName}_${runtime}_${pid}`;
402
+ }
403
+
404
+ function getSessionState() {
405
+ ensureStateDir();
406
+ const sessionId = getSessionIdentifier();
407
+ const stateFile = path.join(STATE_DIR, `session_state_${sessionId}.json`);
408
+
409
+ try {
410
+ if (fs.existsSync(stateFile)) {
411
+ const data = fs.readFileSync(stateFile, 'utf8');
412
+ return JSON.parse(data);
413
+ }
414
+ } catch (error) {
415
+ debugLog(`Could not read session state: ${error.message}`);
416
+ }
417
+
418
+ const projectName = getProjectName();
419
+ return {
420
+ lastStopTime: null,
421
+ lastActivityTime: Date.now(),
422
+ sessionId: sessionId,
423
+ pendingCompletion: false,
424
+ project: projectName,
425
+ recentTools: [],
426
+ recentFiles: [],
427
+ totalToolUses: 0,
428
+ };
429
+ }
430
+
431
+ function saveSessionState(state) {
432
+ ensureStateDir();
433
+ const sessionId = getSessionIdentifier();
434
+ const stateFile = path.join(STATE_DIR, `session_state_${sessionId}.json`);
435
+
436
+ try {
437
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
438
+ } catch (error) {
439
+ debugLog(`Could not save session state: ${error.message}`);
440
+ }
441
+ }
442
+
443
+ // ============================================
444
+ // SECTION 6: Event Processor (Source-Agnostic)
445
+ // ============================================
446
+
447
+ /**
448
+ * Process common events - NO source-specific logic here
449
+ */
450
+ async function processEvent(event) {
451
+ debugLog(`Processing event: ${event.eventType} from ${event.source}`);
452
+
453
+ switch (event.eventType) {
454
+ case 'tool.before':
455
+ handleToolStart(event);
456
+ break;
457
+
458
+ case 'tool.after':
459
+ handleToolEnd(event);
460
+ break;
461
+
462
+ case 'session.idle':
463
+ handleSessionIdle(event);
464
+ break;
465
+
466
+ case 'session.start':
467
+ handleSessionStart(event);
468
+ break;
469
+
470
+ case 'permission':
471
+ await handlePermission(event);
472
+ break;
473
+
474
+ case 'permission_notification':
475
+ // Fire-and-forget: a Notification event with permission_prompt type.
476
+ // Does NOT block or poll — just informs the user that a permission dialog is open.
477
+ handlePermissionNotification(event);
478
+ break;
479
+
480
+ case 'question':
481
+ handleQuestion(event);
482
+ break;
483
+
484
+ case 'idle_input':
485
+ handleIdleInput(event);
486
+ break;
487
+
488
+ case 'intervention':
489
+ handleIntervention(event);
490
+ break;
491
+
492
+ case 'error':
493
+ handleError(event);
494
+ break;
495
+
496
+ case 'check.completion':
497
+ handleCheckCompletion(event);
498
+ break;
499
+
500
+ case 'session.end':
501
+ handleSessionEnd(event);
502
+ break;
503
+
504
+ case 'subagent.start':
505
+ handleSubagentStart(event);
506
+ break;
507
+
508
+ case 'subagent.stop':
509
+ handleSubagentStop(event);
510
+ break;
511
+
512
+ case 'user.prompt':
513
+ handleUserPrompt(event);
514
+ break;
515
+
516
+ case 'teammate.idle':
517
+ handleTeammateIdle(event);
518
+ break;
519
+
520
+ case 'task.completed':
521
+ handleTaskCompleted(event);
522
+ break;
523
+
524
+ case 'context.compact':
525
+ debugLog('Context compact event - tracking only');
526
+ break;
527
+
528
+ case 'session.status':
529
+ // CRITICAL: session.status is NOT real activity (internal updates)
530
+ debugLog('session.status event - ignoring (not real activity)');
531
+ break;
532
+
533
+ default:
534
+ debugLog(`Ignoring ${event.eventType} event (not relevant)`);
535
+ }
536
+ }
537
+
538
+ // ============================================
539
+ // SECTION 7: Event Handlers
540
+ // ============================================
541
+
542
+ function handleToolStart(event) {
543
+ const state = getSessionState();
544
+ const now = Date.now();
545
+
546
+ debugLog(`Tool starting: ${event.data.tool || 'unknown'}`);
547
+
548
+ state.lastActivityTime = now;
549
+ state.pendingCompletion = false;
550
+
551
+ if (!state.recentTools) state.recentTools = [];
552
+ if (!state.totalToolUses) state.totalToolUses = 0;
553
+
554
+ state.recentTools.unshift(event.data.tool || 'unknown');
555
+ state.recentTools = state.recentTools.slice(0, 10);
556
+ state.totalToolUses++;
557
+
558
+ saveSessionState(state);
559
+
560
+ cancelCompletionTimer(event.projectName);
561
+ debugLog(`Activity detected, completion timer cancelled (${state.totalToolUses} tools total)`);
562
+ }
563
+
564
+ function handleToolEnd(event) {
565
+ const state = getSessionState();
566
+ state.lastActivityTime = Date.now();
567
+ saveSessionState(state);
568
+ debugLog(`Tool complete: ${event.data.tool || 'unknown'}`);
569
+ }
570
+
571
+ function handleSessionIdle(event) {
572
+ const state = getSessionState();
573
+ const now = Date.now();
574
+
575
+ debugLog(`Session idle detected - starting ${COMPLETION_TIMEOUT / 1000}s completion timer`);
576
+
577
+ state.lastStopTime = now;
578
+ state.pendingCompletion = true;
579
+ saveSessionState(state);
580
+
581
+ scheduleCompletionTimer(event.projectName);
582
+ }
583
+
584
+ function handleSessionStart(event) {
585
+ const state = getSessionState();
586
+ state.sessionId = Date.now().toString();
587
+ state.lastActivityTime = Date.now();
588
+ state.pendingCompletion = false;
589
+ saveSessionState(state);
590
+ cancelCompletionTimer(event.projectName);
591
+ debugLog(`New session started: ${state.sessionId}`);
592
+ }
593
+
594
+ /**
595
+ * Handle permission events (agent needs user to approve a tool)
596
+ *
597
+ * Builds a rich notification with tool name + details when available,
598
+ * falls back to the message text when tool details aren't available.
599
+ * Content is identical between Claude Code and OpenCode.
600
+ */
601
+ async function handlePermission(event) {
602
+ const d = event.data;
603
+ debugLog(`Permission event: tool=${d.tool}, message=${d.message}`);
604
+
605
+ const { title, body } = buildPermissionNotification(event);
606
+
607
+ // Check if WebSocket clients are connected — if so, the events channel
608
+ // will broadcast the permission-requested event and we skip the push notification
609
+ const wsActive = await hasWebSocketClients();
610
+
611
+ // For Claude Code PermissionRequest: block and poll for iPhone response
612
+ if (IS_CLAUDE_CODE && event.source === 'claude-code') {
613
+ const requestId = Math.random().toString(36).substring(2, 15);
614
+ debugLog(`Starting bidirectional permission flow (requestId: ${requestId})`);
615
+
616
+ let result;
617
+ if (wsActive) {
618
+ // WebSocket clients connected — skip push notification, but still register
619
+ // the pending request on the server so polling can find it.
620
+ debugLog(`[Notifier] WebSocket clients connected, skipping push notification`);
621
+ if (IS_CLAUDE_CODE) {
622
+ console.error(`\n=== WEBSOCKET ACTIVE — SKIPPING PUSH [${requestId}] ===`);
623
+ console.error(`Title: ${title}`);
624
+ console.error(`Message: ${body}`);
625
+ console.error(`=== REGISTERING REQUEST & POLLING VIA WEBSOCKET CHANNEL ===\n`);
626
+ }
627
+
628
+ // POST to /api/notify with waitForResponse so the server creates a pending
629
+ // request entry. Without this, GET /api/response returns 404 every time.
630
+ result = await sendNotificationAndPoll(
631
+ title,
632
+ body,
633
+ 'permission',
634
+ event.source,
635
+ requestId,
636
+ d
637
+ );
638
+ } else {
639
+ // No WebSocket clients — send push notification and poll
640
+ result = await sendNotificationAndPoll(
641
+ title,
642
+ body,
643
+ 'permission',
644
+ event.source,
645
+ requestId,
646
+ d
647
+ );
648
+ }
649
+
650
+ if (result && result.decision) {
651
+ const hookResponse = {
652
+ hookSpecificOutput: {
653
+ hookEventName: 'PermissionRequest',
654
+ permissionDecision: result.decision,
655
+ permissionDecisionReason: `User ${result.decision === 'allow' ? 'approved' : 'denied'} via ${wsActive ? 'WebSocket' : 'iPhone notification'}`,
656
+ },
657
+ };
658
+ // Write decision to stdout for Claude Code to read
659
+ process.stdout.write(JSON.stringify(hookResponse));
660
+ debugLog(`Wrote hook decision to stdout: ${result.decision}`);
661
+ } else {
662
+ debugLog('No response received - falling through to local permission dialog');
663
+ // Output nothing → Claude Code shows normal permission dialog
664
+ }
665
+ return;
666
+ }
667
+
668
+ // For OpenCode or non-blocking: fire-and-forget as before
669
+ if (wsActive) {
670
+ debugLog(`[Notifier] WebSocket clients connected, skipping push notification for permission`);
671
+ } else {
672
+ sendNotification(title, body, 'permission', event.source);
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Handle permission_notification events (Notification hook with permission_prompt type).
678
+ *
679
+ * Unlike handlePermission(), this does NOT block or poll for a response.
680
+ * It just sends a fire-and-forget notification to inform the user that
681
+ * Claude Code's local permission dialog is open.
682
+ */
683
+ async function handlePermissionNotification(event) {
684
+ const d = event.data;
685
+ debugLog(`Permission notification event (non-blocking): message=${d.message}`);
686
+
687
+ // Skip push if WebSocket clients are connected (they get the event via the events channel)
688
+ const wsActive = await hasWebSocketClients();
689
+ if (wsActive) {
690
+ debugLog(`[Notifier] WebSocket clients connected, skipping push for permission_notification`);
691
+ return;
692
+ }
693
+
694
+ const { title, body } = buildPermissionNotification(event);
695
+ sendNotification(title, body, 'permission', event.source);
696
+ }
697
+
698
+ /**
699
+ * Handle question/elicitation events (agent is asking user a question)
700
+ *
701
+ * Includes the question text and options when available.
702
+ * Content is identical between Claude Code and OpenCode.
703
+ */
704
+ function handleQuestion(event) {
705
+ const d = event.data;
706
+ debugLog(`Question event: message=${d.message}`);
707
+
708
+ const { title, body } = buildQuestionNotification(event);
709
+
710
+ sendNotification(title, body, 'question', event.source);
711
+ }
712
+
713
+ /**
714
+ * Handle idle input events (agent is idle, waiting for user to type)
715
+ *
716
+ * Lighter notification - just tells user the agent is waiting.
717
+ */
718
+ function handleIdleInput(event) {
719
+ const d = event.data;
720
+ debugLog(`Idle input event: message=${d.message}`);
721
+
722
+ const title = `Waiting for Input`;
723
+ const body = d.message || `Agent is waiting for your input in ${event.projectName}`;
724
+
725
+ sendNotification(title, body, 'idle_input', event.source);
726
+ }
727
+
728
+ /**
729
+ * Handle generic intervention (fallback for unrecognizable events)
730
+ *
731
+ * Sends whatever information is available - never drops a notification.
732
+ */
733
+ function handleIntervention(event) {
734
+ const d = event.data;
735
+ debugLog(`Intervention event: message=${d.message}`);
736
+
737
+ const title = d.title || `Needs Attention`;
738
+ const body = d.message || `Needs your attention in ${event.projectName}`;
739
+
740
+ sendNotification(title, body, 'intervention', event.source);
741
+ }
742
+
743
+ function handleError(event) {
744
+ debugLog(`Error detected: ${event.data.message}`);
745
+ sendNotification(
746
+ `Error in ${event.projectName}`,
747
+ event.data.message || 'An error occurred',
748
+ 'error',
749
+ event.source
750
+ );
751
+ }
752
+
753
+ function handleCheckCompletion(event) {
754
+ debugLog(`Manual completion check requested`);
755
+ checkCompletion(event.projectName, event.source);
756
+ }
757
+
758
+ function handleSessionEnd(event) {
759
+ const state = getSessionState();
760
+ state.pendingCompletion = false;
761
+ saveSessionState(state);
762
+ cancelCompletionTimer(event.projectName);
763
+ debugLog('Session ended - cleaned up state');
764
+ }
765
+
766
+ function handleSubagentStart(event) {
767
+ const state = getSessionState();
768
+ state.lastActivityTime = Date.now();
769
+ state.pendingCompletion = false;
770
+ saveSessionState(state);
771
+ cancelCompletionTimer(event.projectName);
772
+ debugLog(`Subagent started: ${event.data.agentType}`);
773
+ }
774
+
775
+ function handleSubagentStop(event) {
776
+ const state = getSessionState();
777
+ state.lastActivityTime = Date.now();
778
+ saveSessionState(state);
779
+ debugLog(`Subagent stopped: ${event.data.agentType}`);
780
+ }
781
+
782
+ function handleUserPrompt(event) {
783
+ const state = getSessionState();
784
+ state.lastActivityTime = Date.now();
785
+ state.pendingCompletion = false;
786
+ saveSessionState(state);
787
+ cancelCompletionTimer(event.projectName);
788
+ debugLog('User prompt submitted - activity detected');
789
+ }
790
+
791
+ function handleTeammateIdle(event) {
792
+ debugLog(`Teammate idle: ${event.data.teammate}`);
793
+ sendNotification(
794
+ `Teammate Idle`,
795
+ `${event.data.teammate} is idle in ${event.projectName}`,
796
+ 'teammate_idle',
797
+ event.source
798
+ );
799
+ }
800
+
801
+ function handleTaskCompleted(event) {
802
+ debugLog(`Task completed: ${event.data.message}`);
803
+ sendNotification(
804
+ `Task Completed`,
805
+ event.data.message || `A task was completed in ${event.projectName}`,
806
+ 'task_completed',
807
+ event.source
808
+ );
809
+ }
810
+
811
+ // ============================================
812
+ // SECTION 8: Notification Message Builders
813
+ // ============================================
814
+
815
+ /**
816
+ * Build permission notification content.
817
+ * Same structure regardless of source (Claude Code or OpenCode).
818
+ *
819
+ * When we have tool details:
820
+ * Title: "Permission: Bash"
821
+ * Body: "npm test" or "Allow: rm -rf /tmp/build"
822
+ *
823
+ * When we only have a message:
824
+ * Title: "Permission Needed"
825
+ * Body: "Claude needs your permission to use Bash"
826
+ */
827
+ function buildPermissionNotification(event) {
828
+ const d = event.data;
829
+ const toolName = d.tool || '';
830
+ const command = d.command || '';
831
+ const filePath = d.filePath || '';
832
+ const description = d.description || '';
833
+ const message = d.message || '';
834
+
835
+ // Case 1: We know the tool name and have details
836
+ if (toolName && toolName !== 'Unknown' && toolName !== '') {
837
+ const title = `Permission: ${toolName}`;
838
+ let body = '';
839
+
840
+ if (toolName === 'Bash' && command) {
841
+ // For Bash, show the command
842
+ body = command.length > 200 ? command.substring(0, 200) + '...' : command;
843
+ } else if ((toolName === 'Edit' || toolName === 'Write' || toolName === 'Read') && filePath) {
844
+ // For file operations, show the file path
845
+ body = filePath;
846
+ } else if (description) {
847
+ body = description;
848
+ } else if (command) {
849
+ body = command;
850
+ } else if (filePath) {
851
+ body = filePath;
852
+ } else {
853
+ body = `Approve ${toolName} in ${event.projectName}`;
854
+ }
855
+
856
+ return { title, body };
857
+ }
858
+
859
+ // Case 2: We only have a message (e.g., from Notification event)
860
+ if (message) {
861
+ return {
862
+ title: `Permission Needed`,
863
+ body: message,
864
+ };
865
+ }
866
+
867
+ // Case 3: Minimal fallback
868
+ return {
869
+ title: `Permission Needed`,
870
+ body: `Agent needs permission in ${event.projectName}`,
871
+ };
872
+ }
873
+
874
+ /**
875
+ * Build question/elicitation notification content.
876
+ * Same structure regardless of source.
877
+ *
878
+ * Handles two formats:
879
+ * 1. Claude Code: { message: "question text", title: "..." }
880
+ * 2. OpenCode: { questions: [{ header, options: [{ label, description }] }] }
881
+ *
882
+ * Output is always:
883
+ * Title: "Question: <header>" or "Question"
884
+ * Body: "<question text> | Options: A / B / C"
885
+ */
886
+ function buildQuestionNotification(event) {
887
+ const d = event.data;
888
+ const message = d.message || '';
889
+ const title = d.title || '';
890
+ const questions = d.questions || [];
891
+
892
+ // Case 1: OpenCode question.asked with structured questions array
893
+ if (questions.length > 0) {
894
+ const q = questions[0]; // Use first question
895
+ const header = q.header || q.question || '';
896
+ const options = (q.options || []).map((o) => o.label).filter(Boolean);
897
+
898
+ const notifTitle = header ? `Question: ${header}` : 'Question';
899
+ let body = q.question || header || '';
900
+
901
+ if (options.length > 0) {
902
+ body = body ? `${body} | Options: ${options.join(' / ')}` : `Options: ${options.join(' / ')}`;
903
+ }
904
+
905
+ if (!body) {
906
+ body = `Agent is asking a question in ${event.projectName}`;
907
+ }
908
+
909
+ return {
910
+ title: notifTitle,
911
+ body: body.length > 300 ? body.substring(0, 300) + '...' : body,
912
+ };
913
+ }
914
+
915
+ // Case 2: Claude Code notification with message text
916
+ const notifTitle = title && title !== 'Permission needed' ? title : 'Question';
917
+
918
+ if (message) {
919
+ return {
920
+ title: notifTitle,
921
+ body: message.length > 300 ? message.substring(0, 300) + '...' : message,
922
+ };
923
+ }
924
+
925
+ // Case 3: Minimal fallback
926
+ return {
927
+ title: 'Question',
928
+ body: `Agent is asking a question in ${event.projectName}`,
929
+ };
930
+ }
931
+
932
+ // ============================================
933
+ // SECTION 9: Completion Timer Management
934
+ // ============================================
935
+
936
+ function scheduleCompletionTimer(projectName) {
937
+ if (IS_CLAUDE_CODE) {
938
+ // Completion timer cannot work in Claude Code (each hook is a separate process)
939
+ return;
940
+ }
941
+ debugLog(`Scheduling completion check for ${projectName}`);
942
+ cancelCompletionTimer(projectName);
943
+
944
+ const timer = setTimeout(() => {
945
+ debugLog(`Completion timer fired for ${projectName}`);
946
+ checkCompletion(projectName, RUNTIME);
947
+ }, COMPLETION_TIMEOUT);
948
+
949
+ completionTimers.set(projectName, timer);
950
+ debugLog(`Completion timer scheduled (45s)`);
951
+ }
952
+
953
+ function cancelCompletionTimer(projectName) {
954
+ const existingTimer = completionTimers.get(projectName);
955
+ if (existingTimer) {
956
+ clearTimeout(existingTimer);
957
+ completionTimers.delete(projectName);
958
+ debugLog(`Completion timer cancelled for ${projectName}`);
959
+ }
960
+ }
961
+
962
+ function checkCompletion(projectName, source) {
963
+ if (IS_CLAUDE_CODE) {
964
+ return;
965
+ }
966
+ const state = getSessionState();
967
+ const now = Date.now();
968
+
969
+ debugLog(`Checking completion status for ${projectName}`);
970
+ debugLog(` pendingCompletion: ${state.pendingCompletion}`);
971
+ debugLog(` lastStopTime: ${state.lastStopTime}`);
972
+ debugLog(` lastActivityTime: ${state.lastActivityTime}`);
973
+
974
+ if (
975
+ state.pendingCompletion &&
976
+ state.lastStopTime &&
977
+ state.lastActivityTime <= state.lastStopTime &&
978
+ now - state.lastStopTime >= COMPLETION_TIMEOUT
979
+ ) {
980
+ debugLog(`Conditions met - sending completion notification`);
981
+
982
+ const message = createCompletionMessage(state, projectName);
983
+
984
+ sendNotification(`${projectName} Complete`, message, 'completion', source);
985
+
986
+ state.pendingCompletion = false;
987
+ saveSessionState(state);
988
+ } else {
989
+ debugLog(`No completion notification needed`);
990
+ }
991
+ }
992
+
993
+ function createCompletionMessage(state, projectName) {
994
+ const timestamp = new Date().toLocaleTimeString();
995
+ let message = `Session completed in ${projectName} at ${timestamp}`;
996
+
997
+ const totalTools = state.totalToolUses || 0;
998
+ if (totalTools > 0) {
999
+ message += ` | ${totalTools} tools used`;
1000
+
1001
+ if (state.recentTools && state.recentTools.length > 0) {
1002
+ const toolSummary = state.recentTools.slice(0, 3).join(', ');
1003
+ message += ` | Recent: ${toolSummary}`;
1004
+ }
1005
+
1006
+ if (state.recentFiles && state.recentFiles.length > 0) {
1007
+ const fileSummary = state.recentFiles.slice(0, 3).join(', ');
1008
+ message += ` | Files: ${fileSummary}`;
1009
+ }
1010
+ }
1011
+
1012
+ return message;
1013
+ }
1014
+
1015
+ // ============================================
1016
+ // SECTION 10: Notification Service
1017
+ // ============================================
1018
+
1019
+ /**
1020
+ * Send a notification and poll the server for a user response.
1021
+ * Used for PermissionRequest bidirectional flow.
1022
+ *
1023
+ * Returns { decision: 'allow' | 'deny' } or null on timeout.
1024
+ */
1025
+ function sendNotificationAndPoll(title, body, category, source, requestId, eventData) {
1026
+ return new Promise((resolve) => {
1027
+ const timestamp = new Date().toISOString();
1028
+
1029
+ const runtimePrefix = source === 'opencode' ? '[OpenCode]' : '[Claude]';
1030
+ const envPrefix = USE_LOCAL ? '[LOCAL]' : '';
1031
+ const finalTitle = `${runtimePrefix}${envPrefix ? ' ' + envPrefix : ''} ${title}`;
1032
+
1033
+ debugLog(`Sending bidirectional notification: "${finalTitle}" (requestId: ${requestId})`);
1034
+
1035
+ const payload = JSON.stringify({
1036
+ title: finalTitle,
1037
+ message: body,
1038
+ waitForResponse: true,
1039
+ ...(DEVICE_TOKEN && { deviceToken: DEVICE_TOKEN }),
1040
+ data: {
1041
+ category,
1042
+ project: getProjectName(),
1043
+ timestamp,
1044
+ requestId,
1045
+ clientTimestamp: timestamp,
1046
+ source: 'shooter-completion-detector',
1047
+ environment: USE_LOCAL ? 'local' : 'remote',
1048
+ runtime: source,
1049
+ toolName: eventData.tool || '',
1050
+ toolInput: eventData.toolInput || {},
1051
+ sessionId: eventData.sessionId || '',
1052
+ },
1053
+ });
1054
+
1055
+ const options = {
1056
+ method: 'POST',
1057
+ headers: {
1058
+ 'Content-Type': 'application/json',
1059
+ Authorization: `Bearer ${AUTH_KEY}`,
1060
+ 'Content-Length': Buffer.byteLength(payload),
1061
+ 'User-Agent': `Shooter-Notifier/3.0 ${source}`,
1062
+ },
1063
+ };
1064
+
1065
+ // Step 1: Send the notification
1066
+ const protocol = API_URL.startsWith('https') ? https : http;
1067
+ const req = protocol.request(API_URL, options, (res) => {
1068
+ let responseData = '';
1069
+ res.on('data', (chunk) => (responseData += chunk));
1070
+ res.on('end', () => {
1071
+ if (IS_CLAUDE_CODE) {
1072
+ console.error(`\n=== BIDIRECTIONAL NOTIFICATION SENT [${requestId}] ===`);
1073
+ console.error(`Title: ${finalTitle}`);
1074
+ console.error(`Message: ${body}`);
1075
+ console.error(`Status: ${res.statusCode}`);
1076
+ console.error(`=== NOW POLLING FOR RESPONSE ===\n`);
1077
+ }
1078
+
1079
+ if (res.statusCode !== 200) {
1080
+ debugLog(`Notification send failed: ${res.statusCode} - falling through to local dialog`);
1081
+ resolve(null);
1082
+ return;
1083
+ }
1084
+
1085
+ // Step 2: Start polling for user response
1086
+ startPolling(requestId, resolve);
1087
+ });
1088
+ });
1089
+
1090
+ req.on('error', (error) => {
1091
+ debugLog(`Notification request error: ${error.message} - falling through to local dialog`);
1092
+ resolve(null);
1093
+ });
1094
+
1095
+ req.setTimeout(10000, () => {
1096
+ req.destroy(new Error('Request timeout'));
1097
+ });
1098
+
1099
+ req.write(payload);
1100
+ req.end();
1101
+ });
1102
+ }
1103
+
1104
+ /**
1105
+ * Poll GET /api/response?requestId=xxx every POLL_INTERVAL until decided or timeout.
1106
+ */
1107
+ function startPolling(requestId, resolve) {
1108
+ const startTime = Date.now();
1109
+ let resolved = false;
1110
+
1111
+ const overallTimeout = setTimeout(() => {
1112
+ if (!resolved) {
1113
+ resolved = true;
1114
+ clearInterval(pollTimer);
1115
+ debugLog(
1116
+ `Permission polling timed out after ${PERMISSION_TIMEOUT / 1000}s - falling through to local dialog`
1117
+ );
1118
+ if (IS_CLAUDE_CODE) {
1119
+ console.error(`\n=== PERMISSION TIMEOUT [${requestId}] ===`);
1120
+ console.error(
1121
+ `No response after ${PERMISSION_TIMEOUT / 1000}s - falling through to local dialog`
1122
+ );
1123
+ console.error(`=== END ===\n`);
1124
+ }
1125
+ resolve(null);
1126
+ }
1127
+ }, PERMISSION_TIMEOUT);
1128
+
1129
+ const pollTimer = setInterval(() => {
1130
+ if (resolved) {
1131
+ clearInterval(pollTimer);
1132
+ return;
1133
+ }
1134
+
1135
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
1136
+ debugLog(`Polling for response (${elapsed}s elapsed)...`);
1137
+
1138
+ const pollUrl = `${RESPONSE_URL}?requestId=${encodeURIComponent(requestId)}`;
1139
+ const pollOptions = {
1140
+ method: 'GET',
1141
+ headers: {
1142
+ Authorization: `Bearer ${AUTH_KEY}`,
1143
+ },
1144
+ };
1145
+
1146
+ const protocol = RESPONSE_URL.startsWith('https') ? https : http;
1147
+ const pollReq = protocol.request(pollUrl, pollOptions, (res) => {
1148
+ let data = '';
1149
+ res.on('data', (chunk) => (data += chunk));
1150
+ res.on('end', () => {
1151
+ if (resolved) return;
1152
+
1153
+ try {
1154
+ const result = JSON.parse(data);
1155
+ if (result.status === 'decided' && result.decision) {
1156
+ resolved = true;
1157
+ clearInterval(pollTimer);
1158
+ clearTimeout(overallTimeout);
1159
+
1160
+ debugLog(`Decision received: ${result.decision} (after ${elapsed}s)`);
1161
+ if (IS_CLAUDE_CODE) {
1162
+ console.error(`\n=== DECISION RECEIVED [${requestId}] ===`);
1163
+ console.error(`Decision: ${result.decision}`);
1164
+ console.error(`Elapsed: ${elapsed}s`);
1165
+ console.error(`=== END ===\n`);
1166
+ }
1167
+
1168
+ resolve({ decision: result.decision });
1169
+ }
1170
+ // status === 'pending' → keep polling
1171
+ } catch (e) {
1172
+ debugLog(`Poll parse error: ${e.message}`);
1173
+ }
1174
+ });
1175
+ });
1176
+
1177
+ pollReq.on('error', (error) => {
1178
+ debugLog(`Poll request error: ${error.message}`);
1179
+ // Don't resolve on poll error — keep trying until timeout
1180
+ });
1181
+
1182
+ pollReq.setTimeout(10000, () => {
1183
+ pollReq.destroy(new Error('Request timeout'));
1184
+ });
1185
+
1186
+ pollReq.end();
1187
+ }, POLL_INTERVAL);
1188
+ }
1189
+
1190
+ function sendNotification(title, body, category = 'completion', source = RUNTIME) {
1191
+ const requestId = Math.random().toString(36).substring(2, 15);
1192
+ const timestamp = new Date().toISOString();
1193
+
1194
+ // Prefix: [Claude] or [OpenCode], optionally [LOCAL]
1195
+ const runtimePrefix = source === 'opencode' ? '[OpenCode]' : '[Claude]';
1196
+ const envPrefix = USE_LOCAL ? '[LOCAL]' : '';
1197
+ const finalTitle = `${runtimePrefix}${envPrefix ? ' ' + envPrefix : ''} ${title}`;
1198
+
1199
+ debugLog(`Sending notification: "${finalTitle}"`);
1200
+ debugLog(` Message: "${body.substring(0, 100)}..."`);
1201
+ debugLog(` Category: ${category}, RequestID: ${requestId}`);
1202
+
1203
+ const payload = JSON.stringify({
1204
+ title: finalTitle,
1205
+ message: body,
1206
+ ...(DEVICE_TOKEN && { deviceToken: DEVICE_TOKEN }),
1207
+ data: {
1208
+ category,
1209
+ project: getProjectName(),
1210
+ timestamp,
1211
+ requestId,
1212
+ clientTimestamp: timestamp,
1213
+ source: 'shooter-completion-detector',
1214
+ environment: USE_LOCAL ? 'local' : 'remote',
1215
+ runtime: source,
1216
+ },
1217
+ });
1218
+
1219
+ const options = {
1220
+ method: 'POST',
1221
+ headers: {
1222
+ 'Content-Type': 'application/json',
1223
+ Authorization: `Bearer ${AUTH_KEY}`,
1224
+ 'Content-Length': Buffer.byteLength(payload),
1225
+ 'User-Agent': `Shooter-Notifier/3.0 ${source}`,
1226
+ },
1227
+ };
1228
+
1229
+ const protocol = API_URL.startsWith('https') ? https : http;
1230
+ const req = protocol.request(API_URL, options, (res) => {
1231
+ let responseData = '';
1232
+ res.on('data', (chunk) => (responseData += chunk));
1233
+ res.on('end', () => {
1234
+ if (IS_CLAUDE_CODE) {
1235
+ console.error(`\n=== NOTIFICATION SENT [${requestId}] @ ${timestamp} ===`);
1236
+ console.error(`Project: ${getProjectName()}`);
1237
+ console.error(`Category: ${category}`);
1238
+ console.error(`Title: ${finalTitle}`);
1239
+ console.error(`Message: ${body}`);
1240
+ console.error(`API URL: ${API_URL} (${USE_LOCAL ? 'LOCAL' : 'REMOTE'})`);
1241
+ console.error(`Status Code: ${res.statusCode}`);
1242
+ console.error(`Response: ${responseData}`);
1243
+ console.error(`=== END NOTIFICATION ===\n`);
1244
+ }
1245
+
1246
+ if (res.statusCode !== 200) {
1247
+ debugLog(`HTTP ERROR: ${res.statusCode} ${responseData}`);
1248
+ } else {
1249
+ debugLog(`Notification sent successfully`);
1250
+ }
1251
+ });
1252
+ });
1253
+
1254
+ req.on('error', (error) => {
1255
+ debugLog(`Request error: ${error.message}`);
1256
+ if (IS_CLAUDE_CODE) {
1257
+ console.error('Notification request error:', error.message);
1258
+ }
1259
+ });
1260
+
1261
+ req.setTimeout(10000, () => {
1262
+ req.destroy(new Error('Request timeout'));
1263
+ });
1264
+
1265
+ req.write(payload);
1266
+ req.end();
1267
+ }
1268
+
1269
+ // ============================================
1270
+ // Utility: Debug Logging
1271
+ // ============================================
1272
+
1273
+ function debugLog(msg) {
1274
+ if (!DEBUG_ENABLED) return;
1275
+
1276
+ try {
1277
+ const timestamp = new Date().toISOString();
1278
+ const logFile = IS_CLAUDE_CODE ? DEBUG_LOG_FILE : '/tmp/shooter-opencode-debug.log';
1279
+ fs.writeFileSync(logFile, `[${timestamp}] ${msg}\n`, { flag: 'a' });
1280
+ } catch (e) {
1281
+ // Silent fail
1282
+ }
1283
+ }
1284
+
1285
+ // ============================================
1286
+ // SECTION 11: Entry Points
1287
+ // ============================================
1288
+
1289
+ // ============================================
1290
+ // 11A: Claude Code CLI Entry Point
1291
+ // ============================================
1292
+
1293
+ async function claudeCodeMain() {
1294
+ // Validate required environment variables (only in Claude Code CLI mode)
1295
+ if (!USE_LOCAL && !REMOTE_BASE_URL) {
1296
+ console.error(
1297
+ 'SHOOTER_API_URL environment variable is required when SHOOTER_USE_LOCAL is not true'
1298
+ );
1299
+ process.exit(1);
1300
+ }
1301
+
1302
+ const cliArg = process.argv[2] || 'Unknown';
1303
+
1304
+ debugLog(`Shooter Notifier CLI invoked: ${cliArg}`);
1305
+ debugLog(` Runtime: ${RUNTIME}`);
1306
+ debugLog(` Environment: ${USE_LOCAL ? 'LOCAL' : 'REMOTE'}`);
1307
+ debugLog(` Session ID: ${getSessionIdentifier()}`);
1308
+
1309
+ // Read stdin JSON (Claude Code passes event data here)
1310
+ const stdinData = await readStdin();
1311
+ if (stdinData) {
1312
+ debugLog(` Stdin data received: ${JSON.stringify(stdinData).substring(0, 500)}`);
1313
+ } else {
1314
+ debugLog(` No stdin data (legacy mode or TTY)`);
1315
+ }
1316
+
1317
+ // Adapt CLI event + stdin data to common format
1318
+ const event = adaptClaudeCodeEvent(cliArg, stdinData);
1319
+
1320
+ // Process the event (await for blocking handlers like PermissionRequest)
1321
+ await processEvent(event);
1322
+ }
1323
+
1324
+ // ============================================
1325
+ // 11B: OpenCode Plugin Entry Point
1326
+ // ============================================
1327
+
1328
+ const OpenCodePlugin = async (ctx) => {
1329
+ debugLog('Shooter Notifier plugin loaded');
1330
+ debugLog(` Runtime: ${RUNTIME}`);
1331
+ debugLog(` Environment: ${USE_LOCAL ? 'LOCAL' : 'REMOTE'}`);
1332
+ debugLog(` API URL: ${API_URL}`);
1333
+ debugLog(` Session ID: ${getSessionIdentifier()}`);
1334
+
1335
+ // Extract project name from context
1336
+ let projectName = getProjectName();
1337
+ if (ctx?.project && typeof ctx.project === 'string') {
1338
+ projectName = ctx.project;
1339
+ } else if (ctx?.directory && typeof ctx.directory === 'string') {
1340
+ projectName = path.basename(ctx.directory);
1341
+ } else if (ctx?.project?.name && typeof ctx.project.name === 'string') {
1342
+ projectName = ctx.project.name;
1343
+ }
1344
+
1345
+ debugLog(`Project name: ${projectName}`);
1346
+
1347
+ return {
1348
+ // Generic event handler - catches ALL OpenCode events
1349
+ event: async ({ event }) => {
1350
+ if (!event || !event.type) return;
1351
+
1352
+ // Log ALL raw event types so we can discover what OpenCode sends
1353
+ debugLog(`[RAW EVENT] type=${event.type} keys=${Object.keys(event).join(',')}`);
1354
+ if (event.properties) {
1355
+ debugLog(`[RAW EVENT] properties=${JSON.stringify(event.properties).substring(0, 300)}`);
1356
+ }
1357
+
1358
+ // Extract properties from OpenCode event
1359
+ const props = event.properties || {};
1360
+
1361
+ const commonEvent = adaptOpenCodeEvent(event.type, {
1362
+ tool: event.tool || props.tool,
1363
+ toolInput: event.toolInput || event.args || {},
1364
+ command: event.command || event.args?.command || '',
1365
+ filePath: event.filePath || event.args?.filePath || '',
1366
+ files: event.files,
1367
+ message: event.message || event.error || props.message || '',
1368
+ questions: props.questions || [],
1369
+ });
1370
+
1371
+ processEvent(commonEvent);
1372
+ },
1373
+
1374
+ // Specific hook: Before tool execution
1375
+ 'tool.execute.before': async (input, output) => {
1376
+ const commonEvent = adaptOpenCodeEvent('tool.execute.before', {
1377
+ tool: input?.tool || 'unknown',
1378
+ toolInput: output?.args || {},
1379
+ command: output?.args?.command || '',
1380
+ filePath: output?.args?.filePath || '',
1381
+ });
1382
+ processEvent(commonEvent);
1383
+ },
1384
+
1385
+ // Specific hook: After tool execution
1386
+ 'tool.execute.after': async (input, output) => {
1387
+ const commonEvent = adaptOpenCodeEvent('tool.execute.after', {
1388
+ tool: input?.tool || 'unknown',
1389
+ });
1390
+ processEvent(commonEvent);
1391
+ },
1392
+
1393
+ // Specific hook: Permission asked (agent needs user approval)
1394
+ 'permission.asked': async (input, output) => {
1395
+ debugLog(`OpenCode permission.asked: tool=${input?.tool}`);
1396
+ const commonEvent = adaptOpenCodeEvent('permission.asked', {
1397
+ tool: input?.tool || 'unknown',
1398
+ toolInput: input?.args || output?.args || {},
1399
+ command: input?.args?.command || output?.args?.command || '',
1400
+ filePath: input?.args?.filePath || output?.args?.filePath || '',
1401
+ message: input?.message || '',
1402
+ });
1403
+ processEvent(commonEvent);
1404
+ },
1405
+ };
1406
+ };
1407
+
1408
+ // ============================================
1409
+ // Exports and Main Execution
1410
+ // ============================================
1411
+
1412
+ // Export for OpenCode plugin system
1413
+ if (typeof module !== 'undefined' && module.exports) {
1414
+ module.exports = OpenCodePlugin;
1415
+ module.exports.OpenCodePlugin = OpenCodePlugin;
1416
+ module.exports.ShooterNotifier = OpenCodePlugin;
1417
+ }
1418
+
1419
+ // Run main() when called directly from CLI (Claude Code)
1420
+ if (IS_CLAUDE_CODE) {
1421
+ // Handle process cleanup (Claude Code CLI only)
1422
+ process.on('SIGINT', () => {
1423
+ process.exit(0);
1424
+ });
1425
+
1426
+ process.on('SIGTERM', () => {
1427
+ process.exit(0);
1428
+ });
1429
+
1430
+ claudeCodeMain();
1431
+ }