@juspay/shooter 1.21.0 → 1.23.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 (221) 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/{DOEXXmsh.js → Bj5wFimK.js} +2 -2
  6. package/build/client/_app/immutable/chunks/Bj5wFimK.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/Bj5wFimK.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{EqMAkEha.js → BjYr_-Ss.js} +1 -1
  9. package/build/client/_app/immutable/chunks/BjYr_-Ss.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/BjYr_-Ss.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/DULfdsh6.js +6 -0
  15. package/build/client/_app/immutable/chunks/DULfdsh6.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/DULfdsh6.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/{BmfLecb1.js → fcNfTA-E.js} +1 -1
  18. package/build/client/_app/immutable/chunks/fcNfTA-E.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/fcNfTA-E.js.gz +0 -0
  20. package/build/client/_app/immutable/entry/{app.CeSxgGat.js → app.Bvoqymnp.js} +2 -2
  21. package/build/client/_app/immutable/entry/app.Bvoqymnp.js.br +0 -0
  22. package/build/client/_app/immutable/entry/app.Bvoqymnp.js.gz +0 -0
  23. package/build/client/_app/immutable/entry/start.BqXCPPZJ.js +1 -0
  24. package/build/client/_app/immutable/entry/start.BqXCPPZJ.js.br +2 -0
  25. package/build/client/_app/immutable/entry/start.BqXCPPZJ.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/{0.oaPwxh1O.js → 0.Bv_TwEnq.js} +1 -1
  27. package/build/client/_app/immutable/nodes/0.Bv_TwEnq.js.br +0 -0
  28. package/build/client/_app/immutable/nodes/0.Bv_TwEnq.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{1.DMPyoM-M.js → 1.7lffTIeb.js} +1 -1
  30. package/build/client/_app/immutable/nodes/1.7lffTIeb.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/1.7lffTIeb.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/{10.Cbm7nQKK.js → 10.ChiIrIDl.js} +1 -1
  33. package/build/client/_app/immutable/nodes/10.ChiIrIDl.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/10.ChiIrIDl.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js → 11.DO3vyXEv.js} +2 -2
  36. package/build/client/_app/immutable/nodes/11.DO3vyXEv.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js.gz → 11.DO3vyXEv.js.gz} +0 -0
  38. package/build/client/_app/immutable/nodes/2.iMIqsE7n.js +23 -0
  39. package/build/client/_app/immutable/nodes/2.iMIqsE7n.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/2.iMIqsE7n.js.gz +0 -0
  41. package/build/client/_app/immutable/nodes/{3.BgLpGnzb.js → 3.CArnSHOO.js} +1 -1
  42. package/build/client/_app/immutable/nodes/3.CArnSHOO.js.br +0 -0
  43. package/build/client/_app/immutable/nodes/3.CArnSHOO.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/{5.Avc1-gVb.js → 5.DziEu9rx.js} +1 -1
  45. package/build/client/_app/immutable/nodes/5.DziEu9rx.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/5.DziEu9rx.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/{6.Dw2wEssJ.js → 6.B8l1RwkB.js} +1 -1
  48. package/build/client/_app/immutable/nodes/6.B8l1RwkB.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/6.B8l1RwkB.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/{7.DwKZjoBg.js → 7.BPyfhDis.js} +1 -1
  51. package/build/client/_app/immutable/nodes/7.BPyfhDis.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/7.BPyfhDis.js.gz +0 -0
  53. package/build/client/_app/immutable/nodes/{8.ZUAI6g5E.js → 8.D_vszZ9E.js} +1 -1
  54. package/build/client/_app/immutable/nodes/8.D_vszZ9E.js.br +0 -0
  55. package/build/client/_app/immutable/nodes/8.D_vszZ9E.js.gz +0 -0
  56. package/build/client/_app/immutable/nodes/{9.I_KGXPwB.js → 9.Drah-do-.js} +1 -1
  57. package/build/client/_app/immutable/nodes/9.Drah-do-.js.br +0 -0
  58. package/build/client/_app/immutable/nodes/9.Drah-do-.js.gz +0 -0
  59. package/build/client/_app/version.json +1 -1
  60. package/build/client/_app/version.json.br +0 -0
  61. package/build/client/_app/version.json.gz +0 -0
  62. package/build/pty-holder.cjs +6 -0
  63. package/build/server/chunks/{0-vrTNAfZB.js → 0-DAB_6Vm1.js} +2 -2
  64. package/build/server/chunks/{0-vrTNAfZB.js.map → 0-DAB_6Vm1.js.map} +1 -1
  65. package/build/server/chunks/{1-nbr-bOoF.js → 1-D-qMYaCx.js} +2 -2
  66. package/build/server/chunks/{1-nbr-bOoF.js.map → 1-D-qMYaCx.js.map} +1 -1
  67. package/build/server/chunks/{10-ChyvvJ6w.js → 10-CeFFGo-X.js} +2 -2
  68. package/build/server/chunks/{10-ChyvvJ6w.js.map → 10-CeFFGo-X.js.map} +1 -1
  69. package/build/server/chunks/{11-6ZAjL3uU.js → 11-DRMu_ATU.js} +2 -2
  70. package/build/server/chunks/{11-6ZAjL3uU.js.map → 11-DRMu_ATU.js.map} +1 -1
  71. package/build/server/chunks/{2-DWFRVDWJ.js → 2-B7OLBMNH.js} +4 -4
  72. package/build/server/chunks/{2-DWFRVDWJ.js.map → 2-B7OLBMNH.js.map} +1 -1
  73. package/build/server/chunks/{3-CKANM_WM.js → 3-B38ZarLw.js} +2 -2
  74. package/build/server/chunks/{3-CKANM_WM.js.map → 3-B38ZarLw.js.map} +1 -1
  75. package/build/server/chunks/{5-BxVjs2qi.js → 5-D-Uv1voC.js} +2 -2
  76. package/build/server/chunks/{5-BxVjs2qi.js.map → 5-D-Uv1voC.js.map} +1 -1
  77. package/build/server/chunks/{6-Cbf1AAMQ.js → 6-DP46cUej.js} +2 -2
  78. package/build/server/chunks/{6-Cbf1AAMQ.js.map → 6-DP46cUej.js.map} +1 -1
  79. package/build/server/chunks/{7-CMK2quEf.js → 7-B29_3ar6.js} +2 -2
  80. package/build/server/chunks/{7-CMK2quEf.js.map → 7-B29_3ar6.js.map} +1 -1
  81. package/build/server/chunks/{8-DhdfkfDM.js → 8-DCnSDVrX.js} +2 -2
  82. package/build/server/chunks/{8-DhdfkfDM.js.map → 8-DCnSDVrX.js.map} +1 -1
  83. package/build/server/chunks/{9-CPpxtRM5.js → 9-BwqDc8wC.js} +2 -2
  84. package/build/server/chunks/{9-CPpxtRM5.js.map → 9-BwqDc8wC.js.map} +1 -1
  85. package/build/server/chunks/_page.svelte-8OFzwdNA.js +758 -0
  86. package/build/server/chunks/_page.svelte-8OFzwdNA.js.map +1 -0
  87. package/build/server/chunks/{_server.ts-BWVlO8iV.js → _server.ts-05JJOdcX.js} +15 -12
  88. package/build/server/chunks/_server.ts-05JJOdcX.js.map +1 -0
  89. package/build/server/chunks/{_server.ts-BevnuePu.js → _server.ts-BCljU9Sg.js} +7 -3
  90. package/build/server/chunks/_server.ts-BCljU9Sg.js.map +1 -0
  91. package/build/server/chunks/{_server.ts-D-vgx5UZ.js → _server.ts-BTmknWpO.js} +2 -2
  92. package/build/server/chunks/{_server.ts-D-vgx5UZ.js.map → _server.ts-BTmknWpO.js.map} +1 -1
  93. package/build/server/chunks/{_server.ts-tChyh9FX.js → _server.ts-BXhmLZwN.js} +4 -2
  94. package/build/server/chunks/{_server.ts-tChyh9FX.js.map → _server.ts-BXhmLZwN.js.map} +1 -1
  95. package/build/server/chunks/{_server.ts-CvJKTS3Z.js → _server.ts-BbRSpB74.js} +4 -2
  96. package/build/server/chunks/{_server.ts-CvJKTS3Z.js.map → _server.ts-BbRSpB74.js.map} +1 -1
  97. package/build/server/chunks/{_server.ts-CC2K8-L2.js → _server.ts-Blx6TuRU.js} +4 -2
  98. package/build/server/chunks/_server.ts-Blx6TuRU.js.map +1 -0
  99. package/build/server/chunks/_server.ts-C6NRpe7e.js +33 -0
  100. package/build/server/chunks/_server.ts-C6NRpe7e.js.map +1 -0
  101. package/build/server/chunks/_server.ts-CGqCOCdK.js +53 -0
  102. package/build/server/chunks/_server.ts-CGqCOCdK.js.map +1 -0
  103. package/build/server/chunks/{_server.ts-X1R7L_QI.js → _server.ts-CYWXjihn.js} +4 -2
  104. package/build/server/chunks/{_server.ts-X1R7L_QI.js.map → _server.ts-CYWXjihn.js.map} +1 -1
  105. package/build/server/chunks/{_server.ts-CD7JP3fz.js → _server.ts-D0___krA.js} +4 -2
  106. package/build/server/chunks/_server.ts-D0___krA.js.map +1 -0
  107. package/build/server/chunks/{_server.ts-VzDcFFgy.js → _server.ts-DPHRUFYS.js} +4 -2
  108. package/build/server/chunks/_server.ts-DPHRUFYS.js.map +1 -0
  109. package/build/server/chunks/{_server.ts-D0zRDSx0.js → _server.ts-D_WRex0k.js} +4 -2
  110. package/build/server/chunks/_server.ts-D_WRex0k.js.map +1 -0
  111. package/build/server/chunks/{_server.ts-CA5KUENM.js → _server.ts-Da1kSClZ.js} +4 -2
  112. package/build/server/chunks/_server.ts-Da1kSClZ.js.map +1 -0
  113. package/build/server/chunks/{_server.ts-Dp-hXW_I.js → _server.ts-l3cd4Cto.js} +4 -2
  114. package/build/server/chunks/_server.ts-l3cd4Cto.js.map +1 -0
  115. package/build/server/chunks/{library-apns-Dl3iRE2h.js → library-apns-D8RPINlv.js} +62 -7
  116. package/build/server/chunks/library-apns-D8RPINlv.js.map +1 -0
  117. package/build/server/chunks/{pending-requests-C9p57WoU.js → pending-requests-8rWjrF6d.js} +3 -2
  118. package/build/server/chunks/pending-requests-8rWjrF6d.js.map +1 -0
  119. package/build/server/chunks/presence-store-Bx_g0-Gd.js +23 -0
  120. package/build/server/chunks/presence-store-Bx_g0-Gd.js.map +1 -0
  121. package/build/server/chunks/{pty-manager-ZqXqa-6A.js → pty-manager-DDjG7DlH.js} +297 -31
  122. package/build/server/chunks/pty-manager-DDjG7DlH.js.map +1 -0
  123. package/build/server/chunks/shooter-home-4f_HkdGI.js +10 -0
  124. package/build/server/chunks/shooter-home-4f_HkdGI.js.map +1 -0
  125. package/build/server/index.js +1 -1
  126. package/build/server/index.js.map +1 -1
  127. package/build/server/manifest.js +38 -24
  128. package/build/server/manifest.js.map +1 -1
  129. package/package.json +4 -2
  130. package/server.ts +2 -2
  131. package/src/lib/modules/client/common/index.ts +1 -0
  132. package/src/lib/modules/client/common/presence.ts +47 -0
  133. package/src/lib/modules/client/dashboard/AutopilotPanel.svelte +188 -4
  134. package/src/lib/modules/client/dashboard/autopilot-driver.svelte.ts +681 -0
  135. package/src/lib/modules/client/dashboard/decide-injection.ts +127 -0
  136. package/src/lib/modules/client/dashboard/store.svelte.ts +65 -24
  137. package/src/lib/modules/client/neurolink/fetch-proxy.ts +38 -1
  138. package/src/lib/modules/client/terminal/xterm-wrapper.ts +52 -12
  139. package/src/lib/modules/server/apn/apns-payload.ts +50 -0
  140. package/src/lib/modules/server/apn/library-apns.ts +50 -8
  141. package/src/lib/modules/server/apn/pending-requests.ts +3 -1
  142. package/src/lib/modules/server/sessions/autopilot-context.ts +57 -0
  143. package/src/lib/modules/server/sessions/autopilot-engine.ts +148 -43
  144. package/src/lib/modules/server/sessions/litellm-client.ts +90 -34
  145. package/src/lib/modules/server/sessions/next-step-consensus.ts +27 -2
  146. package/src/lib/modules/server/sessions/summary-store.ts +3 -1
  147. package/src/lib/modules/server/terminal/agent-launch.ts +26 -0
  148. package/src/lib/modules/server/terminal/pty-holder.cjs +6 -0
  149. package/src/lib/modules/server/terminal/pty-manager.ts +292 -38
  150. package/src/lib/modules/server/terminal/session-watcher.ts +12 -2
  151. package/src/lib/modules/server/terminal/terminal-emulator.ts +102 -0
  152. package/src/lib/modules/server/terminal/terminal-store.ts +3 -1
  153. package/src/lib/modules/server/utils/shooter-home.ts +16 -0
  154. package/src/lib/modules/server/ws/presence-store.ts +50 -0
  155. package/src/lib/modules/server/ws/server.ts +18 -2
  156. package/src/lib/modules/server/ws/terminal-handler.ts +11 -6
  157. package/src/lib/types/autopilot.ts +65 -0
  158. package/src/lib/types/generated/WsProtocol.ts +10 -1
  159. package/src/lib/types/server.ts +27 -1
  160. package/src/lib/types/terminal-client.ts +3 -0
  161. package/src/lib/types/ws.ts +3 -2
  162. package/src/routes/api/autopilot/goal/+server.ts +72 -0
  163. package/src/routes/api/neurolink-proxy/+server.ts +8 -2
  164. package/src/routes/api/notify/+server.ts +22 -15
  165. package/src/routes/api/presence/+server.ts +39 -0
  166. package/src/routes/api/ws-status/+server.ts +8 -5
  167. package/build/client/_app/immutable/assets/2.BHi6pjT2.css +0 -1
  168. package/build/client/_app/immutable/assets/2.BHi6pjT2.css.br +0 -0
  169. package/build/client/_app/immutable/assets/2.BHi6pjT2.css.gz +0 -0
  170. package/build/client/_app/immutable/chunks/BmfLecb1.js.br +0 -0
  171. package/build/client/_app/immutable/chunks/BmfLecb1.js.gz +0 -0
  172. package/build/client/_app/immutable/chunks/CRkG7oE4.js +0 -1
  173. package/build/client/_app/immutable/chunks/CRkG7oE4.js.br +0 -0
  174. package/build/client/_app/immutable/chunks/CRkG7oE4.js.gz +0 -0
  175. package/build/client/_app/immutable/chunks/DOEXXmsh.js.br +0 -0
  176. package/build/client/_app/immutable/chunks/DOEXXmsh.js.gz +0 -0
  177. package/build/client/_app/immutable/chunks/EqMAkEha.js.br +0 -0
  178. package/build/client/_app/immutable/chunks/EqMAkEha.js.gz +0 -0
  179. package/build/client/_app/immutable/chunks/J5-Cr5oR.js +0 -6
  180. package/build/client/_app/immutable/chunks/J5-Cr5oR.js.br +0 -0
  181. package/build/client/_app/immutable/chunks/J5-Cr5oR.js.gz +0 -0
  182. package/build/client/_app/immutable/entry/app.CeSxgGat.js.br +0 -0
  183. package/build/client/_app/immutable/entry/app.CeSxgGat.js.gz +0 -0
  184. package/build/client/_app/immutable/entry/start.DrnJFwxA.js +0 -1
  185. package/build/client/_app/immutable/entry/start.DrnJFwxA.js.br +0 -2
  186. package/build/client/_app/immutable/entry/start.DrnJFwxA.js.gz +0 -0
  187. package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.br +0 -0
  188. package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.gz +0 -0
  189. package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.br +0 -0
  190. package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.gz +0 -0
  191. package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.br +0 -0
  192. package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.gz +0 -0
  193. package/build/client/_app/immutable/nodes/11.CKmZjP_a.js.br +0 -0
  194. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js +0 -13
  195. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.br +0 -0
  196. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.gz +0 -0
  197. package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.br +0 -0
  198. package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.gz +0 -0
  199. package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.br +0 -0
  200. package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.gz +0 -0
  201. package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.br +0 -0
  202. package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.gz +0 -0
  203. package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.br +0 -0
  204. package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.gz +0 -0
  205. package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.br +0 -0
  206. package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.gz +0 -0
  207. package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.br +0 -0
  208. package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.gz +0 -0
  209. package/build/server/chunks/_page.svelte-tBuIq8Pg.js +0 -159
  210. package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +0 -1
  211. package/build/server/chunks/_server.ts-BWVlO8iV.js.map +0 -1
  212. package/build/server/chunks/_server.ts-BevnuePu.js.map +0 -1
  213. package/build/server/chunks/_server.ts-CA5KUENM.js.map +0 -1
  214. package/build/server/chunks/_server.ts-CC2K8-L2.js.map +0 -1
  215. package/build/server/chunks/_server.ts-CD7JP3fz.js.map +0 -1
  216. package/build/server/chunks/_server.ts-D0zRDSx0.js.map +0 -1
  217. package/build/server/chunks/_server.ts-Dp-hXW_I.js.map +0 -1
  218. package/build/server/chunks/_server.ts-VzDcFFgy.js.map +0 -1
  219. package/build/server/chunks/library-apns-Dl3iRE2h.js.map +0 -1
  220. package/build/server/chunks/pending-requests-C9p57WoU.js.map +0 -1
  221. package/build/server/chunks/pty-manager-ZqXqa-6A.js.map +0 -1
