@juspay/shooter 1.20.0 → 1.22.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 (275) hide show
  1. package/.claude/hooks/notifier.cjs +94 -1
  2. package/build/client/_app/immutable/assets/2.JWRrnR-w.css +1 -0
  3. package/build/client/_app/immutable/assets/2.JWRrnR-w.css.br +0 -0
  4. package/build/client/_app/immutable/assets/2.JWRrnR-w.css.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{ZS5XYDx_.js → B1bOvemT.js} +1 -1
  6. package/build/client/_app/immutable/chunks/B1bOvemT.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/{ZS5XYDx_.js.gz → B1bOvemT.js.gz} +0 -0
  8. package/build/client/_app/immutable/chunks/BfbPKMXz.js +3 -0
  9. package/build/client/_app/immutable/chunks/BfbPKMXz.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/BfbPKMXz.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/C4Hns_Wl.js +1 -0
  12. package/build/client/_app/immutable/chunks/C4Hns_Wl.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/C4Hns_Wl.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/C87ZRWX0.js +1 -0
  15. package/build/client/_app/immutable/chunks/C87ZRWX0.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/C87ZRWX0.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/{BvmdJful.js → CJulw9ux.js} +1 -1
  18. package/build/client/_app/immutable/chunks/CJulw9ux.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/CJulw9ux.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/{DT4H19pV.js → CZg4kn4E.js} +1 -1
  21. package/build/client/_app/immutable/chunks/CZg4kn4E.js.br +0 -0
  22. package/build/client/_app/immutable/chunks/CZg4kn4E.js.gz +0 -0
  23. package/build/client/_app/immutable/chunks/{DIZ3Qst5.js → DhK7PwI_.js} +1 -1
  24. package/build/client/_app/immutable/chunks/DhK7PwI_.js.br +0 -0
  25. package/build/client/_app/immutable/chunks/{DIZ3Qst5.js.gz → DhK7PwI_.js.gz} +0 -0
  26. package/build/client/_app/immutable/chunks/{ClIPTXf3.js → DomZZqvG.js} +1 -1
  27. package/build/client/_app/immutable/chunks/DomZZqvG.js.br +0 -0
  28. package/build/client/_app/immutable/chunks/DomZZqvG.js.gz +0 -0
  29. package/build/client/_app/immutable/chunks/{BB2l8o4X.js → i5iZvmIH.js} +1 -1
  30. package/build/client/_app/immutable/chunks/i5iZvmIH.js.br +0 -0
  31. package/build/client/_app/immutable/chunks/i5iZvmIH.js.gz +0 -0
  32. package/build/client/_app/immutable/entry/{app.Bd-DfeJi.js → app.CTqz33nP.js} +2 -2
  33. package/build/client/_app/immutable/entry/app.CTqz33nP.js.br +0 -0
  34. package/build/client/_app/immutable/entry/app.CTqz33nP.js.gz +0 -0
  35. package/build/client/_app/immutable/entry/start.Dj-Kvgwo.js +1 -0
  36. package/build/client/_app/immutable/entry/start.Dj-Kvgwo.js.br +2 -0
  37. package/build/client/_app/immutable/entry/start.Dj-Kvgwo.js.gz +0 -0
  38. package/build/client/_app/immutable/nodes/0.Qn7Ktiht.js +10 -0
  39. package/build/client/_app/immutable/nodes/0.Qn7Ktiht.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/0.Qn7Ktiht.js.gz +0 -0
  41. package/build/client/_app/immutable/nodes/{1.DT4dq6Ay.js → 1.BxWOfNlo.js} +1 -1
  42. package/build/client/_app/immutable/nodes/1.BxWOfNlo.js.br +0 -0
  43. package/build/client/_app/immutable/nodes/1.BxWOfNlo.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/{10.CF7RGXpe.js → 10.BGPYD1s1.js} +1 -1
  45. package/build/client/_app/immutable/nodes/10.BGPYD1s1.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/10.BGPYD1s1.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/{11.BV_G7yLI.js → 11.BxY1PUjC.js} +1 -1
  48. package/build/client/_app/immutable/nodes/11.BxY1PUjC.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/11.BxY1PUjC.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/2.Bc2qALkX.js +23 -0
  51. package/build/client/_app/immutable/nodes/2.Bc2qALkX.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/2.Bc2qALkX.js.gz +0 -0
  53. package/build/client/_app/immutable/nodes/{3.0MMe3oxR.js → 3.N2-A8noI.js} +1 -1
  54. package/build/client/_app/immutable/nodes/3.N2-A8noI.js.br +0 -0
  55. package/build/client/_app/immutable/nodes/3.N2-A8noI.js.gz +0 -0
  56. package/build/client/_app/immutable/nodes/{4.CBX9A3ka.js → 4.BFYS2g9C.js} +3 -3
  57. package/build/client/_app/immutable/nodes/4.BFYS2g9C.js.br +0 -0
  58. package/build/client/_app/immutable/nodes/4.BFYS2g9C.js.gz +0 -0
  59. package/build/client/_app/immutable/nodes/{5.DIVKuZc9.js → 5.DziEu9rx.js} +1 -1
  60. package/build/client/_app/immutable/nodes/5.DziEu9rx.js.br +0 -0
  61. package/build/client/_app/immutable/nodes/5.DziEu9rx.js.gz +0 -0
  62. package/build/client/_app/immutable/nodes/{6.ComiWlV6.js → 6.BWF9Qx6F.js} +1 -1
  63. package/build/client/_app/immutable/nodes/6.BWF9Qx6F.js.br +0 -0
  64. package/build/client/_app/immutable/nodes/6.BWF9Qx6F.js.gz +0 -0
  65. package/build/client/_app/immutable/nodes/{7.vkPx1kVP.js → 7.DHuDIdpz.js} +1 -1
  66. package/build/client/_app/immutable/nodes/7.DHuDIdpz.js.br +0 -0
  67. package/build/client/_app/immutable/nodes/7.DHuDIdpz.js.gz +0 -0
  68. package/build/client/_app/immutable/nodes/{8.Bmr3sWbS.js → 8.D0Ijt9Vv.js} +1 -1
  69. package/build/client/_app/immutable/nodes/8.D0Ijt9Vv.js.br +0 -0
  70. package/build/client/_app/immutable/nodes/8.D0Ijt9Vv.js.gz +0 -0
  71. package/build/client/_app/immutable/nodes/{9.CAJucyeI.js → 9.2Piwo35J.js} +1 -1
  72. package/build/client/_app/immutable/nodes/9.2Piwo35J.js.br +0 -0
  73. package/build/client/_app/immutable/nodes/9.2Piwo35J.js.gz +0 -0
  74. package/build/client/_app/version.json +1 -1
  75. package/build/client/_app/version.json.br +0 -0
  76. package/build/client/_app/version.json.gz +0 -0
  77. package/build/pty-holder.cjs +6 -0
  78. package/build/server/chunks/{0-DDGB6CRT.js → 0-CVGsyVKN.js} +4 -2
  79. package/build/server/chunks/0-CVGsyVKN.js.map +1 -0
  80. package/build/server/chunks/{1-DEjonQXD.js → 1-BAlAsKdp.js} +2 -2
  81. package/build/server/chunks/{1-DEjonQXD.js.map → 1-BAlAsKdp.js.map} +1 -1
  82. package/build/server/chunks/{10-BK1kiiiw.js → 10-BUCX7Aqz.js} +2 -2
  83. package/build/server/chunks/{10-BK1kiiiw.js.map → 10-BUCX7Aqz.js.map} +1 -1
  84. package/build/server/chunks/{11-CJPjkEF3.js → 11-DHPvc2yA.js} +2 -2
  85. package/build/server/chunks/{11-CJPjkEF3.js.map → 11-DHPvc2yA.js.map} +1 -1
  86. package/build/server/chunks/{2-RLnhlWh5.js → 2-DLOMdCHW.js} +4 -4
  87. package/build/server/chunks/{2-RLnhlWh5.js.map → 2-DLOMdCHW.js.map} +1 -1
  88. package/build/server/chunks/{3-Dd4pJBqZ.js → 3-DCf69LYo.js} +2 -2
  89. package/build/server/chunks/{3-Dd4pJBqZ.js.map → 3-DCf69LYo.js.map} +1 -1
  90. package/build/server/chunks/{4-Bb5VFhsO.js → 4-D92KnTmb.js} +3 -3
  91. package/build/server/chunks/{4-Bb5VFhsO.js.map → 4-D92KnTmb.js.map} +1 -1
  92. package/build/server/chunks/{5-oNoWuIsn.js → 5-D-Uv1voC.js} +2 -2
  93. package/build/server/chunks/{5-oNoWuIsn.js.map → 5-D-Uv1voC.js.map} +1 -1
  94. package/build/server/chunks/{6-DdRMnKNa.js → 6-DUrC2Naz.js} +2 -2
  95. package/build/server/chunks/{6-DdRMnKNa.js.map → 6-DUrC2Naz.js.map} +1 -1
  96. package/build/server/chunks/{7-vLOMMetm.js → 7-TXwjMHt2.js} +2 -2
  97. package/build/server/chunks/{7-vLOMMetm.js.map → 7-TXwjMHt2.js.map} +1 -1
  98. package/build/server/chunks/{8-rJyiQLFs.js → 8-D2X_jBsT.js} +2 -2
  99. package/build/server/chunks/{8-rJyiQLFs.js.map → 8-D2X_jBsT.js.map} +1 -1
  100. package/build/server/chunks/{9-CVSNNYED.js → 9-DK0hH5Xa.js} +2 -2
  101. package/build/server/chunks/{9-CVSNNYED.js.map → 9-DK0hH5Xa.js.map} +1 -1
  102. package/build/server/chunks/Banner-BgaAs1rs.js.map +1 -1
  103. package/build/server/chunks/Button-D0hZ7JYt.js.map +1 -1
  104. package/build/server/chunks/Icon-D0GBnDcs.js.map +1 -1
  105. package/build/server/chunks/Input-OmIiydSx.js.map +1 -1
  106. package/build/server/chunks/Pill-4xJ-VhAA.js.map +1 -1
  107. package/build/server/chunks/Shimmer-Dw2uvTC1.js.map +1 -1
  108. package/build/server/chunks/_error.svelte-CZnkxeLr.js.map +1 -1
  109. package/build/server/chunks/_layout.svelte-DfgNGGiM.js.map +1 -1
  110. package/build/server/chunks/_page.svelte-8OFzwdNA.js +758 -0
  111. package/build/server/chunks/_page.svelte-8OFzwdNA.js.map +1 -0
  112. package/build/server/chunks/_page.svelte-BTlfUsBp.js.map +1 -1
  113. package/build/server/chunks/_page.svelte-C7B0qdrC.js.map +1 -1
  114. package/build/server/chunks/{_page.svelte-BLo2v_8E.js → _page.svelte-Gv9p8nlS.js} +3 -4
  115. package/build/server/chunks/_page.svelte-Gv9p8nlS.js.map +1 -0
  116. package/build/server/chunks/_page.svelte-dabsQl9c.js.map +1 -1
  117. package/build/server/chunks/{_server.ts-bk_EeAdY.js → _server.ts-05JJOdcX.js} +20 -14
  118. package/build/server/chunks/_server.ts-05JJOdcX.js.map +1 -0
  119. package/build/server/chunks/{_server.ts-DEx9-epI.js → _server.ts-B54Pvhgc.js} +4 -3
  120. package/build/server/chunks/_server.ts-B54Pvhgc.js.map +1 -0
  121. package/build/server/chunks/_server.ts-BB46Fbqn.js +59 -0
  122. package/build/server/chunks/_server.ts-BB46Fbqn.js.map +1 -0
  123. package/build/server/chunks/{_server.ts-BRAzC6W1.js → _server.ts-BCljU9Sg.js} +33 -8
  124. package/build/server/chunks/_server.ts-BCljU9Sg.js.map +1 -0
  125. package/build/server/chunks/{_server.ts-D-vgx5UZ.js → _server.ts-BTmknWpO.js} +2 -2
  126. package/build/server/chunks/{_server.ts-D-vgx5UZ.js.map → _server.ts-BTmknWpO.js.map} +1 -1
  127. package/build/server/chunks/{_server.ts-tChyh9FX.js → _server.ts-BXhmLZwN.js} +4 -2
  128. package/build/server/chunks/{_server.ts-tChyh9FX.js.map → _server.ts-BXhmLZwN.js.map} +1 -1
  129. package/build/server/chunks/{_server.ts-CvJKTS3Z.js → _server.ts-BbRSpB74.js} +4 -2
  130. package/build/server/chunks/{_server.ts-CvJKTS3Z.js.map → _server.ts-BbRSpB74.js.map} +1 -1
  131. package/build/server/chunks/{_server.ts-DKNIsQeH.js → _server.ts-Bol54_Qo.js} +4 -3
  132. package/build/server/chunks/_server.ts-Bol54_Qo.js.map +1 -0
  133. package/build/server/chunks/{_server.ts-Dz9Jd9Jh.js → _server.ts-C0PO_cAu.js} +4 -3
  134. package/build/server/chunks/{_server.ts-Dz9Jd9Jh.js.map → _server.ts-C0PO_cAu.js.map} +1 -1
  135. package/build/server/chunks/_server.ts-C6NRpe7e.js +33 -0
  136. package/build/server/chunks/_server.ts-C6NRpe7e.js.map +1 -0
  137. package/build/server/chunks/_server.ts-CGqCOCdK.js +53 -0
  138. package/build/server/chunks/_server.ts-CGqCOCdK.js.map +1 -0
  139. package/build/server/chunks/{_server.ts-B2wIgsW4.js → _server.ts-CZb-BI5H.js} +4 -3
  140. package/build/server/chunks/_server.ts-CZb-BI5H.js.map +1 -0
  141. package/build/server/chunks/_server.ts-DPHRUFYS.js +159 -0
  142. package/build/server/chunks/_server.ts-DPHRUFYS.js.map +1 -0
  143. package/build/server/chunks/{_server.ts-AnBXfZXh.js → _server.ts-D_WRex0k.js} +5 -3
  144. package/build/server/chunks/_server.ts-D_WRex0k.js.map +1 -0
  145. package/build/server/chunks/{_server.ts-CJGyN8mw.js → _server.ts-DiBMY7Ho.js} +4 -3
  146. package/build/server/chunks/_server.ts-DiBMY7Ho.js.map +1 -0
  147. package/build/server/chunks/cache-BlMaDsHi.js.map +1 -1
  148. package/build/server/chunks/{guest-registry-t0-7Zv5q.js → guest-registry-Dxvd7p-g.js} +10 -1
  149. package/build/server/chunks/guest-registry-Dxvd7p-g.js.map +1 -0
  150. package/build/server/chunks/index-CoYB03g7.js.map +1 -1
  151. package/build/server/chunks/index2-dSGQ9Eaa.js.map +1 -1
  152. package/build/server/chunks/{library-apns-Dl3iRE2h.js → library-apns-D8RPINlv.js} +62 -7
  153. package/build/server/chunks/library-apns-D8RPINlv.js.map +1 -0
  154. package/build/server/chunks/{pending-requests-C9p57WoU.js → pending-requests-8rWjrF6d.js} +3 -2
  155. package/build/server/chunks/pending-requests-8rWjrF6d.js.map +1 -0
  156. package/build/server/chunks/presence-store-Bx_g0-Gd.js +23 -0
  157. package/build/server/chunks/presence-store-Bx_g0-Gd.js.map +1 -0
  158. package/build/server/chunks/{pty-manager-CkZNoW1t.js → pty-manager-CoWVT56F.js} +27 -6
  159. package/build/server/chunks/pty-manager-CoWVT56F.js.map +1 -0
  160. package/build/server/chunks/root-D4IoFC8F.js.map +1 -1
  161. package/build/server/chunks/shooter-home-4f_HkdGI.js +10 -0
  162. package/build/server/chunks/shooter-home-4f_HkdGI.js.map +1 -0
  163. package/build/server/chunks/state.svelte-CmHqngc_.js.map +1 -1
  164. package/build/server/index.js +1 -1
  165. package/build/server/index.js.map +1 -1
  166. package/build/server/manifest.js +52 -24
  167. package/build/server/manifest.js.map +1 -1
  168. package/package.json +2 -2
  169. package/server.ts +2 -0
  170. package/src/lib/modules/client/common/index.ts +1 -0
  171. package/src/lib/modules/client/common/presence.ts +47 -0
  172. package/src/lib/modules/client/dashboard/AutopilotPanel.svelte +584 -0
  173. package/src/lib/modules/client/dashboard/autopilot-driver.svelte.ts +681 -0
  174. package/src/lib/modules/client/dashboard/decide-injection.ts +127 -0
  175. package/src/lib/modules/client/dashboard/index.ts +1 -0
  176. package/src/lib/modules/client/dashboard/store.svelte.ts +65 -24
  177. package/src/lib/modules/client/neurolink/fetch-proxy.ts +38 -1
  178. package/src/lib/modules/client/neurolink/provider-config.ts +13 -37
  179. package/src/lib/modules/server/apn/apns-payload.ts +50 -0
  180. package/src/lib/modules/server/apn/library-apns.ts +50 -8
  181. package/src/lib/modules/server/apn/pending-requests.ts +3 -1
  182. package/src/lib/modules/server/sessions/autopilot-context.ts +57 -0
  183. package/src/lib/modules/server/sessions/autopilot-engine.ts +451 -0
  184. package/src/lib/modules/server/sessions/litellm-client.ts +171 -0
  185. package/src/lib/modules/server/sessions/next-step-consensus.ts +210 -0
  186. package/src/lib/modules/server/sessions/summary-store.ts +113 -0
  187. package/src/lib/modules/server/terminal/agent-launch.ts +26 -0
  188. package/src/lib/modules/server/terminal/pty-holder.cjs +6 -0
  189. package/src/lib/modules/server/terminal/pty-manager.ts +13 -3
  190. package/src/lib/modules/server/terminal/session-watcher.ts +12 -2
  191. package/src/lib/modules/server/terminal/terminal-store.ts +3 -1
  192. package/src/lib/modules/server/utils/shooter-home.ts +16 -0
  193. package/src/lib/modules/server/ws/events-handler.ts +32 -0
  194. package/src/lib/modules/server/ws/presence-store.ts +50 -0
  195. package/src/lib/types/autopilot.ts +138 -0
  196. package/src/lib/types/index.ts +1 -0
  197. package/src/lib/types/terminal-client.ts +2 -0
  198. package/src/routes/+layout.server.ts +2 -0
  199. package/src/routes/+layout.svelte +19 -4
  200. package/src/routes/+page.svelte +9 -1
  201. package/src/routes/api/autopilot/+server.ts +74 -0
  202. package/src/routes/api/autopilot/goal/+server.ts +72 -0
  203. package/src/routes/api/neurolink-proxy/+server.ts +39 -8
  204. package/src/routes/api/notify/+server.ts +38 -14
  205. package/src/routes/api/presence/+server.ts +39 -0
  206. package/src/routes/api/summaries/+server.ts +99 -0
  207. package/src/routes/api/ws-status/+server.ts +8 -5
  208. package/build/client/_app/immutable/assets/2.DjiwkLqE.css +0 -1
  209. package/build/client/_app/immutable/assets/2.DjiwkLqE.css.br +0 -0
  210. package/build/client/_app/immutable/assets/2.DjiwkLqE.css.gz +0 -0
  211. package/build/client/_app/immutable/chunks/BB2l8o4X.js.br +0 -0
  212. package/build/client/_app/immutable/chunks/BB2l8o4X.js.gz +0 -0
  213. package/build/client/_app/immutable/chunks/BvmdJful.js.br +0 -0
  214. package/build/client/_app/immutable/chunks/BvmdJful.js.gz +0 -0
  215. package/build/client/_app/immutable/chunks/CRkG7oE4.js +0 -1
  216. package/build/client/_app/immutable/chunks/CRkG7oE4.js.br +0 -0
  217. package/build/client/_app/immutable/chunks/CRkG7oE4.js.gz +0 -0
  218. package/build/client/_app/immutable/chunks/C_YNQL8b.js +0 -3
  219. package/build/client/_app/immutable/chunks/C_YNQL8b.js.br +0 -0
  220. package/build/client/_app/immutable/chunks/C_YNQL8b.js.gz +0 -0
  221. package/build/client/_app/immutable/chunks/ClIPTXf3.js.br +0 -0
  222. package/build/client/_app/immutable/chunks/ClIPTXf3.js.gz +0 -0
  223. package/build/client/_app/immutable/chunks/DIZ3Qst5.js.br +0 -0
  224. package/build/client/_app/immutable/chunks/DT4H19pV.js.br +0 -0
  225. package/build/client/_app/immutable/chunks/DT4H19pV.js.gz +0 -0
  226. package/build/client/_app/immutable/chunks/ZS5XYDx_.js.br +0 -0
  227. package/build/client/_app/immutable/chunks/pRcLbE0d.js +0 -1
  228. package/build/client/_app/immutable/chunks/pRcLbE0d.js.br +0 -0
  229. package/build/client/_app/immutable/chunks/pRcLbE0d.js.gz +0 -0
  230. package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.br +0 -0
  231. package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.gz +0 -0
  232. package/build/client/_app/immutable/entry/start.evvp4tX7.js +0 -1
  233. package/build/client/_app/immutable/entry/start.evvp4tX7.js.br +0 -2
  234. package/build/client/_app/immutable/entry/start.evvp4tX7.js.gz +0 -0
  235. package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js +0 -10
  236. package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.br +0 -0
  237. package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.gz +0 -0
  238. package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.br +0 -0
  239. package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.gz +0 -0
  240. package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.br +0 -0
  241. package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.gz +0 -0
  242. package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.br +0 -0
  243. package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.gz +0 -0
  244. package/build/client/_app/immutable/nodes/2.DcRhsjYp.js +0 -13
  245. package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.br +0 -0
  246. package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.gz +0 -0
  247. package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.br +0 -0
  248. package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.gz +0 -0
  249. package/build/client/_app/immutable/nodes/4.CBX9A3ka.js.br +0 -0
  250. package/build/client/_app/immutable/nodes/4.CBX9A3ka.js.gz +0 -0
  251. package/build/client/_app/immutable/nodes/5.DIVKuZc9.js.br +0 -0
  252. package/build/client/_app/immutable/nodes/5.DIVKuZc9.js.gz +0 -0
  253. package/build/client/_app/immutable/nodes/6.ComiWlV6.js.br +0 -0
  254. package/build/client/_app/immutable/nodes/6.ComiWlV6.js.gz +0 -0
  255. package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.br +0 -0
  256. package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.gz +0 -0
  257. package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.br +0 -0
  258. package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.gz +0 -0
  259. package/build/client/_app/immutable/nodes/9.CAJucyeI.js.br +0 -0
  260. package/build/client/_app/immutable/nodes/9.CAJucyeI.js.gz +0 -0
  261. package/build/server/chunks/0-DDGB6CRT.js.map +0 -1
  262. package/build/server/chunks/_page.svelte-BLo2v_8E.js.map +0 -1
  263. package/build/server/chunks/_page.svelte-tBuIq8Pg.js +0 -159
  264. package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +0 -1
  265. package/build/server/chunks/_server.ts-AnBXfZXh.js.map +0 -1
  266. package/build/server/chunks/_server.ts-B2wIgsW4.js.map +0 -1
  267. package/build/server/chunks/_server.ts-BRAzC6W1.js.map +0 -1
  268. package/build/server/chunks/_server.ts-CJGyN8mw.js.map +0 -1
  269. package/build/server/chunks/_server.ts-DEx9-epI.js.map +0 -1
  270. package/build/server/chunks/_server.ts-DKNIsQeH.js.map +0 -1
  271. package/build/server/chunks/_server.ts-bk_EeAdY.js.map +0 -1
  272. package/build/server/chunks/guest-registry-t0-7Zv5q.js.map +0 -1
  273. package/build/server/chunks/library-apns-Dl3iRE2h.js.map +0 -1
  274. package/build/server/chunks/pending-requests-C9p57WoU.js.map +0 -1
  275. package/build/server/chunks/pty-manager-CkZNoW1t.js.map +0 -1
