@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.
- package/.claude/hooks/notifier.cjs +94 -1
- package/build/client/_app/immutable/assets/2.JWRrnR-w.css +1 -0
- package/build/client/_app/immutable/assets/2.JWRrnR-w.css.br +0 -0
- package/build/client/_app/immutable/assets/2.JWRrnR-w.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{DOEXXmsh.js → Bj5wFimK.js} +2 -2
- package/build/client/_app/immutable/chunks/Bj5wFimK.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bj5wFimK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{EqMAkEha.js → BjYr_-Ss.js} +1 -1
- package/build/client/_app/immutable/chunks/BjYr_-Ss.js.br +0 -0
- package/build/client/_app/immutable/chunks/BjYr_-Ss.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C4Hns_Wl.js +1 -0
- package/build/client/_app/immutable/chunks/C4Hns_Wl.js.br +0 -0
- package/build/client/_app/immutable/chunks/C4Hns_Wl.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DULfdsh6.js +6 -0
- package/build/client/_app/immutable/chunks/DULfdsh6.js.br +0 -0
- package/build/client/_app/immutable/chunks/DULfdsh6.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BmfLecb1.js → fcNfTA-E.js} +1 -1
- package/build/client/_app/immutable/chunks/fcNfTA-E.js.br +0 -0
- package/build/client/_app/immutable/chunks/fcNfTA-E.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CeSxgGat.js → app.Bvoqymnp.js} +2 -2
- package/build/client/_app/immutable/entry/app.Bvoqymnp.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Bvoqymnp.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BqXCPPZJ.js +1 -0
- package/build/client/_app/immutable/entry/start.BqXCPPZJ.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BqXCPPZJ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.oaPwxh1O.js → 0.Bv_TwEnq.js} +1 -1
- package/build/client/_app/immutable/nodes/0.Bv_TwEnq.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Bv_TwEnq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.DMPyoM-M.js → 1.7lffTIeb.js} +1 -1
- package/build/client/_app/immutable/nodes/1.7lffTIeb.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.7lffTIeb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{10.Cbm7nQKK.js → 10.ChiIrIDl.js} +1 -1
- package/build/client/_app/immutable/nodes/10.ChiIrIDl.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.ChiIrIDl.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js → 11.DO3vyXEv.js} +2 -2
- package/build/client/_app/immutable/nodes/11.DO3vyXEv.js.br +0 -0
- package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js.gz → 11.DO3vyXEv.js.gz} +0 -0
- package/build/client/_app/immutable/nodes/2.iMIqsE7n.js +23 -0
- package/build/client/_app/immutable/nodes/2.iMIqsE7n.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.iMIqsE7n.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.BgLpGnzb.js → 3.CArnSHOO.js} +1 -1
- package/build/client/_app/immutable/nodes/3.CArnSHOO.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CArnSHOO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.Avc1-gVb.js → 5.DziEu9rx.js} +1 -1
- package/build/client/_app/immutable/nodes/5.DziEu9rx.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.DziEu9rx.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.Dw2wEssJ.js → 6.B8l1RwkB.js} +1 -1
- package/build/client/_app/immutable/nodes/6.B8l1RwkB.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B8l1RwkB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.DwKZjoBg.js → 7.BPyfhDis.js} +1 -1
- package/build/client/_app/immutable/nodes/7.BPyfhDis.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.BPyfhDis.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.ZUAI6g5E.js → 8.D_vszZ9E.js} +1 -1
- package/build/client/_app/immutable/nodes/8.D_vszZ9E.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.D_vszZ9E.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.I_KGXPwB.js → 9.Drah-do-.js} +1 -1
- package/build/client/_app/immutable/nodes/9.Drah-do-.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.Drah-do-.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/pty-holder.cjs +6 -0
- package/build/server/chunks/{0-vrTNAfZB.js → 0-DAB_6Vm1.js} +2 -2
- package/build/server/chunks/{0-vrTNAfZB.js.map → 0-DAB_6Vm1.js.map} +1 -1
- package/build/server/chunks/{1-nbr-bOoF.js → 1-D-qMYaCx.js} +2 -2
- package/build/server/chunks/{1-nbr-bOoF.js.map → 1-D-qMYaCx.js.map} +1 -1
- package/build/server/chunks/{10-ChyvvJ6w.js → 10-CeFFGo-X.js} +2 -2
- package/build/server/chunks/{10-ChyvvJ6w.js.map → 10-CeFFGo-X.js.map} +1 -1
- package/build/server/chunks/{11-6ZAjL3uU.js → 11-DRMu_ATU.js} +2 -2
- package/build/server/chunks/{11-6ZAjL3uU.js.map → 11-DRMu_ATU.js.map} +1 -1
- package/build/server/chunks/{2-DWFRVDWJ.js → 2-B7OLBMNH.js} +4 -4
- package/build/server/chunks/{2-DWFRVDWJ.js.map → 2-B7OLBMNH.js.map} +1 -1
- package/build/server/chunks/{3-CKANM_WM.js → 3-B38ZarLw.js} +2 -2
- package/build/server/chunks/{3-CKANM_WM.js.map → 3-B38ZarLw.js.map} +1 -1
- package/build/server/chunks/{5-BxVjs2qi.js → 5-D-Uv1voC.js} +2 -2
- package/build/server/chunks/{5-BxVjs2qi.js.map → 5-D-Uv1voC.js.map} +1 -1
- package/build/server/chunks/{6-Cbf1AAMQ.js → 6-DP46cUej.js} +2 -2
- package/build/server/chunks/{6-Cbf1AAMQ.js.map → 6-DP46cUej.js.map} +1 -1
- package/build/server/chunks/{7-CMK2quEf.js → 7-B29_3ar6.js} +2 -2
- package/build/server/chunks/{7-CMK2quEf.js.map → 7-B29_3ar6.js.map} +1 -1
- package/build/server/chunks/{8-DhdfkfDM.js → 8-DCnSDVrX.js} +2 -2
- package/build/server/chunks/{8-DhdfkfDM.js.map → 8-DCnSDVrX.js.map} +1 -1
- package/build/server/chunks/{9-CPpxtRM5.js → 9-BwqDc8wC.js} +2 -2
- package/build/server/chunks/{9-CPpxtRM5.js.map → 9-BwqDc8wC.js.map} +1 -1
- package/build/server/chunks/_page.svelte-8OFzwdNA.js +758 -0
- package/build/server/chunks/_page.svelte-8OFzwdNA.js.map +1 -0
- package/build/server/chunks/{_server.ts-BWVlO8iV.js → _server.ts-05JJOdcX.js} +15 -12
- package/build/server/chunks/_server.ts-05JJOdcX.js.map +1 -0
- package/build/server/chunks/{_server.ts-BevnuePu.js → _server.ts-BCljU9Sg.js} +7 -3
- package/build/server/chunks/_server.ts-BCljU9Sg.js.map +1 -0
- package/build/server/chunks/{_server.ts-D-vgx5UZ.js → _server.ts-BTmknWpO.js} +2 -2
- package/build/server/chunks/{_server.ts-D-vgx5UZ.js.map → _server.ts-BTmknWpO.js.map} +1 -1
- package/build/server/chunks/{_server.ts-tChyh9FX.js → _server.ts-BXhmLZwN.js} +4 -2
- package/build/server/chunks/{_server.ts-tChyh9FX.js.map → _server.ts-BXhmLZwN.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CvJKTS3Z.js → _server.ts-BbRSpB74.js} +4 -2
- package/build/server/chunks/{_server.ts-CvJKTS3Z.js.map → _server.ts-BbRSpB74.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CC2K8-L2.js → _server.ts-Blx6TuRU.js} +4 -2
- package/build/server/chunks/_server.ts-Blx6TuRU.js.map +1 -0
- package/build/server/chunks/_server.ts-C6NRpe7e.js +33 -0
- package/build/server/chunks/_server.ts-C6NRpe7e.js.map +1 -0
- package/build/server/chunks/_server.ts-CGqCOCdK.js +53 -0
- package/build/server/chunks/_server.ts-CGqCOCdK.js.map +1 -0
- package/build/server/chunks/{_server.ts-X1R7L_QI.js → _server.ts-CYWXjihn.js} +4 -2
- package/build/server/chunks/{_server.ts-X1R7L_QI.js.map → _server.ts-CYWXjihn.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CD7JP3fz.js → _server.ts-D0___krA.js} +4 -2
- package/build/server/chunks/_server.ts-D0___krA.js.map +1 -0
- package/build/server/chunks/{_server.ts-VzDcFFgy.js → _server.ts-DPHRUFYS.js} +4 -2
- package/build/server/chunks/_server.ts-DPHRUFYS.js.map +1 -0
- package/build/server/chunks/{_server.ts-D0zRDSx0.js → _server.ts-D_WRex0k.js} +4 -2
- package/build/server/chunks/_server.ts-D_WRex0k.js.map +1 -0
- package/build/server/chunks/{_server.ts-CA5KUENM.js → _server.ts-Da1kSClZ.js} +4 -2
- package/build/server/chunks/_server.ts-Da1kSClZ.js.map +1 -0
- package/build/server/chunks/{_server.ts-Dp-hXW_I.js → _server.ts-l3cd4Cto.js} +4 -2
- package/build/server/chunks/_server.ts-l3cd4Cto.js.map +1 -0
- package/build/server/chunks/{library-apns-Dl3iRE2h.js → library-apns-D8RPINlv.js} +62 -7
- package/build/server/chunks/library-apns-D8RPINlv.js.map +1 -0
- package/build/server/chunks/{pending-requests-C9p57WoU.js → pending-requests-8rWjrF6d.js} +3 -2
- package/build/server/chunks/pending-requests-8rWjrF6d.js.map +1 -0
- package/build/server/chunks/presence-store-Bx_g0-Gd.js +23 -0
- package/build/server/chunks/presence-store-Bx_g0-Gd.js.map +1 -0
- package/build/server/chunks/{pty-manager-ZqXqa-6A.js → pty-manager-DDjG7DlH.js} +297 -31
- package/build/server/chunks/pty-manager-DDjG7DlH.js.map +1 -0
- package/build/server/chunks/shooter-home-4f_HkdGI.js +10 -0
- package/build/server/chunks/shooter-home-4f_HkdGI.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +38 -24
- package/build/server/manifest.js.map +1 -1
- package/package.json +4 -2
- package/server.ts +2 -2
- package/src/lib/modules/client/common/index.ts +1 -0
- package/src/lib/modules/client/common/presence.ts +47 -0
- package/src/lib/modules/client/dashboard/AutopilotPanel.svelte +188 -4
- package/src/lib/modules/client/dashboard/autopilot-driver.svelte.ts +681 -0
- package/src/lib/modules/client/dashboard/decide-injection.ts +127 -0
- package/src/lib/modules/client/dashboard/store.svelte.ts +65 -24
- package/src/lib/modules/client/neurolink/fetch-proxy.ts +38 -1
- package/src/lib/modules/client/terminal/xterm-wrapper.ts +52 -12
- package/src/lib/modules/server/apn/apns-payload.ts +50 -0
- package/src/lib/modules/server/apn/library-apns.ts +50 -8
- package/src/lib/modules/server/apn/pending-requests.ts +3 -1
- package/src/lib/modules/server/sessions/autopilot-context.ts +57 -0
- package/src/lib/modules/server/sessions/autopilot-engine.ts +148 -43
- package/src/lib/modules/server/sessions/litellm-client.ts +90 -34
- package/src/lib/modules/server/sessions/next-step-consensus.ts +27 -2
- package/src/lib/modules/server/sessions/summary-store.ts +3 -1
- package/src/lib/modules/server/terminal/agent-launch.ts +26 -0
- package/src/lib/modules/server/terminal/pty-holder.cjs +6 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +292 -38
- package/src/lib/modules/server/terminal/session-watcher.ts +12 -2
- package/src/lib/modules/server/terminal/terminal-emulator.ts +102 -0
- package/src/lib/modules/server/terminal/terminal-store.ts +3 -1
- package/src/lib/modules/server/utils/shooter-home.ts +16 -0
- package/src/lib/modules/server/ws/presence-store.ts +50 -0
- package/src/lib/modules/server/ws/server.ts +18 -2
- package/src/lib/modules/server/ws/terminal-handler.ts +11 -6
- package/src/lib/types/autopilot.ts +65 -0
- package/src/lib/types/generated/WsProtocol.ts +10 -1
- package/src/lib/types/server.ts +27 -1
- package/src/lib/types/terminal-client.ts +3 -0
- package/src/lib/types/ws.ts +3 -2
- package/src/routes/api/autopilot/goal/+server.ts +72 -0
- package/src/routes/api/neurolink-proxy/+server.ts +8 -2
- package/src/routes/api/notify/+server.ts +22 -15
- package/src/routes/api/presence/+server.ts +39 -0
- package/src/routes/api/ws-status/+server.ts +8 -5
- package/build/client/_app/immutable/assets/2.BHi6pjT2.css +0 -1
- package/build/client/_app/immutable/assets/2.BHi6pjT2.css.br +0 -0
- package/build/client/_app/immutable/assets/2.BHi6pjT2.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BmfLecb1.js.br +0 -0
- package/build/client/_app/immutable/chunks/BmfLecb1.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CRkG7oE4.js +0 -1
- package/build/client/_app/immutable/chunks/CRkG7oE4.js.br +0 -0
- package/build/client/_app/immutable/chunks/CRkG7oE4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DOEXXmsh.js.br +0 -0
- package/build/client/_app/immutable/chunks/DOEXXmsh.js.gz +0 -0
- package/build/client/_app/immutable/chunks/EqMAkEha.js.br +0 -0
- package/build/client/_app/immutable/chunks/EqMAkEha.js.gz +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js +0 -6
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.br +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CeSxgGat.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CeSxgGat.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DrnJFwxA.js +0 -1
- package/build/client/_app/immutable/entry/start.DrnJFwxA.js.br +0 -2
- package/build/client/_app/immutable/entry/start.DrnJFwxA.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.gz +0 -0
- package/build/client/_app/immutable/nodes/11.CKmZjP_a.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.zlrdNFtH.js +0 -13
- package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.gz +0 -0
- package/build/server/chunks/_page.svelte-tBuIq8Pg.js +0 -159
- package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +0 -1
- package/build/server/chunks/_server.ts-BWVlO8iV.js.map +0 -1
- package/build/server/chunks/_server.ts-BevnuePu.js.map +0 -1
- package/build/server/chunks/_server.ts-CA5KUENM.js.map +0 -1
- package/build/server/chunks/_server.ts-CC2K8-L2.js.map +0 -1
- package/build/server/chunks/_server.ts-CD7JP3fz.js.map +0 -1
- package/build/server/chunks/_server.ts-D0zRDSx0.js.map +0 -1
- package/build/server/chunks/_server.ts-Dp-hXW_I.js.map +0 -1
- package/build/server/chunks/_server.ts-VzDcFFgy.js.map +0 -1
- package/build/server/chunks/library-apns-Dl3iRE2h.js.map +0 -1
- package/build/server/chunks/pending-requests-C9p57WoU.js.map +0 -1
- package/build/server/chunks/pty-manager-ZqXqa-6A.js.map +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side authoritative terminal emulator (Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Wraps a DOM-free @xterm/headless Terminal + @xterm/addon-serialize. Every
|
|
5
|
+
* PTY output chunk is fed in via write(); snapshot() returns a VT-escape string
|
|
6
|
+
* that reconstructs the CURRENT screen — including the alternate buffer (TUIs
|
|
7
|
+
* like vim/htop) and modes — when written into a fresh terminal. This is what
|
|
8
|
+
* a new or reconnecting client receives instead of a raw scrollback replay,
|
|
9
|
+
* fixing late-join corruption (G2) and the scrollback/live duplication race (G3).
|
|
10
|
+
*
|
|
11
|
+
* Caveats handled here (see the keystone spike): @xterm/addon-serialize does
|
|
12
|
+
* NOT serialize cursor visibility (DECTCEM ?25l), so we track it from the byte
|
|
13
|
+
* stream and re-emit it; cursor position restores functionally but not
|
|
14
|
+
* byte-exactly (do not assert byte-equality). Pinned to @xterm/headless@6.0.0
|
|
15
|
+
* and @xterm/addon-serialize@0.14.0 (serialize() reaches into _core internals).
|
|
16
|
+
*
|
|
17
|
+
* Interop note: both packages export via CJS in a way tsx/Node cannot resolve
|
|
18
|
+
* as named ESM imports, so the runtime values come through createRequire() while
|
|
19
|
+
* the types are imported separately.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { TerminalSnapshot } from '$lib/types';
|
|
23
|
+
import type { SerializeAddon as SerializeAddonInstance } from '@xterm/addon-serialize';
|
|
24
|
+
import type { Terminal as HeadlessTerminal } from '@xterm/headless';
|
|
25
|
+
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const { Terminal } = require('@xterm/headless') as {
|
|
30
|
+
Terminal: new (options?: object) => HeadlessTerminal;
|
|
31
|
+
};
|
|
32
|
+
const { SerializeAddon } = require('@xterm/addon-serialize') as {
|
|
33
|
+
SerializeAddon: new () => SerializeAddonInstance;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Scrollback lines retained in the emulator and included in snapshots. */
|
|
37
|
+
const SNAPSHOT_SCROLLBACK_LINES = 1000;
|
|
38
|
+
|
|
39
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
40
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
41
|
+
|
|
42
|
+
export class TerminalEmulator {
|
|
43
|
+
private cursorHidden = false;
|
|
44
|
+
private readonly serializer: SerializeAddonInstance;
|
|
45
|
+
private readonly term: HeadlessTerminal;
|
|
46
|
+
|
|
47
|
+
constructor(cols: number, rows: number) {
|
|
48
|
+
this.term = new Terminal({
|
|
49
|
+
allowProposedApi: true,
|
|
50
|
+
cols: cols > 0 ? cols : 80,
|
|
51
|
+
rows: rows > 0 ? rows : 24,
|
|
52
|
+
scrollback: SNAPSHOT_SCROLLBACK_LINES,
|
|
53
|
+
});
|
|
54
|
+
this.serializer = new SerializeAddon();
|
|
55
|
+
// @xterm/addon-serialize types its addon against @xterm/xterm's Terminal,
|
|
56
|
+
// but we run it on @xterm/headless's Terminal. Runtime-compatible; the cast
|
|
57
|
+
// bridges the two structurally-different Terminal types at loadAddon only.
|
|
58
|
+
this.term.loadAddon(this.serializer as unknown as Parameters<HeadlessTerminal['loadAddon']>[0]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dispose(): void {
|
|
62
|
+
try {
|
|
63
|
+
this.serializer.dispose();
|
|
64
|
+
this.term.dispose();
|
|
65
|
+
} catch {
|
|
66
|
+
// Already disposed — ignore.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
resize(cols: number, rows: number): void {
|
|
71
|
+
if (cols > 0 && rows > 0) {
|
|
72
|
+
this.term.resize(cols, rows);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Capture the current screen as a VT-escape string. Serialization runs inside
|
|
78
|
+
* a write() callback so all previously-written bytes are parsed first.
|
|
79
|
+
*/
|
|
80
|
+
snapshot(): Promise<TerminalSnapshot> {
|
|
81
|
+
return new Promise<TerminalSnapshot>((resolve) => {
|
|
82
|
+
this.term.write('', () => {
|
|
83
|
+
let data = this.serializer.serialize({ scrollback: SNAPSHOT_SCROLLBACK_LINES });
|
|
84
|
+
// SerializeAddon omits cursor visibility — re-emit when hidden.
|
|
85
|
+
if (this.cursorHidden) {
|
|
86
|
+
data += HIDE_CURSOR;
|
|
87
|
+
}
|
|
88
|
+
resolve({ cols: this.term.cols, data, rows: this.term.rows });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
write(data: string): void {
|
|
94
|
+
// Track cursor visibility (DECTCEM) from the stream; last toggle wins.
|
|
95
|
+
const hideIdx = data.lastIndexOf(HIDE_CURSOR);
|
|
96
|
+
const showIdx = data.lastIndexOf(SHOW_CURSOR);
|
|
97
|
+
if (hideIdx !== -1 || showIdx !== -1) {
|
|
98
|
+
this.cursorHidden = hideIdx > showIdx;
|
|
99
|
+
}
|
|
100
|
+
this.term.write(data);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -13,9 +13,11 @@ import Database from 'better-sqlite3';
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
|
|
16
|
+
import { shooterDataDir } from '../utils/shooter-home.js';
|
|
17
|
+
|
|
16
18
|
// ── Constants ────────────────────────────────────────────────────────
|
|
17
19
|
|
|
18
|
-
const DB_DIR =
|
|
20
|
+
const DB_DIR = shooterDataDir();
|
|
19
21
|
const DB_PATH = path.join(DB_DIR, 'shooter.db');
|
|
20
22
|
|
|
21
23
|
// ── Snake/Camel Conversion ───────────────────────────────────────────
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Shooter data directory — the SQLite DB and engine/permission state live here.
|
|
6
|
+
*
|
|
7
|
+
* Honors `SHOOTER_HOME` so an isolated instance (a test server, or a second server run alongside
|
|
8
|
+
* the user's daemon) keeps its DB/state separate from the default `~/.shooter` instead of sharing
|
|
9
|
+
* it (which would let one server's `reconnectAll()` adopt the other's live terminals). This matches
|
|
10
|
+
* the CLI (`bin/shooter.cjs`) and the env loader (`env.ts`), which already treat `SHOOTER_HOME` as
|
|
11
|
+
* the data dir. When unset, falls back to `~/.shooter`.
|
|
12
|
+
*/
|
|
13
|
+
export function shooterDataDir(): string {
|
|
14
|
+
const override = process.env.SHOOTER_HOME?.trim();
|
|
15
|
+
return override && override.length > 0 ? override : join(homedir(), '.shooter');
|
|
16
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Viewer-presence store: "is someone actually watching" (foreground) vs "away".
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: the phone-resident autonomous loop holds a PERSISTENT /ws/events
|
|
4
|
+
// connection to run. So raw WebSocket-connection count no longer means "the user is
|
|
5
|
+
// watching" — it's always > 0. The autopilot push decision must instead key on whether a
|
|
6
|
+
// viewer is FOREGROUNDED (heartbeat within TTL), reported via POST /api/presence.
|
|
7
|
+
//
|
|
8
|
+
// globalThis singleton so the SvelteKit route (which writes it) and the server.ts engine
|
|
9
|
+
// (which reads it) share one instance across the dual module graph — same pattern as the
|
|
10
|
+
// ws ticket store and the event-listener registry.
|
|
11
|
+
|
|
12
|
+
const PRESENCE_TTL_MS = 45_000;
|
|
13
|
+
const KEY = '__shooter_presence';
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line no-restricted-syntax -- internal singleton shape, never exported
|
|
16
|
+
interface PresenceRecord {
|
|
17
|
+
everReported: boolean;
|
|
18
|
+
lastForegroundAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True once any client has ever reported presence (used for backward-compatible fallback). */
|
|
22
|
+
export function hasEverReported(): boolean {
|
|
23
|
+
return store().everReported;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when a viewer reported `foreground` within the TTL window. */
|
|
27
|
+
export function isViewerPresent(
|
|
28
|
+
now: number = Date.now(),
|
|
29
|
+
ttlMs: number = PRESENCE_TTL_MS
|
|
30
|
+
): boolean {
|
|
31
|
+
return now - store().lastForegroundAt < ttlMs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record a presence heartbeat. `foreground` marks the viewer as actively watching (now);
|
|
36
|
+
* `background` marks them away immediately (so push resumes without waiting out the TTL).
|
|
37
|
+
*/
|
|
38
|
+
export function reportPresence(state: 'background' | 'foreground', now: number = Date.now()): void {
|
|
39
|
+
const s = store();
|
|
40
|
+
s.everReported = true;
|
|
41
|
+
s.lastForegroundAt = state === 'foreground' ? now : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function store(): PresenceRecord {
|
|
45
|
+
const g = globalThis as Record<string, unknown>;
|
|
46
|
+
if (!g[KEY]) {
|
|
47
|
+
g[KEY] = { everReported: false, lastForegroundAt: 0 };
|
|
48
|
+
}
|
|
49
|
+
return g[KEY] as PresenceRecord;
|
|
50
|
+
}
|
|
@@ -56,8 +56,24 @@ export function setupWebSocketHandlers(
|
|
|
56
56
|
): void {
|
|
57
57
|
const host = request.headers.host ?? 'localhost';
|
|
58
58
|
let pathname: string;
|
|
59
|
+
let snapshotCapable = false;
|
|
60
|
+
let lastSeq = 0;
|
|
59
61
|
try {
|
|
60
|
-
|
|
62
|
+
const url = new URL(request.url || '/', `http://${host}`);
|
|
63
|
+
pathname = url.pathname;
|
|
64
|
+
// Capability negotiation: clients that understand the {snapshot} frame
|
|
65
|
+
// advertise ?caps=snapshot. Others fall back to raw scrollback replay.
|
|
66
|
+
snapshotCapable = url.searchParams.get('caps') === 'snapshot';
|
|
67
|
+
// Reconnect resume (Phase 2): a returning client passes the highest output
|
|
68
|
+
// seq it already applied so the server can replay just the gap (or snapshot
|
|
69
|
+
// if the gap aged out of the ring). Absent / non-numeric ⇒ fresh join.
|
|
70
|
+
const rawLastSeq = url.searchParams.get('lastSeq');
|
|
71
|
+
if (rawLastSeq !== null) {
|
|
72
|
+
const parsed = Number(rawLastSeq);
|
|
73
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
74
|
+
lastSeq = Math.floor(parsed);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
61
77
|
} catch {
|
|
62
78
|
socket.destroy();
|
|
63
79
|
return;
|
|
@@ -103,7 +119,7 @@ export function setupWebSocketHandlers(
|
|
|
103
119
|
|
|
104
120
|
if (terminalMatch) {
|
|
105
121
|
const terminalId = terminalMatch[1];
|
|
106
|
-
handleTerminalConnection(ws, terminalId, scope);
|
|
122
|
+
handleTerminalConnection(ws, terminalId, scope, snapshotCapable, lastSeq);
|
|
107
123
|
} else if (superSessionMatch) {
|
|
108
124
|
const superSessionId = superSessionMatch[1];
|
|
109
125
|
handleSuperSessionConnection(ws, superSessionId);
|
|
@@ -32,7 +32,9 @@ let _ptyManager: null | PtyManagerLike = null;
|
|
|
32
32
|
export function handleTerminalConnection(
|
|
33
33
|
ws: WebSocket,
|
|
34
34
|
terminalId: string,
|
|
35
|
-
scope?: TicketScope
|
|
35
|
+
scope?: TicketScope,
|
|
36
|
+
snapshotCapable = false,
|
|
37
|
+
lastSeq = 0
|
|
36
38
|
): void {
|
|
37
39
|
// ── 1. Look up the terminal ──────────────────────────────────────
|
|
38
40
|
if (!_ptyManager) {
|
|
@@ -48,11 +50,14 @@ export function handleTerminalConnection(
|
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
// ── 2. Attach via pty-manager (registers client + sends
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
53
|
+
// ── 2. Attach via pty-manager (registers client + sends initial state) ──
|
|
54
|
+
// Snapshot-capable clients (?caps=snapshot) get a serialized current-screen
|
|
55
|
+
// snapshot; others get the legacy raw scrollback replay. A reconnecting
|
|
56
|
+
// client also passes lastSeq (>0) so attach() can replay only the missing
|
|
57
|
+
// frames from the ring instead of re-snapshotting. attach() adds the ws to
|
|
58
|
+
// terminal.clients + outputBuffers; broadcastOutput() then delivers all live
|
|
59
|
+
// PTY output — no per-client onData listener needed.
|
|
60
|
+
_ptyManager.attach(terminalId, ws, { lastSeq, snapshot: snapshotCapable });
|
|
56
61
|
|
|
57
62
|
// If the terminal already exited, tell the client immediately.
|
|
58
63
|
if (terminal.status === 'exited') {
|
|
@@ -15,6 +15,16 @@ export interface AutopilotState {
|
|
|
15
15
|
/** Per-session autopilot lifecycle state. */
|
|
16
16
|
export type AutopilotStatus = 'error' | 'idle' | 'running';
|
|
17
17
|
|
|
18
|
+
/** Verdict from guardCommand() — whether a concrete command is safe to write to the PTY. */
|
|
19
|
+
export interface CommandVerdict {
|
|
20
|
+
/** The trimmed command (echoed back for convenience). */
|
|
21
|
+
command: string;
|
|
22
|
+
/** Human-readable reason for the verdict. */
|
|
23
|
+
reason: string;
|
|
24
|
+
/** True when the command passed all guards and may be injected. */
|
|
25
|
+
safe: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
/** Result from mergeNextStepConsensus(). */
|
|
19
29
|
export interface ConsensusResult {
|
|
20
30
|
/** Number of agent lists passed in. */
|
|
@@ -25,6 +35,61 @@ export interface ConsensusResult {
|
|
|
25
35
|
steps: NextStep[];
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
/** A record of one autonomous-loop decision, surfaced in the dashboard panel. */
|
|
39
|
+
export interface DriverAction {
|
|
40
|
+
/** ms timestamp of the action. */
|
|
41
|
+
at: number;
|
|
42
|
+
/** The command injected, or a short reason it was skipped / failed. */
|
|
43
|
+
detail: string;
|
|
44
|
+
/** What happened. */
|
|
45
|
+
kind: DriverActionKind;
|
|
46
|
+
/** The terminal acted on. */
|
|
47
|
+
terminalId: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Outcome of one driver evaluation. */
|
|
51
|
+
export type DriverActionKind = 'error' | 'injected' | 'skipped';
|
|
52
|
+
|
|
53
|
+
/** Decision from decideInjection() — the gate deciding whether to auto-act. */
|
|
54
|
+
export interface GateDecision {
|
|
55
|
+
/** True when all safety gates pass and the loop should produce + inject a command. */
|
|
56
|
+
act: boolean;
|
|
57
|
+
/** Human-readable reason (always set, for logging + the phone UI). */
|
|
58
|
+
reason: string;
|
|
59
|
+
/** The consensus step being acted on (present when act is true). */
|
|
60
|
+
step?: NextStep;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Tunable thresholds for the auto-inject safety gate. */
|
|
64
|
+
export interface InjectionPolicy {
|
|
65
|
+
/** Suppress injection within this many ms of observed human / output activity. */
|
|
66
|
+
humanGraceMs: number;
|
|
67
|
+
/** Minimum confidence of the top consensus step required to inject. */
|
|
68
|
+
injectConfidence: number;
|
|
69
|
+
/** Stop auto-injecting a terminal after this many consecutive actions without progress. */
|
|
70
|
+
maxAutoActions: number;
|
|
71
|
+
/** Minimum gap in ms between injections into the same terminal. */
|
|
72
|
+
minIntervalMs: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Per-terminal snapshot the driver passes to decideInjection(). */
|
|
76
|
+
export interface InjectionState {
|
|
77
|
+
/** Consecutive auto-injections without a human touch or successful tool completion. */
|
|
78
|
+
autoActionCount: number;
|
|
79
|
+
/** True only for terminals Shooter created (POST /api/terminals); external sessions are read-only. */
|
|
80
|
+
isManaged: boolean;
|
|
81
|
+
/** Normalized text of the last consensus step acted on (dedup guard). */
|
|
82
|
+
lastActedStep: null | string;
|
|
83
|
+
/** ms timestamp of the last observed human input / terminal output activity. */
|
|
84
|
+
lastActivityAt: number;
|
|
85
|
+
/** The most recent WireShooterEvent type seen for this terminal. */
|
|
86
|
+
lastEventType: string;
|
|
87
|
+
/** ms timestamp of the last command injection into this terminal. */
|
|
88
|
+
lastInjectedAt: number;
|
|
89
|
+
/** The terminal id. */
|
|
90
|
+
terminalId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
28
93
|
/** Options for mergeNextStepConsensus(). */
|
|
29
94
|
export interface MergeOptions {
|
|
30
95
|
/** Max steps taken from each agent list (default 3). */
|
|
@@ -238,20 +238,29 @@ export type TerminalOutputMessage = {
|
|
|
238
238
|
* @memberof TerminalOutputMessage
|
|
239
239
|
*/
|
|
240
240
|
data: string;
|
|
241
|
+
/**
|
|
242
|
+
* @description Monotonic per-terminal sequence number (starts at 1, increments by 1 for every broadcast output frame). Clients track lastSeq to enable gap detection and reconnect resume (Phase 2).
|
|
243
|
+
|
|
244
|
+
* @type { number }
|
|
245
|
+
* @memberof TerminalOutputMessage
|
|
246
|
+
*/
|
|
247
|
+
seq: number;
|
|
241
248
|
};
|
|
242
249
|
|
|
243
250
|
export function decodeTerminalOutputMessage(rawInput: unknown): TerminalOutputMessage | null {
|
|
244
251
|
if (isJSON(rawInput)) {
|
|
245
252
|
const decodedType = decodeString(rawInput['type']);
|
|
246
253
|
const decodedData = decodeString(rawInput['data']);
|
|
254
|
+
const decodedSeq = decodeNumber(rawInput['seq']);
|
|
247
255
|
|
|
248
|
-
if (decodedType === null || decodedData === null) {
|
|
256
|
+
if (decodedType === null || decodedData === null || decodedSeq === null) {
|
|
249
257
|
return null;
|
|
250
258
|
}
|
|
251
259
|
|
|
252
260
|
return {
|
|
253
261
|
type: decodedType,
|
|
254
262
|
data: decodedData,
|
|
263
|
+
seq: decodedSeq,
|
|
255
264
|
};
|
|
256
265
|
}
|
|
257
266
|
return null;
|
package/src/lib/types/server.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type WebSocket from 'ws';
|
|
|
10
10
|
|
|
11
11
|
import type { CodexStreamParser } from '../modules/server/sessions/codex-parser';
|
|
12
12
|
import type { HolderClient } from '../modules/server/terminal/holder-client';
|
|
13
|
+
import type { TerminalEmulator } from '../modules/server/terminal/terminal-emulator';
|
|
13
14
|
import type { ConversationMessage } from './sessions';
|
|
14
15
|
|
|
15
16
|
// ── holder-client types ─────────────────────────────────────────────
|
|
@@ -87,6 +88,8 @@ export interface PtyManagedTerminal {
|
|
|
87
88
|
createdAt: Date;
|
|
88
89
|
currentCwd: null | string;
|
|
89
90
|
cwd: string;
|
|
91
|
+
/** Server-side headless emulator for snapshot-on-join (null when disabled). */
|
|
92
|
+
emulator: null | TerminalEmulator;
|
|
90
93
|
exitCode: null | number;
|
|
91
94
|
exitedAt: Date | null;
|
|
92
95
|
holderPid: number;
|
|
@@ -100,6 +103,10 @@ export interface PtyManagedTerminal {
|
|
|
100
103
|
pty: HolderClient;
|
|
101
104
|
rows: number;
|
|
102
105
|
scrollback: string;
|
|
106
|
+
/** Monotonic counter; equals the last assigned seq for this terminal. */
|
|
107
|
+
seqCounter: number;
|
|
108
|
+
/** Bounded replay ring of recent output chunks (max SEQ_RING_MAX_ENTRIES). */
|
|
109
|
+
seqRing: SeqRingEntry[];
|
|
103
110
|
sessionFile: null | string;
|
|
104
111
|
socketPath: string;
|
|
105
112
|
status: 'exited' | 'running';
|
|
@@ -113,7 +120,14 @@ export interface PtyOutputBuffer {
|
|
|
113
120
|
size: number;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
/**
|
|
124
|
+
* One entry in the per-terminal sequence ring (in-memory store type;
|
|
125
|
+
* structurally identical to the generated wire SeqRingEntry).
|
|
126
|
+
*/
|
|
127
|
+
export interface SeqRingEntry {
|
|
128
|
+
data: string;
|
|
129
|
+
seq: number;
|
|
130
|
+
}
|
|
117
131
|
|
|
118
132
|
export interface SessionWatchedFile {
|
|
119
133
|
callbacks: Set<OnNewEntries>;
|
|
@@ -121,3 +135,15 @@ export interface SessionWatchedFile {
|
|
|
121
135
|
offset: number;
|
|
122
136
|
watcher: FSWatcher;
|
|
123
137
|
}
|
|
138
|
+
|
|
139
|
+
// ── generic-session-watcher types ───────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A serialized snapshot of a terminal's current screen (VT escape string)
|
|
143
|
+
* produced by the server-side headless emulator, plus its dimensions.
|
|
144
|
+
*/
|
|
145
|
+
export interface TerminalSnapshot {
|
|
146
|
+
cols: number;
|
|
147
|
+
data: string;
|
|
148
|
+
rows: number;
|
|
149
|
+
}
|
|
@@ -108,6 +108,8 @@ export interface ShortcutsHelpProps {
|
|
|
108
108
|
export interface TerminalInstance {
|
|
109
109
|
dispose: () => void;
|
|
110
110
|
fitAddon: FitAddon | null;
|
|
111
|
+
/** Highest output `seq` seen from the server (for Phase 2 reconnect resume). */
|
|
112
|
+
getLastSeq: () => number;
|
|
111
113
|
sendInput: (data: string) => void;
|
|
112
114
|
term: Terminal;
|
|
113
115
|
}
|
|
@@ -147,5 +149,6 @@ export interface WsTerminalInboundMessage {
|
|
|
147
149
|
data?: string;
|
|
148
150
|
path?: string;
|
|
149
151
|
rows?: number;
|
|
152
|
+
seq?: number;
|
|
150
153
|
type: string;
|
|
151
154
|
}
|
package/src/lib/types/ws.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface TerminalManagedTerminal {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export interface TerminalPtyManagerLike {
|
|
76
|
-
attach: (id: string, ws: WebSocket) => boolean;
|
|
76
|
+
attach: (id: string, ws: WebSocket, opts?: { lastSeq?: number; snapshot?: boolean }) => boolean;
|
|
77
77
|
detach: (id: string, ws: WebSocket) => boolean;
|
|
78
78
|
getTerminal: (id: string) => TerminalManagedTerminal | undefined;
|
|
79
79
|
}
|
|
@@ -146,8 +146,9 @@ export type WireTerminalServerMessage =
|
|
|
146
146
|
| { bytes: number; type: 'output-dropped' }
|
|
147
147
|
| { chunk: number; data: string; total: number; type: 'scrollback' }
|
|
148
148
|
| { code: null | number; signal: null | string; type: 'exit' }
|
|
149
|
+
| { cols: number; data: string; rows: number; seq: number; type: 'snapshot' }
|
|
149
150
|
| { cols: number; rows: number; type: 'resize' }
|
|
150
|
-
| { data: string; type: 'output' }
|
|
151
|
+
| { data: string; seq: number; type: 'output' }
|
|
151
152
|
| { message: string; type: 'error' };
|
|
152
153
|
|
|
153
154
|
// ── notification-sessions type alias ────────────────────────────────
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Set/read the per-terminal autopilot GOAL that anchors the engine's context every cycle.
|
|
2
|
+
//
|
|
3
|
+
// POST { terminalId, goal } pins the goal; GET ?terminalId=… reads it back. Like
|
|
4
|
+
// /api/autopilot, this reaches the engine via globalThis.__shooter_autopilot rather than
|
|
5
|
+
// importing it (importing would start a second event subscriber). Goals live in-memory in the
|
|
6
|
+
// engine (no file fallback): if the engine is not running there is nothing to set, so we 503.
|
|
7
|
+
|
|
8
|
+
import { validateAuth } from '$lib/modules/server/auth';
|
|
9
|
+
import { json } from '@sveltejs/kit';
|
|
10
|
+
|
|
11
|
+
import type { RequestHandler } from './$types';
|
|
12
|
+
|
|
13
|
+
function control():
|
|
14
|
+
| undefined
|
|
15
|
+
| {
|
|
16
|
+
getGoal?: (terminalId: string) => string | undefined;
|
|
17
|
+
setGoal?: (terminalId: string, goal: string) => void;
|
|
18
|
+
} {
|
|
19
|
+
return (globalThis as Record<string, unknown>).__shooter_autopilot as
|
|
20
|
+
| undefined
|
|
21
|
+
| {
|
|
22
|
+
getGoal?: (terminalId: string) => string | undefined;
|
|
23
|
+
setGoal?: (terminalId: string, goal: string) => void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const GET: RequestHandler = ({ request, url }) => {
|
|
28
|
+
const authError = validateAuth(request);
|
|
29
|
+
if (authError) {
|
|
30
|
+
return authError;
|
|
31
|
+
}
|
|
32
|
+
const terminalId = url.searchParams.get('terminalId') ?? '';
|
|
33
|
+
if (!terminalId) {
|
|
34
|
+
return json({ error: 'terminalId query param is required' }, { status: 400 });
|
|
35
|
+
}
|
|
36
|
+
const ctrl = control();
|
|
37
|
+
if (!ctrl?.getGoal) {
|
|
38
|
+
return json({ error: 'autopilot engine not running', running: false }, { status: 503 });
|
|
39
|
+
}
|
|
40
|
+
return json({ goal: ctrl.getGoal(terminalId) ?? null, running: true });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const POST: RequestHandler = async ({ request }) => {
|
|
44
|
+
const authError = validateAuth(request);
|
|
45
|
+
if (authError) {
|
|
46
|
+
return authError;
|
|
47
|
+
}
|
|
48
|
+
let body: { goal?: unknown; terminalId?: unknown };
|
|
49
|
+
try {
|
|
50
|
+
body = (await request.json()) as { goal?: unknown; terminalId?: unknown };
|
|
51
|
+
} catch {
|
|
52
|
+
return json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
if (typeof body.terminalId !== 'string' || body.terminalId.length === 0) {
|
|
55
|
+
return json({ error: 'terminalId must be a non-empty string' }, { status: 400 });
|
|
56
|
+
}
|
|
57
|
+
if (typeof body.goal !== 'string') {
|
|
58
|
+
return json({ error: 'goal must be a string' }, { status: 400 });
|
|
59
|
+
}
|
|
60
|
+
if (body.goal.length > 500) {
|
|
61
|
+
// The goal is prepended to EVERY engine LLM context, so an unbounded string would permanently
|
|
62
|
+
// bloat (and could dominate) the prompt. Cap it like the summaries route caps its fields.
|
|
63
|
+
return json({ error: 'goal must be 500 characters or fewer' }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ctrl = control();
|
|
67
|
+
if (!ctrl?.setGoal) {
|
|
68
|
+
return json({ error: 'autopilot engine not running', running: false }, { status: 503 });
|
|
69
|
+
}
|
|
70
|
+
ctrl.setGoal(body.terminalId, body.goal);
|
|
71
|
+
return json({ goal: body.goal.trim() || null, running: true, terminalId: body.terminalId });
|
|
72
|
+
};
|
|
@@ -112,10 +112,16 @@ export const POST: RequestHandler = async ({ request }) => {
|
|
|
112
112
|
// For Bearer-token providers, only inject the header when the key is non-empty
|
|
113
113
|
// to avoid sending a malformed `Authorization: Bearer ` to the upstream.
|
|
114
114
|
if (provider === 'anthropic') {
|
|
115
|
-
|
|
115
|
+
// Only inject the key header when non-empty — sending `x-api-key: ` (empty) is a malformed
|
|
116
|
+
// header that the upstream rejects with a confusing error instead of a clean 401.
|
|
117
|
+
if (apiKeyEnv.anthropic) {
|
|
118
|
+
forwardHeaders['x-api-key'] = apiKeyEnv.anthropic;
|
|
119
|
+
}
|
|
116
120
|
forwardHeaders['anthropic-version'] = forwardHeaders['anthropic-version'] ?? '2023-06-01';
|
|
117
121
|
} else if (provider === 'google-ai') {
|
|
118
|
-
|
|
122
|
+
if (apiKeyEnv['google-ai']) {
|
|
123
|
+
forwardHeaders['x-goog-api-key'] = apiKeyEnv['google-ai'];
|
|
124
|
+
}
|
|
119
125
|
} else if (provider === 'openai') {
|
|
120
126
|
if (apiKeyEnv.openai) {
|
|
121
127
|
forwardHeaders.Authorization = `Bearer ${apiKeyEnv.openai}`;
|
|
@@ -173,13 +173,8 @@ function intelligentNotificationFilter(
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
reason: 'Stop hook completion notification - session finished',
|
|
180
|
-
send: true,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
176
|
+
// (Removed a dead `source === 'stop-hook'` branch: the notifier emits
|
|
177
|
+
// 'shooter-completion-detector', never 'stop-hook', and the default below already allows it.)
|
|
183
178
|
|
|
184
179
|
// Filter out only very specific spam patterns to be less restrictive
|
|
185
180
|
const spamPatterns = [
|
|
@@ -263,18 +258,28 @@ function isDuplicateNotification(
|
|
|
263
258
|
}
|
|
264
259
|
}
|
|
265
260
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
261
|
+
// RESERVE the slot atomically (check-and-set): a second concurrent request with the same key now
|
|
262
|
+
// sees it as a duplicate before either has delivered, closing the TOCTOU window that let two
|
|
263
|
+
// identical pushes through. The delivery path RELEASES the slot (releaseNotification) if the send
|
|
264
|
+
// fails, so a legitimate retry is not blocked — this replaces the old record-only-on-success
|
|
265
|
+
// scheme while still avoiding cache poisoning on failure.
|
|
266
|
+
notificationCache.set(key, now);
|
|
268
267
|
return false;
|
|
269
268
|
}
|
|
270
269
|
|
|
271
|
-
|
|
272
|
-
function recordNotification(title: string, message: string, data?: NotificationData): void {
|
|
270
|
+
function notificationKey(title: string, message: string, data?: NotificationData): string {
|
|
273
271
|
const dataRecord = data as (NotificationData & { dedupKey?: string }) | undefined;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
return dataRecord?.dedupKey ?? `${title}|${message}|${data?.category || 'unknown'}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Refresh a reserved dedup key after successful delivery (keeps the window measured from send). */
|
|
276
|
+
function recordNotification(title: string, message: string, data?: NotificationData): void {
|
|
277
|
+
notificationCache.set(notificationKey(title, message, data), Date.now());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Release a reserved dedup key when delivery failed, so a legitimate retry is not blocked. */
|
|
281
|
+
function releaseNotification(title: string, message: string, data?: NotificationData): void {
|
|
282
|
+
notificationCache.delete(notificationKey(title, message, data));
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
// TODO(refactor): extract body parsing, filtering, and platform routing into
|
|
@@ -501,6 +506,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
|
|
501
506
|
});
|
|
502
507
|
} else {
|
|
503
508
|
console.error(`[notify] FCM delivery failed: ${fcmResult.error}`);
|
|
509
|
+
releaseNotification(title, message, data); // free the reserved dedup slot for a retry
|
|
504
510
|
|
|
505
511
|
addNotification(
|
|
506
512
|
buildNotificationRecord(
|
|
@@ -581,6 +587,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
|
|
581
587
|
} catch (notificationError) {
|
|
582
588
|
const notifErrMsg = toErrorMessage(notificationError);
|
|
583
589
|
console.error(`[notify] APNs delivery failed: ${notifErrMsg}`);
|
|
590
|
+
releaseNotification(title, message, data); // free the reserved dedup slot for a retry
|
|
584
591
|
|
|
585
592
|
addNotification(
|
|
586
593
|
buildNotificationRecord(canonicalRequestId, title, message, 'failed', data, notifErrMsg)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Viewer-presence endpoint. The dashboard / phone posts a heartbeat so the autopilot
|
|
2
|
+
// engine can push only when the user is AWAY (not foregrounded) — see presence-store.ts.
|
|
3
|
+
// Distinct from raw WebSocket connection count, which the autonomous loop keeps open.
|
|
4
|
+
|
|
5
|
+
import { validateAuth } from '$lib/modules/server/auth';
|
|
6
|
+
import {
|
|
7
|
+
hasEverReported,
|
|
8
|
+
isViewerPresent,
|
|
9
|
+
reportPresence,
|
|
10
|
+
} from '$lib/modules/server/ws/presence-store';
|
|
11
|
+
import { json } from '@sveltejs/kit';
|
|
12
|
+
|
|
13
|
+
import type { RequestHandler } from './$types';
|
|
14
|
+
|
|
15
|
+
export const GET: RequestHandler = ({ request }) => {
|
|
16
|
+
const authError = validateAuth(request);
|
|
17
|
+
if (authError) {
|
|
18
|
+
return authError;
|
|
19
|
+
}
|
|
20
|
+
return json({ everReported: hasEverReported(), present: isViewerPresent() });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const POST: RequestHandler = async ({ request }) => {
|
|
24
|
+
const authError = validateAuth(request);
|
|
25
|
+
if (authError) {
|
|
26
|
+
return authError;
|
|
27
|
+
}
|
|
28
|
+
let body: { state?: unknown };
|
|
29
|
+
try {
|
|
30
|
+
body = (await request.json()) as { state?: unknown };
|
|
31
|
+
} catch {
|
|
32
|
+
return json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
if (body.state !== 'foreground' && body.state !== 'background') {
|
|
35
|
+
return json({ error: "state must be 'foreground' or 'background'" }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
reportPresence(body.state);
|
|
38
|
+
return json({ everReported: hasEverReported(), present: isViewerPresent(), state: body.state });
|
|
39
|
+
};
|