@@ -0,0 +1,681 @@
1
+ // The phone-resident autonomous loop (runs in the WebView / browser).
2
+ //
3
+ // Watches /ws/events + GET /api/summaries to track per-terminal state, and when a
4
+ // Shooter-managed terminal goes idle with a high-confidence consensus next step, it
5
+ // produces a concrete command, vets it (guardCommand), and injects it into the PTY
6
+ // over /ws/terminal/:id — the same transport humans use. AUTO-INJECT mode.
7
+ //
8
+ // The decision logic lives in the pure, unit-tested decide-injection.ts. This module
9
+ // is the I/O shell: sockets, polling, per-terminal bookkeeping, the kill switch.
10
+ //
11
+ // See docs/superpowers/specs/2026-06-01-phone-autonomous-agent-design.md.
12
+
13
+ import type {
14
+ ConsensusResult,
15
+ DriverAction,
16
+ DriverActionKind,
17
+ InjectionState,
18
+ NextStep,
19
+ RawEvent,
20
+ SessionSummaryRecord,
21
+ } from '$lib/types';
22
+
23
+ import { decideInjection, guardCommand, normalizeStep } from './decide-injection';
24
+
25
+ const AUTONOMY_KEY = 'shooter_autonomy';
26
+ const POLL_INTERVAL_MS = 6_000;
27
+ const RECONNECT_MS = 3_000;
28
+ const OPEN_TIMEOUT_MS = 5_000;
29
+ const MAX_ACTIONS = 40;
30
+ // When the WS missed the transition to idle (reconnect, or the agent was already idle when the
31
+ // dashboard opened), the poll path may synthesise an agent-idle from a recent idle-triggered
32
+ // summary — but ONLY if that summary is this fresh, so we never auto-act on stale parked state.
33
+ const IDLE_RESUME_WINDOW_MS = 120_000;
34
+ // Gap between the prompt text and the Enter key for agent TUIs (see inject()). A single chunk
35
+ // ending in CR is treated as a paste — the CR becomes a newline, not a submit — so the Enter must
36
+ // arrive as a SEPARATE write a beat later.
37
+ const AGENT_SUBMIT_DELAY_MS = 120;
38
+ // Allowed shape for a terminal id before it is interpolated into a socket URL (hex / UUID-like).
39
+ const SAFE_ID = /^[A-Za-z0-9_-]+$/;
40
+
41
+ /** Command heads we are willing to treat the next-step text as a literal command for. */
42
+ const SAFE_COMMAND_HEADS = new Set([
43
+ 'bun',
44
+ 'cargo',
45
+ 'cat',
46
+ 'deno',
47
+ 'echo',
48
+ 'eslint',
49
+ 'git',
50
+ 'go',
51
+ 'jest',
52
+ 'ls',
53
+ 'make',
54
+ 'node',
55
+ 'npm',
56
+ 'npx',
57
+ 'pnpm',
58
+ 'prettier',
59
+ 'pwd',
60
+ 'python',
61
+ 'python3',
62
+ 'tsc',
63
+ 'vitest',
64
+ 'yarn',
65
+ ]);
66
+
67
+ /** Terminal commands that are AI agents — inject the next-step as a PROMPT, not a shell command. */
68
+ const AGENT_COMMANDS = new Set(['claude', 'opencode']);
69
+
70
+ // eslint-disable-next-line no-restricted-syntax -- internal DI seam, never exported
71
+ interface AutopilotDriverDeps {
72
+ now: () => number;
73
+ produceCommand: (input: ProduceCommandInput) => Promise<null | string>;
74
+ }
75
+
76
+ // eslint-disable-next-line no-restricted-syntax -- internal
77
+ interface ProduceCommandInput {
78
+ apiKey: string;
79
+ isAgentTerminal: boolean;
80
+ recentOutput: string;
81
+ step: NextStep;
82
+ terminalId: string;
83
+ }
84
+
85
+ // eslint-disable-next-line no-restricted-syntax -- internal per-terminal bookkeeping
86
+ interface TerminalRuntime {
87
+ autoActionCount: number;
88
+ busy: boolean;
89
+ command: string;
90
+ consensus: ConsensusResult | null;
91
+ injectSocket: null | WebSocket;
92
+ isManaged: boolean;
93
+ lastActedStep: null | string;
94
+ lastActivityAt: number;
95
+ lastCommand: null | string;
96
+ lastEventAt: number;
97
+ lastEventType: string;
98
+ lastInjectedAt: number;
99
+ recentOutput: string;
100
+ }
101
+
102
+ /**
103
+ * The autonomous driver. One singleton per page; the AutopilotPanel starts/stops it and
104
+ * reads `enabled` + `actions` reactively.
105
+ */
106
+ export class AutopilotDriver {
107
+ actions = $state<DriverAction[]>([]);
108
+ enabled = $state(false);
109
+
110
+ private apiKey = '';
111
+ private deps: AutopilotDriverDeps;
112
+ private eventsWs: null | WebSocket = null;
113
+ private pollTimer: null | ReturnType<typeof setInterval> = null;
114
+ private reconnectTimer: null | ReturnType<typeof setTimeout> = null;
115
+ private started = false;
116
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- internal, non-reactive bookkeeping
117
+ private terminals = new Map<string, TerminalRuntime>();
118
+
119
+ constructor(deps?: Partial<AutopilotDriverDeps>) {
120
+ this.deps = {
121
+ now: deps?.now ?? ((): number => Date.now()),
122
+ produceCommand: deps?.produceCommand ?? defaultProduceCommand,
123
+ };
124
+ this.enabled = readPersistedAutonomy();
125
+ }
126
+
127
+ setEnabled(value: boolean): void {
128
+ this.enabled = value;
129
+ try {
130
+ localStorage.setItem(AUTONOMY_KEY, JSON.stringify({ enabled: value }));
131
+ } catch {
132
+ // best-effort
133
+ }
134
+ }
135
+
136
+ start(apiKey: string): void {
137
+ this.apiKey = apiKey;
138
+ if (this.started) {
139
+ return;
140
+ }
141
+ this.started = true;
142
+ // Native silent-push wake (iOS): the AppDelegate dispatches 'shooter:wake' so the loop
143
+ // runs a burst when woken in the background.
144
+ if (typeof window !== 'undefined') {
145
+ window.addEventListener('shooter:wake', this.onWake);
146
+ }
147
+ void this.connectEvents();
148
+ void this.refresh();
149
+ this.pollTimer = setInterval(() => void this.refresh(), POLL_INTERVAL_MS);
150
+ }
151
+
152
+ stop(): void {
153
+ this.started = false;
154
+ if (typeof window !== 'undefined') {
155
+ window.removeEventListener('shooter:wake', this.onWake);
156
+ }
157
+ if (this.pollTimer) {
158
+ clearInterval(this.pollTimer);
159
+ this.pollTimer = null;
160
+ }
161
+ if (this.reconnectTimer) {
162
+ clearTimeout(this.reconnectTimer);
163
+ this.reconnectTimer = null;
164
+ }
165
+ if (this.eventsWs) {
166
+ this.eventsWs.onclose = null;
167
+ this.eventsWs.close();
168
+ this.eventsWs = null;
169
+ }
170
+ for (const rt of this.terminals.values()) {
171
+ rt.injectSocket?.close();
172
+ rt.injectSocket = null;
173
+ }
174
+ }
175
+
176
+ private async connectEvents(): Promise<void> {
177
+ const ticket = await this.getTicket();
178
+ if (!ticket || !this.started) {
179
+ return;
180
+ }
181
+ const wsBase = window.location.origin.replace(/^http/, 'ws');
182
+ const ws = new WebSocket(`${wsBase}/ws/events?ticket=${ticket}`);
183
+ this.eventsWs = ws;
184
+ ws.onmessage = (msg: MessageEvent): void => {
185
+ try {
186
+ const raw: unknown = JSON.parse(msg.data as string);
187
+ if (raw && typeof raw === 'object') {
188
+ this.handleEvent(raw as RawEvent);
189
+ }
190
+ } catch {
191
+ // ignore malformed
192
+ }
193
+ };
194
+ ws.onclose = (): void => {
195
+ this.eventsWs = null;
196
+ if (this.started) {
197
+ this.scheduleReconnect();
198
+ }
199
+ };
200
+ ws.onerror = (): void => {
201
+ // close handler does the reconnect
202
+ };
203
+ }
204
+
205
+ private createRuntime(terminalId: string): TerminalRuntime {
206
+ const rt: TerminalRuntime = {
207
+ autoActionCount: 0,
208
+ busy: false,
209
+ command: '',
210
+ consensus: null,
211
+ injectSocket: null,
212
+ isManaged: false,
213
+ lastActedStep: null,
214
+ lastActivityAt: 0,
215
+ lastCommand: null,
216
+ lastEventAt: 0,
217
+ lastEventType: '',
218
+ lastInjectedAt: 0,
219
+ recentOutput: '',
220
+ };
221
+ this.terminals.set(terminalId, rt);
222
+ return rt;
223
+ }
224
+
225
+ private async evaluate(terminalId: string): Promise<void> {
226
+ if (!this.enabled) {
227
+ return;
228
+ }
229
+ const rt = this.terminals.get(terminalId);
230
+ if (!rt || rt.busy || !rt.consensus) {
231
+ return;
232
+ }
233
+ const state: InjectionState = {
234
+ autoActionCount: rt.autoActionCount,
235
+ isManaged: rt.isManaged,
236
+ lastActedStep: rt.lastActedStep,
237
+ lastActivityAt: rt.lastActivityAt,
238
+ lastEventType: rt.lastEventType,
239
+ lastInjectedAt: rt.lastInjectedAt,
240
+ terminalId,
241
+ };
242
+ // Agent terminals (claude/opencode) receive the next step as a PROMPT they reason about, so we
243
+ // allow a best-ranked-but-tentative step through (keeps the autonomous loop live past step 1).
244
+ // Shell terminals stay strict — there the injected text is a command run verbatim.
245
+ const decision = decideInjection(state, rt.consensus, this.deps.now(), undefined, {
246
+ allowTentative: isAgentCommand(rt.command),
247
+ });
248
+ if (!decision.act || !decision.step) {
249
+ return; // common case — stay quiet
250
+ }
251
+
252
+ rt.busy = true;
253
+ try {
254
+ const isAgent = isAgentCommand(rt.command);
255
+ let command: null | string;
256
+ try {
257
+ command = await this.deps.produceCommand({
258
+ apiKey: this.apiKey,
259
+ isAgentTerminal: isAgent,
260
+ recentOutput: rt.recentOutput,
261
+ step: decision.step,
262
+ terminalId,
263
+ });
264
+ } catch {
265
+ command = null;
266
+ }
267
+
268
+ if (!command) {
269
+ // Transient producer failure (e.g. the LLM proxy was busy) — do NOT mark the step acted, so
270
+ // it stays retryable on the next poll instead of being silenced forever.
271
+ this.log(terminalId, 'skipped', `no command for: ${decision.step.text.slice(0, 60)}`);
272
+ return;
273
+ }
274
+ // For agent TUIs the next-step is a natural-language prompt that inject() collapses to a single
275
+ // line; collapse it HERE too, before guardCommand, so a legitimate multi-line prompt isn't
276
+ // rejected as "multi-line" by a guard the injected form would have passed anyway.
277
+ const candidate = isAgent ? command.replace(/\s*[\r\n]+\s*/g, ' ').trim() : command;
278
+ const verdict = guardCommand(candidate, rt.lastCommand);
279
+ if (!verdict.safe) {
280
+ // Deterministic rejection of THIS step text — mark it acted so we don't loop on it.
281
+ rt.lastActedStep = normalizeStep(decision.step.text);
282
+ this.log(terminalId, 'skipped', `guard: ${verdict.reason}`);
283
+ return;
284
+ }
285
+ const ok = await this.inject(terminalId, verdict.command);
286
+ if (ok) {
287
+ rt.lastInjectedAt = this.deps.now();
288
+ rt.lastCommand = verdict.command;
289
+ rt.lastActedStep = normalizeStep(decision.step.text); // mark acted ONLY on a real inject
290
+ rt.autoActionCount += 1;
291
+ this.log(terminalId, 'injected', verdict.command);
292
+ } else {
293
+ // Transient inject failure (socket) — leave the step unmarked so the next poll retries.
294
+ this.log(terminalId, 'error', `inject failed: ${verdict.command}`);
295
+ }
296
+ } finally {
297
+ rt.busy = false;
298
+ }
299
+ }
300
+
301
+ private async fetchSummaries(): Promise<void> {
302
+ try {
303
+ const res = await fetch('/api/summaries?limit=30', {
304
+ headers: { Authorization: `Bearer ${this.apiKey}` },
305
+ });
306
+ if (!res.ok) {
307
+ return;
308
+ }
309
+ const body = (await res.json()) as { summaries?: SessionSummaryRecord[] };
310
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- local, non-reactive dedup
311
+ const latest = new Set<string>();
312
+ for (const rec of body.summaries ?? []) {
313
+ const tid = rec.terminalId;
314
+ if (!tid || latest.has(tid)) {
315
+ continue; // records are newest-first; keep only the latest per terminal
316
+ }
317
+ const rt = this.terminals.get(tid);
318
+ if (!rt) {
319
+ continue;
320
+ }
321
+ latest.add(tid);
322
+ rt.consensus = { agentCount: 5, quorum: 3, steps: parseSteps(rec.nextSteps) };
323
+ // Resume the inject path when the WS missed the transition to idle: if this is a recent
324
+ // agent-idle-triggered summary and nothing newer arrived over the WS, treat the terminal as
325
+ // idle so the poll loop can act (decideInjection hard-requires lastEventType==='agent-idle',
326
+ // which only the WS path otherwise sets — leaving the poll path dead after a reconnect).
327
+ const createdMs = Date.parse(rec.createdAt);
328
+ if (
329
+ rec.trigger === 'agent-idle' &&
330
+ Number.isFinite(createdMs) &&
331
+ createdMs >= rt.lastEventAt &&
332
+ this.deps.now() - createdMs < IDLE_RESUME_WINDOW_MS
333
+ ) {
334
+ rt.lastEventType = 'agent-idle';
335
+ }
336
+ }
337
+ } catch {
338
+ // silent — retry next poll
339
+ }
340
+ }
341
+
342
+ private async fetchTerminals(): Promise<void> {
343
+ try {
344
+ const res = await fetch('/api/terminals', {
345
+ headers: { Authorization: `Bearer ${this.apiKey}` },
346
+ });
347
+ if (!res.ok) {
348
+ return;
349
+ }
350
+ const body = (await res.json()) as {
351
+ terminals?: { command: string; exitedAt: null | string; id: string; status: string }[];
352
+ };
353
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- local, non-reactive dedup
354
+ const seen = new Set<string>();
355
+ for (const t of body.terminals ?? []) {
356
+ if (t.exitedAt !== null || t.status === 'exited') {
357
+ continue; // only track live terminals
358
+ }
359
+ seen.add(t.id);
360
+ const rt = this.terminals.get(t.id) ?? this.createRuntime(t.id);
361
+ rt.isManaged = true; // present in GET /api/terminals ⇒ Shooter-managed
362
+ rt.command = t.command ?? '';
363
+ }
364
+ for (const id of [...this.terminals.keys()]) {
365
+ if (!seen.has(id)) {
366
+ this.terminals.get(id)?.injectSocket?.close();
367
+ this.terminals.delete(id);
368
+ }
369
+ }
370
+ } catch {
371
+ // silent — retry next poll
372
+ }
373
+ }
374
+
375
+ private async getTicket(): Promise<null | string> {
376
+ try {
377
+ const res = await fetch('/api/ws-ticket', {
378
+ headers: { Authorization: `Bearer ${this.apiKey}` },
379
+ method: 'POST',
380
+ });
381
+ if (!res.ok) {
382
+ return null;
383
+ }
384
+ const { ticket } = (await res.json()) as { ticket: string };
385
+ return ticket;
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+
391
+ private handleEvent(raw: RawEvent): void {
392
+ const type = typeof raw.type === 'string' ? raw.type : '';
393
+ const terminalId = typeof raw.terminalId === 'string' ? raw.terminalId : '';
394
+ if (!type || type === 'welcome' || !terminalId) {
395
+ return;
396
+ }
397
+ const rt = this.terminals.get(terminalId);
398
+ if (!rt) {
399
+ return; // unknown terminal until the next /api/terminals poll
400
+ }
401
+ rt.lastEventType = type;
402
+ rt.lastEventAt = this.deps.now(); // so a later summary can't downgrade a fresher live signal
403
+ if (type !== 'agent-idle' && type !== 'agent-question') {
404
+ rt.lastActivityAt = this.deps.now(); // tool/human activity → resets the grace window
405
+ }
406
+ if (type === 'tool-completed' && raw.success === true) {
407
+ rt.autoActionCount = 0; // real progress resets the circuit breaker
408
+ }
409
+ if (type === 'agent-idle' && this.enabled) {
410
+ void this.evaluate(terminalId);
411
+ }
412
+ }
413
+
414
+ private async inject(terminalId: string, command: string): Promise<boolean> {
415
+ const rt = this.terminals.get(terminalId);
416
+ if (!rt) {
417
+ return false;
418
+ }
419
+ // Defense-in-depth: terminalId is a server-issued id, but it reaches this socket-URL sink from
420
+ // the events WebSocket too — constrain it to a safe charset (and URL-encode both it and the
421
+ // ticket) so a malformed/hostile value can't alter the request path or target
422
+ // (CodeQL js/request-forgery). Real ids are short hex / UUID-like.
423
+ if (!SAFE_ID.test(terminalId)) {
424
+ return false;
425
+ }
426
+ let ws = rt.injectSocket;
427
+ if (!ws || ws.readyState > WebSocket.OPEN) {
428
+ const ticket = await this.getTicket();
429
+ if (!ticket) {
430
+ return false;
431
+ }
432
+ const wsBase = window.location.origin.replace(/^http/, 'ws');
433
+ ws = new WebSocket(
434
+ `${wsBase}/ws/terminal/${encodeURIComponent(terminalId)}?ticket=${encodeURIComponent(ticket)}`
435
+ );
436
+ rt.injectSocket = ws;
437
+ const opened = await waitForOpen(ws);
438
+ if (!opened) {
439
+ rt.injectSocket = null; // drop the failed socket so the next attempt reconnects cleanly
440
+ return false;
441
+ }
442
+ }
443
+ if (ws.readyState !== WebSocket.OPEN) {
444
+ return false;
445
+ }
446
+ if (isAgentCommand(rt.command)) {
447
+ // Agent TUIs (claude/opencode) treat a single chunk ending in CR as a bracketed paste — the
448
+ // CR becomes a literal newline, not a submit. Send the prompt text and the Enter as SEPARATE
449
+ // writes, collapsing any newlines so a multi-line step can't submit halfway through.
450
+ const prompt = command.replace(/\s*[\r\n]+\s*/g, ' ').trim();
451
+ if (!prompt) {
452
+ return false;
453
+ }
454
+ ws.send(JSON.stringify({ data: prompt, type: 'input' }));
455
+ await delay(AGENT_SUBMIT_DELAY_MS);
456
+ if (ws.readyState !== WebSocket.OPEN) {
457
+ return false;
458
+ }
459
+ ws.send(JSON.stringify({ data: '\r', type: 'input' }));
460
+ return true;
461
+ }
462
+ ws.send(JSON.stringify({ data: `${command}\r`, type: 'input' }));
463
+ return true;
464
+ }
465
+
466
+ private log(terminalId: string, kind: DriverActionKind, detail: string): void {
467
+ this.actions = [{ at: this.deps.now(), detail, kind, terminalId }, ...this.actions].slice(
468
+ 0,
469
+ MAX_ACTIONS
470
+ );
471
+ }
472
+
473
+ private onWake = (): void => {
474
+ void this.refresh();
475
+ };
476
+
477
+ private async refresh(): Promise<void> {
478
+ if (!this.apiKey) {
479
+ return;
480
+ }
481
+ await Promise.allSettled([this.fetchTerminals(), this.fetchSummaries()]);
482
+ if (this.enabled) {
483
+ for (const id of this.terminals.keys()) {
484
+ void this.evaluate(id);
485
+ }
486
+ }
487
+ }
488
+
489
+ private scheduleReconnect(): void {
490
+ if (this.reconnectTimer) {
491
+ return;
492
+ }
493
+ this.reconnectTimer = setTimeout(() => {
494
+ this.reconnectTimer = null;
495
+ void this.connectEvents();
496
+ }, RECONNECT_MS);
497
+ }
498
+ }
499
+
500
+ async function defaultProduceCommand(input: ProduceCommandInput): Promise<null | string> {
501
+ // Agent terminals (claude/opencode) take a natural-language PROMPT, not a shell command —
502
+ // the consensus next-step is already that prompt, so inject it directly.
503
+ if (input.isAgentTerminal) {
504
+ const prompt = input.step.text.trim();
505
+ return prompt.length > 0 ? prompt : null;
506
+ }
507
+ // Shell terminals: translate the next-step into a concrete command.
508
+ // 1) On-device model via the native bridge (iOS Foundation Models), when present.
509
+ const native = await tryNativeDecide(input);
510
+ if (native) {
511
+ return native;
512
+ }
513
+ // 2) LiteLLM via the server proxy (key stays server-side) — a real decide step in the browser.
514
+ const litellm = await litellmProduceCommand(input);
515
+ if (litellm) {
516
+ return litellm;
517
+ }
518
+ // 3) Heuristic fallback (no LLM): only when the step text is already a literal command.
519
+ const text = input.step.text.trim();
520
+ const backticked = /`([^`]+)`/.exec(text);
521
+ if (backticked) {
522
+ return backticked[1].trim();
523
+ }
524
+ const head = text.split(/\s+/)[0]?.toLowerCase();
525
+ if (head && SAFE_COMMAND_HEADS.has(head) && !/[\r\n]/.test(text) && text.length < 80) {
526
+ return text;
527
+ }
528
+ return null;
529
+ }
530
+
531
+ function delay(ms: number): Promise<void> {
532
+ return new Promise((resolve) => {
533
+ setTimeout(resolve, ms);
534
+ });
535
+ }
536
+
537
+ /**
538
+ * Default command producer: a CONSERVATIVE heuristic with no LLM. It only yields a
539
+ * command when the next-step text is already a literal command (backticked, or a bare
540
+ * command starting with a known head). Prose next-steps return null → push only, never
541
+ * an inject of garbage. The LLM-backed producer (LiteLLM / on-device) replaces this.
542
+ */
543
+ function isAgentCommand(command: string): boolean {
544
+ const firstToken = command.trim().split(/\s+/)[0] ?? '';
545
+ const base = firstToken.split('/').pop() ?? '';
546
+ return AGENT_COMMANDS.has(base);
547
+ }
548
+
549
+ async function litellmProduceCommand(input: ProduceCommandInput): Promise<null | string> {
550
+ const base = readProcessEnv('LITELLM_BASE_URL');
551
+ if (!base || !input.apiKey) {
552
+ return null;
553
+ }
554
+ const model = readProcessEnv('LITELLM_MODEL') || 'open-large';
555
+ const userPrompt =
556
+ `Recent terminal output:\n${input.recentOutput.slice(-2000)}\n\n` +
557
+ `Suggested next step: ${input.step.text}\n\n` +
558
+ 'Reply with ONLY the single shell command to run next — no prose, no backticks, no explanation.';
559
+ try {
560
+ const res = await fetch('/api/neurolink-proxy', {
561
+ body: JSON.stringify({
562
+ body: {
563
+ max_tokens: 60,
564
+ messages: [
565
+ {
566
+ content: 'You are a coding-session copilot. Output ONLY the next shell command.',
567
+ role: 'system',
568
+ },
569
+ { content: userPrompt, role: 'user' },
570
+ ],
571
+ model,
572
+ temperature: 0,
573
+ },
574
+ headers: {},
575
+ provider: 'litellm',
576
+ url: `${base}/chat/completions`,
577
+ }),
578
+ headers: { Authorization: `Bearer ${input.apiKey}`, 'Content-Type': 'application/json' },
579
+ method: 'POST',
580
+ });
581
+ if (!res.ok) {
582
+ return null;
583
+ }
584
+ const data = (await res.json()) as { choices?: { message?: { content?: string } }[] };
585
+ const content = data.choices?.[0]?.message?.content;
586
+ if (typeof content !== 'string') {
587
+ return null;
588
+ }
589
+ const cmd = content
590
+ .trim()
591
+ .replace(/^`+|`+$/g, '')
592
+ .trim();
593
+ return cmd.length > 0 ? cmd : null;
594
+ } catch {
595
+ return null;
596
+ }
597
+ }
598
+
599
+ function parseSteps(json: string): NextStep[] {
600
+ try {
601
+ const parsed: unknown = JSON.parse(json);
602
+ if (!Array.isArray(parsed)) {
603
+ return [];
604
+ }
605
+ // Validate shape — a malformed/old record must not crash the loop downstream.
606
+ return parsed.filter(
607
+ (el): el is NextStep =>
608
+ typeof el === 'object' &&
609
+ el !== null &&
610
+ typeof (el as NextStep).text === 'string' &&
611
+ typeof (el as NextStep).confidence === 'number'
612
+ );
613
+ } catch {
614
+ return [];
615
+ }
616
+ }
617
+
618
+ function readPersistedAutonomy(): boolean {
619
+ if (typeof localStorage === 'undefined') {
620
+ return false;
621
+ }
622
+ try {
623
+ const raw = localStorage.getItem(AUTONOMY_KEY);
624
+ if (!raw) {
625
+ return false;
626
+ }
627
+ const parsed: unknown = JSON.parse(raw);
628
+ return Boolean((parsed as { enabled?: unknown })?.enabled);
629
+ } catch {
630
+ return false;
631
+ }
632
+ }
633
+
634
+ function readProcessEnv(key: string): string {
635
+ // eslint-disable-next-line @typescript-eslint/dot-notation -- bracket access avoids the bundler constant-folding process.env to its build-time value
636
+ const proc = (window as unknown as Record<string, unknown>)['process'] as
637
+ | undefined
638
+ | { env?: Record<string, string | undefined> };
639
+ const value = proc?.env?.[key];
640
+ return typeof value === 'string' ? value : '';
641
+ }
642
+
643
+ function tryNativeDecide(input: ProduceCommandInput): Promise<null | string> {
644
+ const bridge = (
645
+ window as unknown as { ShooterBridge?: { agentDecide?: (ctx: string) => Promise<string> } }
646
+ ).ShooterBridge;
647
+ if (typeof bridge?.agentDecide !== 'function') {
648
+ return Promise.resolve(null);
649
+ }
650
+ const ctx =
651
+ `Recent terminal output:\n${input.recentOutput.slice(-2000)}\n\n` +
652
+ `Suggested next step: ${input.step.text}\n\nReply with the single shell command to run next.`;
653
+ return bridge
654
+ .agentDecide(ctx)
655
+ .then((c) => (typeof c === 'string' && c.trim().length > 0 ? c.trim() : null))
656
+ .catch(() => null);
657
+ }
658
+
659
+ function waitForOpen(ws: WebSocket): Promise<boolean> {
660
+ if (ws.readyState === WebSocket.OPEN) {
661
+ return Promise.resolve(true);
662
+ }
663
+ return new Promise((resolve) => {
664
+ const timer = setTimeout(() => {
665
+ ws.close(); // don't leak a stuck CONNECTING socket (review finding)
666
+ resolve(false);
667
+ }, OPEN_TIMEOUT_MS);
668
+ ws.addEventListener('open', () => {
669
+ clearTimeout(timer);
670
+ resolve(true);
671
+ });
672
+ ws.addEventListener('error', () => {
673
+ clearTimeout(timer);
674
+ ws.close();
675
+ resolve(false);
676
+ });
677
+ });
678
+ }
679
+
680
+ /** Page-level singleton used by the AutopilotPanel. */
681
+ export const autopilotDriver = new AutopilotDriver();