@@ -0,0 +1,451 @@
1
+ // Always-on autopilot engine (server-side).
2
+ //
3
+ // Subscribes to the global ShooterEvent stream, tracks per-session state across
4
+ // ALL threads, and on a meaningful trigger runs: one LiteLLM summary + five
5
+ // DISTINCT-LENS next-step agents (blocker / next-command / risk / validation /
6
+ // progress) -> consensus merge -> persist to SQLite -> push to the phone (using
7
+ // the normal skip logic, so it only pushes when no dashboard is watching).
8
+ //
9
+ // Lives in the server.ts module graph (started from server.ts). Cross-graph
10
+ // state (events, store, control) is shared via globalThis singletons.
11
+
12
+ import type { AgentProposal, NextStep, SessionSummaryRecord, WireShooterEvent } from '$lib/types';
13
+
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
+ import { join } from 'path';
16
+
17
+ import { ptyManager } from '../terminal/pty-manager.js';
18
+ import { shooterDataDir } from '../utils/shooter-home.js';
19
+ import { onShooterEvent } from '../ws/events-handler.js';
20
+ import { isViewerPresent } from '../ws/presence-store.js';
21
+ import {
22
+ buildEngineContext,
23
+ clearEngineGoal,
24
+ getEngineGoal,
25
+ setEngineGoal,
26
+ } from './autopilot-context.js';
27
+ import { isLiteLLMConfigured, litellmJson } from './litellm-client.js';
28
+ import { mergeNextStepConsensus } from './next-step-consensus.js';
29
+ import { summaryStore } from './summary-store.js';
30
+
31
+ // ── Constants ────────────────────────────────────────────────────────
32
+
33
+ const HIGH_SIGNAL = new Set(['agent-idle', 'agent-question', 'tool-failed']);
34
+ const GRACE_MS = 4_000; // fire this long after the FIRST qualifying event (fast, not resetting)
35
+ const MIN_INTERVAL_MS = 30_000; // minimum gap between pipeline runs per session (cost bound)
36
+ const PERIODIC_EVERY = 20;
37
+ const ERROR_THRESHOLD = 3;
38
+ const MAX_EVENTS = 60;
39
+ const SUMMARY_MAX_TOKENS = 400;
40
+ const STEPS_MAX_TOKENS = 400;
41
+ // How many of the five lenses run concurrently. Kept BELOW the typical LiteLLM key parallel cap so
42
+ // the engine never saturates the key by itself: firing all five at once exhausted a
43
+ // max_parallel_requests=5 key and 429'd every call (empty consensus, silent stall). Default 3
44
+ // leaves headroom for the summary's retry + other callers sharing the key. Tune per key via env.
45
+ const LENS_CONCURRENCY = Math.max(1, Number(process.env.AUTOPILOT_LENS_CONCURRENCY) || 3);
46
+ // Consensus quorum: how many of the 5 distinct lenses must agree for a step to be non-tentative
47
+ // (and thus auto-injectable). The lenses are DIFFERENT perspectives that phrase the same action
48
+ // differently, so the Jaccard clustering systematically undercounts true agreement (observed: 4/5
49
+ // lenses proposed the same fix but only 2 clustered). 2-of-5 + the confidence floor + the eight
50
+ // driver guards (idle-only, managed-only, rate-limit, dedup, circuit-breaker, command-guard,
51
+ // kill-switch, human-grace) is the practical bar; quorum 3 left the autopilot almost never firing.
52
+ const CONSENSUS_QUORUM = 2;
53
+ const STATE_FILE = join(shooterDataDir(), 'autopilot.json');
54
+
55
+ // The engine does STRUCTURED extraction (one summary + five voting lenses). A reasoning model like
56
+ // `open-large` does this badly: it reasons out loud and ignores response_format, so the JSON never
57
+ // parses and the consensus collapses to tentative garbage. Pin a fast NON-reasoning model for the
58
+ // pipeline, independent of the user's chat model (LITELLM_MODEL). Overridable via AUTOPILOT_MODEL.
59
+ const ENGINE_MODEL = process.env.AUTOPILOT_MODEL?.trim() || 'open-fast';
60
+
61
+ // Lead with the JSON-only contract; the per-lens perspective is a TRAILING modifier so the model
62
+ // doesn't start "thinking" about a role. No copyable placeholder value in the schema — reasoning
63
+ // models echo it verbatim ("a real shell command or instruction" leaked into real consensus).
64
+ const JSON_API_RULES =
65
+ 'You are a JSON API. Output ONLY one JSON object and nothing else — no prose, no reasoning, no ' +
66
+ 'markdown, no code fences. The first character MUST be { and the last MUST be }.';
67
+
68
+ /** Five DISTINCT perspectives — each a different angle, so converging votes mean real agreement. */
69
+ const LENSES = [
70
+ 'what is currently blocking progress, or is most likely to fail next',
71
+ 'the exact shell commands or file edits the agent should run next, in order',
72
+ 'what could go wrong if the agent continues on its current path, and what to validate first',
73
+ 'how to verify the work so far is correct — the tests, checks, or inspections to run',
74
+ 'the single most direct next step toward completing the session goal',
75
+ ] as const;
76
+
77
+ // ── Per-session state (globalThis-shared) ───────────────────────────
78
+
79
+ // eslint-disable-next-line no-restricted-syntax -- internal engine state, never exported
80
+ interface EngineSession {
81
+ cancelled: boolean;
82
+ errorCount: number;
83
+ eventCount: number;
84
+ events: string[];
85
+ // Highest signal + latest trigger seen during the OPEN grace window — evaluated at FIRE time, not
86
+ // schedule time, so an agent-idle arriving after a low-signal event still runs as a high trigger.
87
+ graceIsHigh: boolean;
88
+ graceTimer: null | ReturnType<typeof setTimeout>;
89
+ graceTrigger: string;
90
+ lastRunAt: number;
91
+ projectName: string;
92
+ running: boolean;
93
+ status: string;
94
+ terminalId: string;
95
+ toolCallCount: number;
96
+ }
97
+
98
+ const SESSIONS_KEY = '__shooter_autopilot_sessions';
99
+ const sessions: Map<string, EngineSession> =
100
+ ((globalThis as Record<string, unknown>)[SESSIONS_KEY] as Map<string, EngineSession>) ||
101
+ new Map<string, EngineSession>();
102
+ (globalThis as Record<string, unknown>)[SESSIONS_KEY] = sessions;
103
+
104
+ let enabled = readPersistedEnabled();
105
+ let unsubscribe: (() => void) | null = null;
106
+
107
+ // ── Control (exposed on globalThis so the /api/autopilot route can reach it) ──
108
+
109
+ export function isAutopilotEnabled(): boolean {
110
+ return enabled;
111
+ }
112
+
113
+ export function setAutopilotEnabled(value: boolean): void {
114
+ enabled = value;
115
+ try {
116
+ mkdirSync(shooterDataDir(), { recursive: true });
117
+ writeFileSync(STATE_FILE, JSON.stringify({ enabled: value }), 'utf-8');
118
+ } catch {
119
+ // best-effort persistence
120
+ }
121
+ if (!value) {
122
+ for (const s of sessions.values()) {
123
+ if (s.graceTimer) {
124
+ clearTimeout(s.graceTimer);
125
+ s.graceTimer = null;
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ /** Start the engine: subscribe to the event stream. Idempotent. */
132
+ export function startAutopilotEngine(): void {
133
+ if (unsubscribe) {
134
+ return;
135
+ }
136
+ const control = {
137
+ getGoal: getEngineGoal,
138
+ isEnabled: isAutopilotEnabled,
139
+ setEnabled: setAutopilotEnabled,
140
+ setGoal: setEngineGoal,
141
+ };
142
+ (globalThis as Record<string, unknown>).__shooter_autopilot = control;
143
+ unsubscribe = onShooterEvent(handleEvent);
144
+ // Pre-track sessions for terminals that already exist at startup (e.g. a server restart that
145
+ // reconnected persisted terminals). Without this a session is only created lazily on its NEXT
146
+ // event, so an agent that went idle before the engine attached would be invisible until it emits
147
+ // again. We deliberately do NOT force a pipeline run here — that would risk spurious LLM spend on
148
+ // a genuinely quiet terminal; we just ensure the session is tracked so the next event triggers.
149
+ try {
150
+ for (const term of ptyManager.list()) {
151
+ if (!sessions.has(term.id)) {
152
+ createSession(term.id);
153
+ }
154
+ }
155
+ } catch {
156
+ // ptyManager not ready yet — sessions will be created lazily on first event
157
+ }
158
+ console.log(
159
+ `[autopilot] engine started (enabled=${enabled}, litellm=${isLiteLLMConfigured() ? 'configured' : 'absent'}, lensConcurrency=${LENS_CONCURRENCY})`
160
+ );
161
+ }
162
+
163
+ // ── Event handling ───────────────────────────────────────────────────
164
+
165
+ function applyEvent(session: EngineSession, event: WireShooterEvent): void {
166
+ const parts: string[] = [event.type];
167
+ if ('tool' in event && event.tool) {
168
+ parts.push(`tool=${event.tool}`);
169
+ }
170
+ if ('error' in event && event.error) {
171
+ parts.push(`error=${event.error.slice(0, 140)}`);
172
+ }
173
+ if ('command' in event && event.command) {
174
+ parts.push(`cmd=${event.command.slice(0, 80)}`);
175
+ }
176
+ if ('message' in event && event.message) {
177
+ // The agent's last message on idle (the OUTCOME — e.g. "the test failed because…") is the
178
+ // richest signal the lenses have; keep enough of it to actually ground the next-step votes.
179
+ parts.push(`msg=${event.message.slice(0, 400)}`);
180
+ }
181
+ session.events.push(parts.join(' '));
182
+ if (session.events.length > MAX_EVENTS) {
183
+ session.events.shift();
184
+ }
185
+ switch (event.type) {
186
+ case 'agent-idle':
187
+ case 'agent-question':
188
+ session.status = 'idle';
189
+ break;
190
+ case 'tool-completed':
191
+ session.toolCallCount += 1;
192
+ break;
193
+ case 'tool-failed':
194
+ session.errorCount += 1;
195
+ break;
196
+ case 'tool-started':
197
+ session.toolCallCount += 1;
198
+ session.status = 'running';
199
+ break;
200
+ default:
201
+ break;
202
+ }
203
+ }
204
+
205
+ function createSession(terminalId: string): EngineSession {
206
+ const session: EngineSession = {
207
+ cancelled: false,
208
+ errorCount: 0,
209
+ eventCount: 0,
210
+ events: [],
211
+ graceIsHigh: false,
212
+ graceTimer: null,
213
+ graceTrigger: '',
214
+ lastRunAt: 0,
215
+ projectName: projectNameFor(terminalId),
216
+ running: false,
217
+ status: 'running',
218
+ terminalId,
219
+ toolCallCount: 0,
220
+ };
221
+ sessions.set(terminalId, session);
222
+ return session;
223
+ }
224
+
225
+ function fallbackSummary(session: EngineSession): string {
226
+ return `${session.status} — ${session.toolCallCount} tool calls, ${session.errorCount} errors`;
227
+ }
228
+
229
+ function handleEvent(event: WireShooterEvent): void {
230
+ const terminalId = 'terminalId' in event ? event.terminalId : undefined;
231
+ if (!terminalId) {
232
+ return;
233
+ }
234
+
235
+ if (event.type === 'terminal-exited') {
236
+ const s = sessions.get(terminalId);
237
+ if (s) {
238
+ s.cancelled = true; // signal any in-flight pipeline to stop before persist/push
239
+ if (s.graceTimer) {
240
+ clearTimeout(s.graceTimer);
241
+ }
242
+ }
243
+ sessions.delete(terminalId);
244
+ clearEngineGoal(terminalId);
245
+ return;
246
+ }
247
+
248
+ const session = sessions.get(terminalId) ?? createSession(terminalId);
249
+ session.eventCount += 1;
250
+ applyEvent(session, event);
251
+
252
+ if (!enabled || session.running) {
253
+ return;
254
+ }
255
+ const isHigh = HIGH_SIGNAL.has(event.type);
256
+ const isPeriodic = session.eventCount % PERIODIC_EVERY === 0;
257
+ const isErrorThreshold = session.errorCount >= ERROR_THRESHOLD;
258
+ if (!isHigh && !isPeriodic && !isErrorThreshold) {
259
+ return;
260
+ }
261
+ if (Date.now() - session.lastRunAt < MIN_INTERVAL_MS) {
262
+ return;
263
+ }
264
+ // Track the strongest signal + latest trigger across the whole window (so a high-signal event
265
+ // arriving after the timer was armed by a low-signal one is not lost). Schedule the timer ONCE;
266
+ // do NOT reset it on every event (so a burst doesn't keep delaying the run).
267
+ if (!session.graceTimer) {
268
+ session.graceIsHigh = isHigh;
269
+ session.graceTrigger = event.type;
270
+ session.graceTimer = setTimeout(() => {
271
+ session.graceTimer = null;
272
+ const trigger = session.graceTrigger;
273
+ const wasHigh = session.graceIsHigh;
274
+ void runPipeline(session, trigger, wasHigh);
275
+ }, GRACE_MS);
276
+ } else {
277
+ session.graceIsHigh = session.graceIsHigh || isHigh;
278
+ session.graceTrigger = event.type;
279
+ }
280
+ }
281
+
282
+ // ── Pipeline ─────────────────────────────────────────────────────────
283
+
284
+ /** Run `fn` over `items` with at most `limit` in flight; preserves input order. */
285
+ async function mapLimit<T, R>(
286
+ items: readonly T[],
287
+ limit: number,
288
+ fn: (item: T) => Promise<R>
289
+ ): Promise<R[]> {
290
+ const results: R[] = new Array<R>(items.length);
291
+ let next = 0;
292
+ const worker = async (): Promise<void> => {
293
+ while (next < items.length) {
294
+ const idx = next++;
295
+ results[idx] = await fn(items[idx]);
296
+ }
297
+ };
298
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
299
+ return results;
300
+ }
301
+
302
+ function persist(
303
+ session: EngineSession,
304
+ summary: string,
305
+ steps: NextStep[],
306
+ trigger: string
307
+ ): void {
308
+ const record: SessionSummaryRecord = {
309
+ createdAt: new Date().toISOString(),
310
+ id: `auto-${session.terminalId}-${Date.now()}`,
311
+ nextSteps: JSON.stringify(steps),
312
+ projectName: session.projectName,
313
+ // No separate JSONL session UUID server-side; key on terminalId so
314
+ // GET /api/summaries?sessionId=<terminalId> works (was previously null).
315
+ sessionId: session.terminalId,
316
+ summary,
317
+ terminalId: session.terminalId,
318
+ trigger,
319
+ };
320
+ try {
321
+ summaryStore.insert(record);
322
+ } catch (err) {
323
+ console.error('[autopilot] persist failed:', err instanceof Error ? err.message : String(err));
324
+ }
325
+ }
326
+
327
+ function projectNameFor(terminalId: string): string {
328
+ try {
329
+ const term = ptyManager.list().find((t) => t.id === terminalId);
330
+ if (term?.cwd) {
331
+ const segs = term.cwd.split('/').filter(Boolean);
332
+ return segs.slice(-2).join('/') || terminalId;
333
+ }
334
+ } catch {
335
+ // ptyManager unavailable — fall through
336
+ }
337
+ return terminalId;
338
+ }
339
+
340
+ async function push(session: EngineSession, summary: string, steps: NextStep[]): Promise<void> {
341
+ const top = steps[0];
342
+ if (!top) {
343
+ return;
344
+ }
345
+ // Presence-aware: when a viewer is foregrounded (watching the live dashboard) skip the
346
+ // push — they see it in-app. Push only when away. If no presence-aware client ever
347
+ // reported, isViewerPresent() is false → push proceeds (prior always-push behavior).
348
+ if (isViewerPresent()) {
349
+ return;
350
+ }
351
+ const port = process.env.PORT || '54007';
352
+ const apiKey = process.env.API_KEY;
353
+ if (!apiKey) {
354
+ return;
355
+ }
356
+ try {
357
+ await fetch(`http://127.0.0.1:${port}/api/notify`, {
358
+ body: JSON.stringify({
359
+ data: {
360
+ category: session.terminalId,
361
+ dedupKey: `${session.terminalId}|${top.text.slice(0, 60)}`,
362
+ sessionId: session.terminalId,
363
+ source: 'autopilot',
364
+ },
365
+ message: `${summary.slice(0, 80)} — Next: ${top.text.slice(0, 60)}`,
366
+ title: `Autopilot: ${session.projectName}`,
367
+ }),
368
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
369
+ method: 'POST',
370
+ });
371
+ } catch (err) {
372
+ console.warn('[autopilot] push failed:', err instanceof Error ? err.message : String(err));
373
+ }
374
+ }
375
+
376
+ // ── Helpers ──────────────────────────────────────────────────────────
377
+
378
+ function readPersistedEnabled(): boolean {
379
+ try {
380
+ if (existsSync(STATE_FILE)) {
381
+ const parsed: unknown = JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
382
+ return Boolean((parsed as { enabled?: unknown })?.enabled);
383
+ }
384
+ } catch {
385
+ // corrupt / unreadable
386
+ }
387
+ return false;
388
+ }
389
+
390
+ async function runPipeline(
391
+ session: EngineSession,
392
+ trigger: string,
393
+ isHigh: boolean
394
+ ): Promise<void> {
395
+ if (!enabled || session.running) {
396
+ return;
397
+ }
398
+ session.running = true;
399
+ session.lastRunAt = Date.now();
400
+ try {
401
+ const context = buildEngineContext({
402
+ errorCount: session.errorCount,
403
+ events: session.events,
404
+ goal: getEngineGoal(session.terminalId),
405
+ projectName: session.projectName,
406
+ status: session.status,
407
+ toolCallCount: session.toolCallCount,
408
+ trigger,
409
+ });
410
+
411
+ const summaryResult = await litellmJson<{ summary: string }>({
412
+ maxTokens: SUMMARY_MAX_TOKENS,
413
+ model: ENGINE_MODEL,
414
+ systemInstruction: `${JSON_API_RULES} The object has one key "summary": a single sentence (max 120 characters) describing the current status of this coding session.`,
415
+ userPrompt: `${context}\n\nSummarise what is happening in this coding session in ONE sentence (max 120 chars).`,
416
+ });
417
+ const summary = summaryResult?.summary?.trim() || fallbackSummary(session);
418
+
419
+ // Run the five lenses at most LENS_CONCURRENCY in flight (default 3) so the engine never
420
+ // saturates the LiteLLM key's parallel cap by itself. Each failed lens yields an empty list;
421
+ // a step needs CONSENSUS_QUORUM (2) of the 5 to be non-tentative.
422
+ const agentLists: AgentProposal[][] = await mapLimit(LENSES, LENS_CONCURRENCY, async (lens) => {
423
+ const r = await litellmJson<{ steps: AgentProposal[] }>({
424
+ maxTokens: STEPS_MAX_TOKENS,
425
+ model: ENGINE_MODEL,
426
+ systemInstruction: `${JSON_API_RULES} The object has one key "steps": an array of 1 to 3 objects, each {"text": the concrete next action for THIS exact session written in full as a string, "confidence": a number between 0 and 1}. Choose the actions from THIS perspective: ${lens}.`,
427
+ userPrompt: `${context}\n\nGiven the session above, what should happen next?`,
428
+ });
429
+ return r?.steps ?? [];
430
+ });
431
+ const consensus = mergeNextStepConsensus(agentLists, { quorum: CONSENSUS_QUORUM });
432
+
433
+ if (!enabled || session.cancelled) {
434
+ return; // engine disabled or terminal exited mid-pipeline — don't persist/push a dead session
435
+ }
436
+ persist(session, summary, consensus.steps, trigger);
437
+ // A run consumed the accumulated errors — reset so the error-threshold trigger (errorCount >= 3)
438
+ // does not stick and re-fire on every subsequent event for the rest of the session's life.
439
+ session.errorCount = 0;
440
+ if (isHigh && consensus.steps.length > 0 && !consensus.steps[0].tentative) {
441
+ await push(session, summary, consensus.steps);
442
+ }
443
+ } catch (err) {
444
+ console.error(
445
+ `[autopilot] pipeline failed for ${session.terminalId}:`,
446
+ err instanceof Error ? err.message : String(err)
447
+ );
448
+ } finally {
449
+ session.running = false;
450
+ }
451
+ }
@@ -0,0 +1,171 @@
1
+ // Server-side LiteLLM client. Calls the OpenAI-compatible chat-completions API
2
+ // directly with the server-side key (process.env) — no proxy, no browser
3
+ // exposure. Used by the always-on autopilot engine.
4
+
5
+ const REQUEST_TIMEOUT_MS = 30_000;
6
+ const MAX_ATTEMPTS = 2;
7
+ const BACKOFF_MS = [800]; // backoff before the single retry (rate-limit / transient failures)
8
+
9
+ /** Pull the model's JSON out of an OpenAI-shaped chat-completion response. */
10
+ export function extractJsonContent(data: unknown): unknown {
11
+ const content = (data as { choices?: { message?: { content?: string } }[] })?.choices?.[0]
12
+ ?.message?.content;
13
+ if (typeof content !== 'string') {
14
+ return null;
15
+ }
16
+ return parseJsonResponse(content);
17
+ }
18
+
19
+ /** True when LITELLM_BASE_URL and LITELLM_API_KEY are both configured server-side. */
20
+ export function isLiteLLMConfigured(): boolean {
21
+ return litellmConfig() !== null;
22
+ }
23
+
24
+ /**
25
+ * Run a structured-JSON prompt against LiteLLM. Returns the parsed object, or
26
+ * null when LiteLLM is unconfigured / unreachable / the response is not JSON.
27
+ */
28
+ export async function litellmJson<T>(opts: {
29
+ maxTokens?: number;
30
+ /** Override the model for this call (e.g. the autopilot engine pins a non-reasoning model). */
31
+ model?: string;
32
+ systemInstruction: string;
33
+ userPrompt: string;
34
+ }): Promise<null | T> {
35
+ const cfg = litellmConfig();
36
+ if (!cfg) {
37
+ return null;
38
+ }
39
+ // Attempt with JSON mode (forces the model to emit ONLY valid JSON — verbose/reasoning models
40
+ // otherwise wrap prose around it), retrying with backoff for rate-limit / transient burst
41
+ // failures. If every JSON-mode attempt fails, fall back ONCE without response_format in case the
42
+ // gateway rejects it — so a non-supporting backend degrades to prior behavior instead of breaking.
43
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
44
+ const result = await attemptCompletion<T>(cfg, opts, true);
45
+ if (result !== null) {
46
+ return result;
47
+ }
48
+ if (attempt < MAX_ATTEMPTS - 1) {
49
+ await delay(BACKOFF_MS[attempt]);
50
+ }
51
+ }
52
+ return attemptCompletion<T>(cfg, opts, false);
53
+ }
54
+
55
+ /**
56
+ * Strip optional markdown fences and parse JSON. If the whole string is not
57
+ * valid JSON, extract the FIRST balanced {...} object (a single forward scan
58
+ * tracking brace depth — correct across nested braces and multiple objects,
59
+ * unlike a greedy regex which would span to the last closing brace).
60
+ */
61
+ export function parseJsonResponse(raw: string): unknown {
62
+ const cleaned = raw
63
+ .replace(/^```(?:json)?\s*/i, '')
64
+ .replace(/```\s*$/, '')
65
+ .trim();
66
+ if (!cleaned) {
67
+ return null;
68
+ }
69
+ try {
70
+ return JSON.parse(cleaned);
71
+ } catch {
72
+ // Scan for the FIRST balanced {...} region that actually parses. A reasoning model often emits
73
+ // an invalid brace region (e.g. "{not: valid}") in its preamble BEFORE the real JSON, so on a
74
+ // failed candidate we must advance to the next "{" and keep scanning — not give up (the old
75
+ // `return null` here discarded every later region and collapsed consensus to empty).
76
+ let start = cleaned.indexOf('{');
77
+ while (start !== -1) {
78
+ let depth = 0;
79
+ let end = -1;
80
+ for (let i = start; i < cleaned.length; i++) {
81
+ if (cleaned[i] === '{') {
82
+ depth++;
83
+ } else if (cleaned[i] === '}') {
84
+ depth--;
85
+ if (depth === 0) {
86
+ end = i;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ if (end === -1) {
92
+ return null; // unbalanced through end of string — nothing more to try
93
+ }
94
+ try {
95
+ return JSON.parse(cleaned.slice(start, end + 1));
96
+ } catch {
97
+ start = cleaned.indexOf('{', start + 1);
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ }
103
+
104
+ async function attemptCompletion<T>(
105
+ cfg: { base: string; key: string; model: string },
106
+ opts: { maxTokens?: number; model?: string; systemInstruction: string; userPrompt: string },
107
+ jsonMode: boolean
108
+ ): Promise<null | T> {
109
+ const controller = new AbortController();
110
+ const timer = setTimeout(() => {
111
+ controller.abort();
112
+ }, REQUEST_TIMEOUT_MS);
113
+ try {
114
+ const body: Record<string, unknown> = {
115
+ max_tokens: opts.maxTokens ?? 400,
116
+ messages: [
117
+ { content: opts.systemInstruction, role: 'system' },
118
+ { content: opts.userPrompt, role: 'user' },
119
+ ],
120
+ model: opts.model?.trim() || cfg.model,
121
+ temperature: 0,
122
+ };
123
+ if (jsonMode) {
124
+ body.response_format = { type: 'json_object' };
125
+ }
126
+ const res = await fetch(`${cfg.base}/chat/completions`, {
127
+ body: JSON.stringify(body),
128
+ headers: { Authorization: `Bearer ${cfg.key}`, 'Content-Type': 'application/json' },
129
+ method: 'POST',
130
+ signal: controller.signal,
131
+ });
132
+ if (!res.ok) {
133
+ // Surface the failure — silently returning null here made the whole autopilot pipeline
134
+ // produce empty consensus invisibly (no log, no signal). 429 = the LiteLLM key's
135
+ // max_parallel_requests is exhausted (often by other processes sharing the key).
136
+ const detail = await res.text().catch(() => '');
137
+ console.warn(
138
+ `[litellm] HTTP ${res.status} (model=${body.model as string}, jsonMode=${jsonMode}): ${detail.slice(0, 200)}`
139
+ );
140
+ return null;
141
+ }
142
+ const data: unknown = await res.json();
143
+ return extractJsonContent(data) as null | T;
144
+ } catch (err) {
145
+ const reason = err instanceof Error ? err.message : String(err);
146
+ // AbortError = our REQUEST_TIMEOUT_MS fired (the gateway hung). Anything else = network/parse.
147
+ console.warn(`[litellm] request failed (model=${opts.model?.trim() || cfg.model}): ${reason}`);
148
+ return null;
149
+ } finally {
150
+ clearTimeout(timer);
151
+ }
152
+ }
153
+
154
+ function delay(ms: number): Promise<void> {
155
+ return new Promise((resolve) => {
156
+ setTimeout(resolve, ms);
157
+ });
158
+ }
159
+
160
+ function litellmConfig(): null | { base: string; key: string; model: string } {
161
+ const base = process.env.LITELLM_BASE_URL?.trim();
162
+ const key = process.env.LITELLM_API_KEY?.trim();
163
+ if (!base || !key) {
164
+ return null;
165
+ }
166
+ return {
167
+ base: base.replace(/\/+$/, ''),
168
+ key,
169
+ model: process.env.LITELLM_MODEL?.trim() || 'open-large',
170
+ };
171
+ }