@juspay/shooter 1.0.0 → 1.2.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 +117 -33
- package/.claude/settings.json +14 -14
- package/README.md +116 -84
- package/bin/shooter.cjs +471 -102
- package/build/client/_app/immutable/assets/{0.CM9Hl6d-.css → 0.BhZOCxO4.css} +1 -1
- package/build/client/_app/immutable/assets/0.BhZOCxO4.css.br +0 -0
- package/build/client/_app/immutable/assets/0.BhZOCxO4.css.gz +0 -0
- package/build/client/_app/immutable/assets/1.BYutk3aU.css +1 -0
- package/build/client/_app/immutable/assets/1.BYutk3aU.css.br +0 -0
- package/build/client/_app/immutable/assets/1.BYutk3aU.css.gz +0 -0
- package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.gz +0 -0
- package/build/client/_app/immutable/assets/3.DGDHCVnW.css +1 -0
- package/build/client/_app/immutable/assets/3.DGDHCVnW.css.br +0 -0
- package/build/client/_app/immutable/assets/3.DGDHCVnW.css.gz +0 -0
- package/build/client/_app/immutable/assets/4.BFUut--w.css +1 -0
- package/build/client/_app/immutable/assets/4.BFUut--w.css.br +0 -0
- package/build/client/_app/immutable/assets/4.BFUut--w.css.gz +0 -0
- package/build/client/_app/immutable/assets/5.BTOx7yt7.css +1 -0
- package/build/client/_app/immutable/assets/5.BTOx7yt7.css.br +0 -0
- package/build/client/_app/immutable/assets/5.BTOx7yt7.css.gz +0 -0
- package/build/client/_app/immutable/assets/6.eZGZN-BF.css +1 -0
- package/build/client/_app/immutable/assets/6.eZGZN-BF.css.br +0 -0
- package/build/client/_app/immutable/assets/6.eZGZN-BF.css.gz +0 -0
- package/build/client/_app/immutable/assets/7.DwS5ZHBh.css +1 -0
- package/build/client/_app/immutable/assets/7.DwS5ZHBh.css.br +0 -0
- package/build/client/_app/immutable/assets/7.DwS5ZHBh.css.gz +0 -0
- package/build/client/_app/immutable/assets/ChatView.CwWbzIL-.css +1 -0
- package/build/client/_app/immutable/assets/ChatView.CwWbzIL-.css.br +0 -0
- package/build/client/_app/immutable/assets/ChatView.CwWbzIL-.css.gz +0 -0
- package/build/client/_app/immutable/assets/Phone.FQEfwCX2.css +1 -0
- package/build/client/_app/immutable/assets/Phone.FQEfwCX2.css.br +0 -0
- package/build/client/_app/immutable/assets/Phone.FQEfwCX2.css.gz +0 -0
- package/build/client/_app/immutable/assets/markdown.Dc-OSJWY.css +1 -0
- package/build/client/_app/immutable/assets/markdown.Dc-OSJWY.css.br +0 -0
- package/build/client/_app/immutable/assets/markdown.Dc-OSJWY.css.gz +0 -0
- package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.gz +0 -0
- package/build/client/_app/immutable/chunks/B-K5Sh65.js +1 -0
- package/build/client/_app/immutable/chunks/B-K5Sh65.js.br +0 -0
- package/build/client/_app/immutable/chunks/B-K5Sh65.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B5NAKyil.js +20 -0
- package/build/client/_app/immutable/chunks/B5NAKyil.js.br +0 -0
- package/build/client/_app/immutable/chunks/B5NAKyil.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B8XegpSE.js +1 -0
- package/build/client/_app/immutable/chunks/B8XegpSE.js.br +0 -0
- package/build/client/_app/immutable/chunks/B8XegpSE.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B8zoBsv3.js +6 -0
- package/build/client/_app/immutable/chunks/B8zoBsv3.js.br +0 -0
- package/build/client/_app/immutable/chunks/B8zoBsv3.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BLszSzTT.js +1 -0
- package/build/client/_app/immutable/chunks/BLszSzTT.js.br +0 -0
- package/build/client/_app/immutable/chunks/BLszSzTT.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BOYo8yTr.js +1 -0
- package/build/client/_app/immutable/chunks/BOYo8yTr.js.br +0 -0
- package/build/client/_app/immutable/chunks/BOYo8yTr.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BTGVxaYV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BYqGCrTe.js +1 -0
- package/build/client/_app/immutable/chunks/BYqGCrTe.js.br +0 -0
- package/build/client/_app/immutable/chunks/BYqGCrTe.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BlxrFPDK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bu1aqm5j.js +1 -0
- package/build/client/_app/immutable/chunks/Bu1aqm5j.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bu1aqm5j.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C4mLaWWx.js +1 -0
- package/build/client/_app/immutable/chunks/C4mLaWWx.js.br +0 -0
- package/build/client/_app/immutable/chunks/C4mLaWWx.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CQjSATpv.js +61 -0
- package/build/client/_app/immutable/chunks/CQjSATpv.js.br +0 -0
- package/build/client/_app/immutable/chunks/CQjSATpv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CSoRdFvv.js +1 -0
- package/build/client/_app/immutable/chunks/CSoRdFvv.js.br +0 -0
- package/build/client/_app/immutable/chunks/CSoRdFvv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CZHsSL_X.js +1 -0
- package/build/client/_app/immutable/chunks/CZHsSL_X.js.br +0 -0
- package/build/client/_app/immutable/chunks/CZHsSL_X.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DLu6yJIZ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DSU1n5N_.js +1 -0
- package/build/client/_app/immutable/chunks/DSU1n5N_.js.br +0 -0
- package/build/client/_app/immutable/chunks/DSU1n5N_.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DVkn4r72.js +1 -0
- package/build/client/_app/immutable/chunks/DVkn4r72.js.br +0 -0
- package/build/client/_app/immutable/chunks/DVkn4r72.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DjsDGxCa.js +41 -0
- package/build/client/_app/immutable/chunks/DjsDGxCa.js.br +0 -0
- package/build/client/_app/immutable/chunks/DjsDGxCa.js.gz +0 -0
- package/build/client/_app/immutable/chunks/PPVm8Dsz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/UJOiqIYE.js +1 -0
- package/build/client/_app/immutable/chunks/UJOiqIYE.js.br +0 -0
- package/build/client/_app/immutable/chunks/UJOiqIYE.js.gz +0 -0
- package/build/client/_app/immutable/chunks/r0JawsZc.js +2 -0
- package/build/client/_app/immutable/chunks/r0JawsZc.js.br +0 -0
- package/build/client/_app/immutable/chunks/r0JawsZc.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.Z3zMnuSx.js +2 -0
- package/build/client/_app/immutable/entry/app.Z3zMnuSx.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Z3zMnuSx.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.Dd-gIP4y.js +1 -0
- package/build/client/_app/immutable/entry/start.Dd-gIP4y.js.br +2 -0
- package/build/client/_app/immutable/entry/start.Dd-gIP4y.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D2YR8tTD.js +1 -0
- package/build/client/_app/immutable/nodes/0.D2YR8tTD.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D2YR8tTD.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.B3m6rO4C.js +1 -0
- package/build/client/_app/immutable/nodes/1.B3m6rO4C.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.B3m6rO4C.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.CyRB2euU.js +1 -0
- package/build/client/_app/immutable/nodes/2.CyRB2euU.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CyRB2euU.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.3yohCM25.js +3 -0
- package/build/client/_app/immutable/nodes/3.3yohCM25.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.3yohCM25.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.DEAcwl7l.js +1 -0
- package/build/client/_app/immutable/nodes/4.DEAcwl7l.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DEAcwl7l.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.C6bLGWQR.js +4 -0
- package/build/client/_app/immutable/nodes/5.C6bLGWQR.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.C6bLGWQR.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.ByTzlA2D.js +2 -0
- package/build/client/_app/immutable/nodes/6.ByTzlA2D.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.ByTzlA2D.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.BPMfwzd2.js +2 -0
- package/build/client/_app/immutable/nodes/7.BPMfwzd2.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.BPMfwzd2.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/client/favicon.svg.gz +0 -0
- package/build/client/manifest.json +13 -0
- package/build/client/manifest.json.br +0 -0
- package/build/client/manifest.json.gz +0 -0
- package/build/pty-holder.cjs +37 -8
- package/build/server/chunks/0-Vk38tI2J.js +9 -0
- package/build/server/chunks/0-Vk38tI2J.js.map +1 -0
- package/build/server/chunks/1-BvYQX5MR.js +9 -0
- package/build/server/chunks/1-BvYQX5MR.js.map +1 -0
- package/build/server/chunks/2-Cl7R4Qk2.js +9 -0
- package/build/server/chunks/2-Cl7R4Qk2.js.map +1 -0
- package/build/server/chunks/3-Ck7ewhOX.js +9 -0
- package/build/server/chunks/3-Ck7ewhOX.js.map +1 -0
- package/build/server/chunks/4-CnDeRm2Z.js +9 -0
- package/build/server/chunks/4-CnDeRm2Z.js.map +1 -0
- package/build/server/chunks/5-IxitzEvN.js +9 -0
- package/build/server/chunks/5-IxitzEvN.js.map +1 -0
- package/build/server/chunks/6-CyZ3r1iS.js +9 -0
- package/build/server/chunks/6-CyZ3r1iS.js.map +1 -0
- package/build/server/chunks/7-BmI7du46.js +9 -0
- package/build/server/chunks/7-BmI7du46.js.map +1 -0
- package/build/server/chunks/Button-Cs1aE6ka.js +80 -0
- package/build/server/chunks/Button-Cs1aE6ka.js.map +1 -0
- package/build/server/chunks/EmptyState-DDFH1K8g.js +26 -0
- package/build/server/chunks/EmptyState-DDFH1K8g.js.map +1 -0
- package/build/server/chunks/Icon-CEUrotA6.js +36 -0
- package/build/server/chunks/Icon-CEUrotA6.js.map +1 -0
- package/build/server/chunks/Shimmer-DB8W1zt6.js +10 -0
- package/build/server/chunks/Shimmer-DB8W1zt6.js.map +1 -0
- package/build/server/chunks/_error.svelte-uCOJNxvr.js +39 -0
- package/build/server/chunks/_error.svelte-uCOJNxvr.js.map +1 -0
- package/build/server/chunks/_layout.svelte-CtWmEJwe.js +56 -0
- package/build/server/chunks/_layout.svelte-CtWmEJwe.js.map +1 -0
- package/build/server/chunks/_page.svelte-BcZaKdX9.js +45 -0
- package/build/server/chunks/_page.svelte-BcZaKdX9.js.map +1 -0
- package/build/server/chunks/_page.svelte-BdYynOck.js +85 -0
- package/build/server/chunks/_page.svelte-BdYynOck.js.map +1 -0
- package/build/server/chunks/_page.svelte-BgevQjq1.js +101 -0
- package/build/server/chunks/_page.svelte-BgevQjq1.js.map +1 -0
- package/build/server/chunks/_page.svelte-CVq6tRb3.js +550 -0
- package/build/server/chunks/_page.svelte-CVq6tRb3.js.map +1 -0
- package/build/server/chunks/_page.svelte-CxWcQ0Am.js +651 -0
- package/build/server/chunks/_page.svelte-CxWcQ0Am.js.map +1 -0
- package/build/server/chunks/_page.svelte-DO4oa_LY.js +44 -0
- package/build/server/chunks/_page.svelte-DO4oa_LY.js.map +1 -0
- package/build/server/chunks/{_server.ts-CbDRDIoP.js → _server.ts-BStnNIcq.js} +9 -11
- package/build/server/chunks/_server.ts-BStnNIcq.js.map +1 -0
- package/build/server/chunks/{_server.ts-DRVbgm6k.js → _server.ts-CAxsWKvS.js} +22 -25
- package/build/server/chunks/_server.ts-CAxsWKvS.js.map +1 -0
- package/build/server/chunks/{_server.ts-CPa6DgIt.js → _server.ts-COu0vNpd.js} +6 -6
- package/build/server/chunks/_server.ts-COu0vNpd.js.map +1 -0
- package/build/server/chunks/{_server.ts-C29xzfaw.js → _server.ts-CTpcLUH8.js} +10 -10
- package/build/server/chunks/_server.ts-CTpcLUH8.js.map +1 -0
- package/build/server/chunks/{_server.ts-D4MNi4cD.js → _server.ts-Cf84YIaW.js} +3 -3
- package/build/server/chunks/{_server.ts-D4MNi4cD.js.map → _server.ts-Cf84YIaW.js.map} +1 -1
- package/build/server/chunks/{_server.ts-BL2FGb5Z.js → _server.ts-Ch-6iOHp.js} +99 -53
- package/build/server/chunks/_server.ts-Ch-6iOHp.js.map +1 -0
- package/build/server/chunks/_server.ts-CtH0dhUp.js +71 -0
- package/build/server/chunks/_server.ts-CtH0dhUp.js.map +1 -0
- package/build/server/chunks/_server.ts-DB_Kg97c.js +73 -0
- package/build/server/chunks/_server.ts-DB_Kg97c.js.map +1 -0
- package/build/server/chunks/{_server.ts-DfajWaqh.js → _server.ts-DV8zTCF9.js} +7 -9
- package/build/server/chunks/_server.ts-DV8zTCF9.js.map +1 -0
- package/build/server/chunks/_server.ts-DYpJImqd.js +99 -0
- package/build/server/chunks/_server.ts-DYpJImqd.js.map +1 -0
- package/build/server/chunks/{_server.ts-ColfDHW8.js → _server.ts-DYvb9ijZ.js} +21 -10
- package/build/server/chunks/_server.ts-DYvb9ijZ.js.map +1 -0
- package/build/server/chunks/{_server.ts-Cv_OrRuL.js → _server.ts-Deok2y88.js} +209 -34
- package/build/server/chunks/_server.ts-Deok2y88.js.map +1 -0
- package/build/server/chunks/{_server.ts-y9-WYDMa.js → _server.ts-WhTJBEJy.js} +5 -4
- package/build/server/chunks/{_server.ts-y9-WYDMa.js.map → _server.ts-WhTJBEJy.js.map} +1 -1
- package/build/server/chunks/{_server.ts-BjOJsoy4.js → _server.ts-XzT2UHM1.js} +6 -5
- package/build/server/chunks/_server.ts-XzT2UHM1.js.map +1 -0
- package/build/server/chunks/{_server.ts-BgdjBZco.js → _server.ts-tSpgyl1D.js} +7 -5
- package/build/server/chunks/_server.ts-tSpgyl1D.js.map +1 -0
- package/build/server/chunks/{_server.ts-BihKSdj_.js → _server.ts-vekTmWAx.js} +8 -8
- package/build/server/chunks/_server.ts-vekTmWAx.js.map +1 -0
- package/build/server/chunks/{auth-CEgFis71.js → auth-DeCdZ83n.js} +2 -2
- package/build/server/chunks/{auth-CEgFis71.js.map → auth-DeCdZ83n.js.map} +1 -1
- package/build/server/chunks/client-BdGHe_hY.js +25 -0
- package/build/server/chunks/client-BdGHe_hY.js.map +1 -0
- package/build/server/chunks/client2-CCBGA-2V.js +7 -0
- package/build/server/chunks/client2-CCBGA-2V.js.map +1 -0
- package/build/server/chunks/error-DDXB3duW.js +12 -0
- package/build/server/chunks/error-DDXB3duW.js.map +1 -0
- package/build/server/chunks/{exports-CJ0Q5XmL.js → index-DwaY1cAm.js} +1111 -1634
- package/build/server/chunks/index-DwaY1cAm.js.map +1 -0
- package/build/server/chunks/index-server-CrDaL06Y.js +9 -0
- package/build/server/chunks/index-server-CrDaL06Y.js.map +1 -0
- package/build/server/chunks/index2-CgclKpUj.js +58 -0
- package/build/server/chunks/index2-CgclKpUj.js.map +1 -0
- package/build/server/chunks/{library-apns-BHxLmuIx.js → library-apns-BqJbvSKh.js} +4 -4
- package/build/server/chunks/library-apns-BqJbvSKh.js.map +1 -0
- package/build/server/chunks/markdown-W_mTBct0.js +8 -0
- package/build/server/chunks/markdown-W_mTBct0.js.map +1 -0
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js +15 -0
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js.map +1 -0
- package/build/server/chunks/{pty-manager-C0FhBiVq.js → pty-manager-BQVB7IVj.js} +155 -326
- package/build/server/chunks/pty-manager-BQVB7IVj.js.map +1 -0
- package/build/server/chunks/root-DDSnEAZv.js +1171 -0
- package/build/server/chunks/root-DDSnEAZv.js.map +1 -0
- package/build/server/chunks/{shared-server-BDY8jh20.js → shared-server-sSGG17Df.js} +2 -3
- package/build/server/chunks/{shared-server-BDY8jh20.js.map → shared-server-sSGG17Df.js.map} +1 -1
- package/build/server/chunks/state.svelte-hBbXlUak.js +11 -0
- package/build/server/chunks/state.svelte-hBbXlUak.js.map +1 -0
- package/build/server/chunks/stores-DHNzYNpX.js +28 -0
- package/build/server/chunks/stores-DHNzYNpX.js.map +1 -0
- package/build/server/index.js +1085 -2242
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +39 -25
- package/build/server/manifest.js.map +1 -1
- package/package.json +32 -9
- package/scripts/fix-generated-types.sh +37 -0
- package/scripts/homebrew/shooter.rb +51 -0
- package/scripts/install.sh +348 -186
- package/scripts/setup.cjs +215 -45
- package/server.ts +114 -71
- package/src/app.css +12 -3
- package/src/app.d.ts +13 -20
- package/src/app.html +3 -2
- package/src/generated/types/API.ts +280 -0
- package/src/generated/types/APN.ts +186 -203
- package/src/generated/types/CLI.ts +18 -25
- package/src/generated/types/Client.ts +589 -0
- package/src/generated/types/Config.ts +53 -0
- package/src/generated/types/Holder.ts +638 -0
- package/src/generated/types/JWT.ts +39 -50
- package/src/generated/types/Notification.ts +426 -0
- package/src/generated/types/OpenCode.ts +356 -0
- package/src/generated/types/Sessions.ts +570 -0
- package/src/generated/types/Terminal.ts +2184 -2071
- package/src/generated/types/WsProtocol.ts +2004 -0
- package/src/generated/types/index.ts +9 -3
- package/src/lib/env.ts +29 -0
- package/src/lib/modules/client/common/cache.ts +10 -2
- package/src/lib/modules/client/common/config-guard.ts +37 -5
- package/src/lib/modules/client/common/error.ts +10 -0
- package/src/lib/modules/client/common/index.ts +6 -5
- package/src/lib/modules/client/common/markdown.ts +22 -1
- package/src/lib/modules/client/common/native-bridge.ts +28 -20
- package/src/lib/modules/client/common/time.ts +13 -11
- package/src/lib/modules/client/terminal/ChatView.svelte +354 -74
- package/src/lib/modules/client/terminal/CommandPalette.svelte +3 -2
- package/src/lib/modules/client/terminal/ConnectionStatus.svelte +7 -1
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +147 -84
- package/src/lib/modules/client/terminal/QuickKeys.svelte +3 -1
- package/src/lib/modules/client/terminal/ShortcutsHelp.svelte +2 -5
- package/src/lib/modules/client/terminal/keyboard-shortcuts.ts +27 -24
- package/src/lib/modules/client/terminal/xterm-wrapper.ts +74 -45
- package/src/lib/modules/server/apn/library-apns.ts +3 -2
- package/src/lib/modules/server/apn/notification-history.ts +2 -13
- package/src/lib/modules/server/apn/notification-sessions.ts +3 -13
- package/src/lib/modules/server/apn/pending-requests.ts +3 -8
- package/src/lib/modules/server/apn/types.ts +5 -4
- package/src/lib/modules/server/cli/index.ts +3 -2
- package/src/lib/modules/server/fcm/fcm-service.ts +8 -6
- package/src/lib/modules/server/sessions/jsonl-parser.ts +3 -3
- package/src/lib/modules/server/sessions/jsonl-reader.ts +86 -26
- package/src/lib/modules/server/sessions/opencode-db-path.ts +26 -0
- package/src/lib/modules/server/sessions/opencode-reader.ts +13 -15
- package/src/lib/modules/server/sessions/process-detector.ts +103 -0
- package/src/lib/modules/server/sessions/types.ts +11 -22
- package/src/lib/modules/server/terminal/holder-client.ts +272 -248
- package/src/lib/modules/server/terminal/opencode-watcher.ts +547 -556
- package/src/lib/modules/server/terminal/pty-holder.cjs +37 -8
- package/src/lib/modules/server/terminal/pty-manager.ts +157 -115
- package/src/lib/modules/server/terminal/session-watcher.ts +6 -4
- package/src/lib/modules/server/terminal/terminal-store.ts +131 -128
- package/src/lib/modules/server/utils/error.ts +9 -0
- package/src/lib/modules/server/ws/events-handler.ts +12 -6
- package/src/lib/modules/server/ws/keepalive.ts +86 -69
- package/src/lib/modules/server/ws/server.ts +43 -37
- package/src/lib/modules/server/ws/session-handler.ts +332 -147
- package/src/lib/modules/server/ws/terminal-handler.ts +29 -17
- package/src/lib/modules/server/ws/ticket-store.ts +29 -26
- package/src/lib/theme.css +30 -0
- package/src/lib/types/config.ts +1 -6
- package/src/routes/+error.svelte +94 -0
- package/src/routes/+layout.svelte +66 -31
- package/src/routes/+page.svelte +25 -22
- package/src/routes/api/debug/+server.ts +3 -1
- package/src/routes/api/device-token/+server.ts +60 -60
- package/src/routes/api/health/+server.ts +81 -73
- package/src/routes/api/notify/+server.ts +115 -68
- package/src/routes/api/qr-config/+server.ts +30 -32
- package/src/routes/api/response/+server.ts +9 -4
- package/src/routes/api/sessions/+server.ts +15 -5
- package/src/routes/api/sessions/connect/+server.ts +125 -0
- package/src/routes/api/sessions/detect/+server.ts +27 -0
- package/src/routes/api/terminals/+server.ts +26 -24
- package/src/routes/api/terminals/[id]/+server.ts +13 -7
- package/src/routes/api/terminals/[id]/paste-image/+server.ts +54 -52
- package/src/routes/api/terminals/[id]/resize/+server.ts +6 -3
- package/src/routes/api/webhook/+server.ts +8 -10
- package/src/routes/api/ws-status/+server.ts +7 -5
- package/src/routes/api/ws-ticket/+server.ts +42 -41
- package/src/routes/config/+page.svelte +149 -75
- package/src/routes/project/+page.svelte +165 -35
- package/src/routes/session/[id]/+page.svelte +479 -283
- package/src/routes/terminals/+page.svelte +58 -45
- package/src/routes/terminals/[id]/+page.svelte +223 -91
- package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.br +0 -0
- package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.gz +0 -0
- package/build/client/_app/immutable/assets/3.C0uFg0IS.css +0 -1
- package/build/client/_app/immutable/assets/3.C0uFg0IS.css.br +0 -0
- package/build/client/_app/immutable/assets/3.C0uFg0IS.css.gz +0 -0
- package/build/client/_app/immutable/assets/4.cJuCkJKZ.css +0 -1
- package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.br +0 -0
- package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.gz +0 -0
- package/build/client/_app/immutable/assets/5.DRjApZQW.css +0 -1
- package/build/client/_app/immutable/assets/5.DRjApZQW.css.br +0 -0
- package/build/client/_app/immutable/assets/5.DRjApZQW.css.gz +0 -0
- package/build/client/_app/immutable/assets/6.AraZrY8I.css +0 -1
- package/build/client/_app/immutable/assets/6.AraZrY8I.css.br +0 -0
- package/build/client/_app/immutable/assets/6.AraZrY8I.css.gz +0 -0
- package/build/client/_app/immutable/assets/7.BCJ1IuMx.css +0 -1
- package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.br +0 -0
- package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.gz +0 -0
- package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css +0 -1
- package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.br +0 -0
- package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.gz +0 -0
- package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css +0 -1
- package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.br +0 -0
- package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BNJphC1q.js +0 -56
- package/build/client/_app/immutable/chunks/BNJphC1q.js.br +0 -0
- package/build/client/_app/immutable/chunks/BNJphC1q.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bvk7mfPM.js +0 -1
- package/build/client/_app/immutable/chunks/Bvk7mfPM.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bvk7mfPM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CAokzuPQ.js +0 -1
- package/build/client/_app/immutable/chunks/CAokzuPQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/CAokzuPQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CGLrx-H5.js +0 -1
- package/build/client/_app/immutable/chunks/CGLrx-H5.js.br +0 -0
- package/build/client/_app/immutable/chunks/CGLrx-H5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CgCpWzEA.js +0 -1
- package/build/client/_app/immutable/chunks/CgCpWzEA.js.br +0 -0
- package/build/client/_app/immutable/chunks/CgCpWzEA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Cjwk_cGO.js +0 -6
- package/build/client/_app/immutable/chunks/Cjwk_cGO.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cjwk_cGO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CtQ8EED1.js +0 -11
- package/build/client/_app/immutable/chunks/CtQ8EED1.js.br +0 -0
- package/build/client/_app/immutable/chunks/CtQ8EED1.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DERQCisl.js +0 -1
- package/build/client/_app/immutable/chunks/DERQCisl.js.br +0 -0
- package/build/client/_app/immutable/chunks/DERQCisl.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DKrg8TQs.js +0 -1
- package/build/client/_app/immutable/chunks/DKrg8TQs.js.br +0 -0
- package/build/client/_app/immutable/chunks/DKrg8TQs.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dkkpz_4D.js +0 -126
- package/build/client/_app/immutable/chunks/Dkkpz_4D.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dkkpz_4D.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DoczjQhA.js +0 -1
- package/build/client/_app/immutable/chunks/DoczjQhA.js.br +0 -0
- package/build/client/_app/immutable/chunks/DoczjQhA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/RpcNruLP.js +0 -2
- package/build/client/_app/immutable/chunks/RpcNruLP.js.br +0 -0
- package/build/client/_app/immutable/chunks/RpcNruLP.js.gz +0 -0
- package/build/client/_app/immutable/chunks/a-St0Zwo.js +0 -1
- package/build/client/_app/immutable/chunks/a-St0Zwo.js.br +0 -0
- package/build/client/_app/immutable/chunks/a-St0Zwo.js.gz +0 -0
- package/build/client/_app/immutable/chunks/bo70OQUZ.js +0 -1
- package/build/client/_app/immutable/chunks/bo70OQUZ.js.br +0 -0
- package/build/client/_app/immutable/chunks/bo70OQUZ.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.QvGgdvTI.js +0 -2
- package/build/client/_app/immutable/entry/app.QvGgdvTI.js.br +0 -0
- package/build/client/_app/immutable/entry/app.QvGgdvTI.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BntDNRMC.js +0 -1
- package/build/client/_app/immutable/entry/start.BntDNRMC.js.br +0 -0
- package/build/client/_app/immutable/entry/start.BntDNRMC.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js +0 -1
- package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.MG1QhfrI.js +0 -1
- package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.B4MlOSh6.js +0 -1
- package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DIwYkjDn.js +0 -3
- package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.D-cIe70D.js +0 -1
- package/build/client/_app/immutable/nodes/4.D-cIe70D.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.D-cIe70D.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.D7zPRe3L.js +0 -1
- package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.BB7QE48r.js +0 -2
- package/build/client/_app/immutable/nodes/6.BB7QE48r.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.BB7QE48r.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.D8mqsrZG.js +0 -2
- package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.gz +0 -0
- package/build/client/manifest.webmanifest +0 -1
- package/build/client/registerSW.js +0 -1
- package/build/client/registerSW.js.br +0 -0
- package/build/client/registerSW.js.gz +0 -0
- package/build/client/sw.js +0 -222
- package/build/client/sw.js.br +0 -0
- package/build/client/sw.js.gz +0 -0
- package/build/client/workbox-5119daf5.js +0 -3395
- package/build/client/workbox-5119daf5.js.br +0 -0
- package/build/client/workbox-5119daf5.js.gz +0 -0
- package/build/server/chunks/0-q2IUp76Y.js +0 -9
- package/build/server/chunks/0-q2IUp76Y.js.map +0 -1
- package/build/server/chunks/1-CU50G5wZ.js +0 -9
- package/build/server/chunks/1-CU50G5wZ.js.map +0 -1
- package/build/server/chunks/2-D01t9s8T.js +0 -9
- package/build/server/chunks/2-D01t9s8T.js.map +0 -1
- package/build/server/chunks/3-5PUQ04wC.js +0 -9
- package/build/server/chunks/3-5PUQ04wC.js.map +0 -1
- package/build/server/chunks/4-e7gywnSG.js +0 -9
- package/build/server/chunks/4-e7gywnSG.js.map +0 -1
- package/build/server/chunks/5-CA1SA6KZ.js +0 -9
- package/build/server/chunks/5-CA1SA6KZ.js.map +0 -1
- package/build/server/chunks/6-71H221sV.js +0 -9
- package/build/server/chunks/6-71H221sV.js.map +0 -1
- package/build/server/chunks/7-Bo-vmdyz.js +0 -9
- package/build/server/chunks/7-Bo-vmdyz.js.map +0 -1
- package/build/server/chunks/_layout.svelte-SFHOxs74.js +0 -132
- package/build/server/chunks/_layout.svelte-SFHOxs74.js.map +0 -1
- package/build/server/chunks/_page.svelte-B4w-2wD-.js +0 -120
- package/build/server/chunks/_page.svelte-B4w-2wD-.js.map +0 -1
- package/build/server/chunks/_page.svelte-B_qAXjkh.js +0 -213
- package/build/server/chunks/_page.svelte-B_qAXjkh.js.map +0 -1
- package/build/server/chunks/_page.svelte-CsF1_TRG.js +0 -50
- package/build/server/chunks/_page.svelte-CsF1_TRG.js.map +0 -1
- package/build/server/chunks/_page.svelte-DJC6U-P0.js +0 -68
- package/build/server/chunks/_page.svelte-DJC6U-P0.js.map +0 -1
- package/build/server/chunks/_page.svelte-DQ6HBtsz.js +0 -407
- package/build/server/chunks/_page.svelte-DQ6HBtsz.js.map +0 -1
- package/build/server/chunks/_page.svelte-LbhhjP21.js +0 -148
- package/build/server/chunks/_page.svelte-LbhhjP21.js.map +0 -1
- package/build/server/chunks/_server.ts-BL2FGb5Z.js.map +0 -1
- package/build/server/chunks/_server.ts-BgdjBZco.js.map +0 -1
- package/build/server/chunks/_server.ts-BihKSdj_.js.map +0 -1
- package/build/server/chunks/_server.ts-BjOJsoy4.js.map +0 -1
- package/build/server/chunks/_server.ts-C29xzfaw.js.map +0 -1
- package/build/server/chunks/_server.ts-CPa6DgIt.js.map +0 -1
- package/build/server/chunks/_server.ts-CbDRDIoP.js.map +0 -1
- package/build/server/chunks/_server.ts-Cl1OEWL4.js +0 -54
- package/build/server/chunks/_server.ts-Cl1OEWL4.js.map +0 -1
- package/build/server/chunks/_server.ts-ColfDHW8.js.map +0 -1
- package/build/server/chunks/_server.ts-Cv_OrRuL.js.map +0 -1
- package/build/server/chunks/_server.ts-DRVbgm6k.js.map +0 -1
- package/build/server/chunks/_server.ts-DfajWaqh.js.map +0 -1
- package/build/server/chunks/client-CxCatAKr.js +0 -255
- package/build/server/chunks/client-CxCatAKr.js.map +0 -1
- package/build/server/chunks/error.svelte-BqdwMWdK.js +0 -26
- package/build/server/chunks/error.svelte-BqdwMWdK.js.map +0 -1
- package/build/server/chunks/exports-CJ0Q5XmL.js.map +0 -1
- package/build/server/chunks/index2-DAxIoAO-.js +0 -36
- package/build/server/chunks/index2-DAxIoAO-.js.map +0 -1
- package/build/server/chunks/jsonl-parser-dmZU_Hyu.js +0 -137
- package/build/server/chunks/jsonl-parser-dmZU_Hyu.js.map +0 -1
- package/build/server/chunks/library-apns-BHxLmuIx.js.map +0 -1
- package/build/server/chunks/markdown-Bxrl3cCF.js +0 -1241
- package/build/server/chunks/markdown-Bxrl3cCF.js.map +0 -1
- package/build/server/chunks/pty-manager-C0FhBiVq.js.map +0 -1
- package/build/server/chunks/stores-D0HorpgL.js +0 -36
- package/build/server/chunks/stores-D0HorpgL.js.map +0 -1
|
@@ -11,21 +11,23 @@
|
|
|
11
11
|
* mode to avoid contention.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import type {
|
|
15
|
+
OpenCodeMessage,
|
|
16
|
+
OpenCodePart,
|
|
17
|
+
OpenCodePartData,
|
|
18
|
+
OpenCodeSession,
|
|
19
|
+
} from '$generated/types';
|
|
20
|
+
|
|
14
21
|
import Database from 'better-sqlite3';
|
|
15
22
|
import * as fs from 'fs';
|
|
16
|
-
import * as path from 'path';
|
|
17
23
|
|
|
18
24
|
import type { ConversationMessage, MessagePart } from '../sessions/types';
|
|
19
25
|
|
|
26
|
+
import { resolveOpenCodeDbPath } from '../sessions/opencode-db-path';
|
|
27
|
+
|
|
20
28
|
// ── Constants ────────────────────────────────────────────────────────
|
|
21
29
|
|
|
22
|
-
const OPENCODE_DB_PATH = (
|
|
23
|
-
if (process.platform === 'darwin') {
|
|
24
|
-
return path.join(process.env.HOME || '', 'Library', 'Application Support', 'opencode', 'opencode.db');
|
|
25
|
-
}
|
|
26
|
-
const xdgData = process.env.XDG_DATA_HOME || path.join(process.env.HOME || '', '.local', 'share');
|
|
27
|
-
return path.join(xdgData, 'opencode', 'opencode.db');
|
|
28
|
-
})();
|
|
30
|
+
const OPENCODE_DB_PATH = resolveOpenCodeDbPath();
|
|
29
31
|
|
|
30
32
|
/** Poll interval in milliseconds. */
|
|
31
33
|
const POLL_INTERVAL_MS = 2000;
|
|
@@ -33,621 +35,610 @@ const POLL_INTERVAL_MS = 2000;
|
|
|
33
35
|
/** Maximum parameters per SQLite IN clause (SQLite limit is 999). */
|
|
34
36
|
const SQLITE_MAX_PARAMS = 500;
|
|
35
37
|
|
|
36
|
-
/**
|
|
37
|
-
* Normalise a timestamp from OpenCode's SQLite database to milliseconds.
|
|
38
|
-
*
|
|
39
|
-
* OpenCode currently stores `time_created` / `time_updated` as Unix
|
|
40
|
-
* **milliseconds**, but this is not formally documented and could change.
|
|
41
|
-
* A simple heuristic distinguishes seconds from milliseconds:
|
|
42
|
-
* - Values < 1e12 (~2001-09-09 in ms, ~33658 AD in seconds) are seconds.
|
|
43
|
-
* - Values >= 1e12 are already milliseconds.
|
|
44
|
-
*/
|
|
45
|
-
function toMillis(timestamp: number): number {
|
|
46
|
-
return timestamp < 1e12 ? timestamp * 1000 : timestamp;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── SQLite Row Types ─────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
interface OpenCodeMessage {
|
|
52
|
-
data: string; // JSON
|
|
53
|
-
id: string;
|
|
54
|
-
session_id: string;
|
|
55
|
-
time_created: number;
|
|
56
|
-
time_updated: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface OpenCodePart {
|
|
60
|
-
data: string; // JSON
|
|
61
|
-
id: string;
|
|
62
|
-
message_id: string;
|
|
63
|
-
session_id: string;
|
|
64
|
-
time_created: number;
|
|
65
|
-
time_updated: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Raw part data as stored in OpenCode's SQLite `part.data` JSON column. */
|
|
69
|
-
interface OpenCodePartData {
|
|
70
|
-
callID?: string;
|
|
71
|
-
id?: string;
|
|
72
|
-
state?: { input?: Record<string, unknown> };
|
|
73
|
-
text?: string;
|
|
74
|
-
tool?: string;
|
|
75
|
-
type: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
38
|
// ── Per-session Watcher State ────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
directory: string;
|
|
82
|
-
id: string;
|
|
83
|
-
time_created: number;
|
|
84
|
-
time_updated: number;
|
|
85
|
-
}
|
|
39
|
+
// WatchState is a runtime/behavioral type (callbacks, emittedSets,
|
|
40
|
+
// intervalHandle) and stays local.
|
|
86
41
|
|
|
87
42
|
interface WatchState {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
43
|
+
callbacks: Set<(messages: ConversationMessage[]) => void>;
|
|
44
|
+
/** Set of message IDs we have already emitted, to avoid duplicates. */
|
|
45
|
+
emittedMessageIds: Set<string>;
|
|
46
|
+
/** Map of part ID to last-seen time_updated, to detect in-place updates. */
|
|
47
|
+
emittedPartUpdatedAt: Map<string, number>;
|
|
48
|
+
intervalHandle: ReturnType<typeof setInterval>;
|
|
49
|
+
/** Highest time_created we have seen for messages (milliseconds). */
|
|
50
|
+
lastMessageTime: number;
|
|
51
|
+
/** Highest time_updated we have seen for parts (milliseconds). */
|
|
52
|
+
lastPartTime: number;
|
|
53
|
+
sessionId: string;
|
|
99
54
|
}
|
|
100
55
|
|
|
101
|
-
// ── OpenCodeWatcher Class ────────────────────────────────────────────
|
|
102
|
-
|
|
103
56
|
class OpenCodeWatcher {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
57
|
+
private watchers = new Map<string, WatchState>();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find the most recent non-archived OpenCode session that matches
|
|
61
|
+
* the given working directory. Checks session.directory equals or
|
|
62
|
+
* starts with `cwd`.
|
|
63
|
+
*
|
|
64
|
+
* Returns the session ID or null if none found.
|
|
65
|
+
*/
|
|
66
|
+
findSessionId(cwd: string, createdAfter?: number): null | string {
|
|
67
|
+
const db = openDb();
|
|
68
|
+
if (!db) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Match sessions that were active (updated) after the terminal was launched.
|
|
74
|
+
// OpenCode resumes existing sessions rather than always creating new ones,
|
|
75
|
+
// so we filter on time_updated (not time_created) to find the session
|
|
76
|
+
// that's being actively used by this terminal instance.
|
|
77
|
+
//
|
|
78
|
+
// createdAfter is JS milliseconds (Date.getTime()), but OpenCode may
|
|
79
|
+
// store time_updated in either seconds or milliseconds. Provide both
|
|
80
|
+
// representations and let the SQL match whichever unit the DB uses:
|
|
81
|
+
// - If time_updated is in seconds: compare against secondsFilter
|
|
82
|
+
// - If time_updated is in milliseconds (>= 1e12): compare against millisFilter
|
|
83
|
+
const millisFilter = createdAfter ?? 0;
|
|
84
|
+
const secondsFilter = createdAfter ? Math.floor(createdAfter / 1000) : 0;
|
|
85
|
+
// Escape LIKE metacharacters (%, _) in the cwd to prevent false
|
|
86
|
+
// matches on paths containing those characters. Uses \ as the
|
|
87
|
+
// escape character declared via the ESCAPE clause.
|
|
88
|
+
const cwdLikePattern = `${cwd.replace(/[%_\\]/g, '\\$&')}/%`;
|
|
89
|
+
const row = db
|
|
90
|
+
.prepare(
|
|
91
|
+
`
|
|
134
92
|
SELECT id
|
|
135
93
|
FROM session
|
|
136
94
|
WHERE (time_archived IS NULL OR time_archived = 0)
|
|
137
|
-
AND (directory = ? OR directory LIKE ?
|
|
138
|
-
AND
|
|
95
|
+
AND (directory = ? OR directory LIKE ? ESCAPE '\\')
|
|
96
|
+
AND (
|
|
97
|
+
(time_updated >= 1e12 AND time_updated > ?)
|
|
98
|
+
OR (time_updated < 1e12 AND time_updated > ?)
|
|
99
|
+
)
|
|
139
100
|
ORDER BY time_updated DESC
|
|
140
101
|
LIMIT 1
|
|
141
102
|
`
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
103
|
+
)
|
|
104
|
+
.get(cwd, cwdLikePattern, millisFilter, secondsFilter) as OpenCodeSession | undefined;
|
|
105
|
+
|
|
106
|
+
return row?.id ?? null;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('[opencode-watcher] Failed to find session:', error);
|
|
109
|
+
return null;
|
|
110
|
+
} finally {
|
|
111
|
+
db.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read all messages and parts for a session from SQLite, converting
|
|
117
|
+
* them to ConversationMessage format.
|
|
118
|
+
*/
|
|
119
|
+
getHistory(sessionId: string): ConversationMessage[] {
|
|
120
|
+
const db = openDb();
|
|
121
|
+
if (!db) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Fetch all messages for this session, ordered chronologically.
|
|
127
|
+
const messages = db
|
|
128
|
+
.prepare(
|
|
129
|
+
`
|
|
169
130
|
SELECT id, session_id, time_created, time_updated, data
|
|
170
131
|
FROM message
|
|
171
132
|
WHERE session_id = ?
|
|
172
133
|
ORDER BY time_created ASC
|
|
173
134
|
`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
135
|
+
)
|
|
136
|
+
.all(sessionId) as OpenCodeMessage[];
|
|
137
|
+
|
|
138
|
+
if (messages.length === 0) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fetch all parts for these messages (batched to avoid SQLite param limit).
|
|
143
|
+
const messageIds = messages.map((m) => m.id);
|
|
144
|
+
const parts = batchInQuery<OpenCodePart>(
|
|
145
|
+
db,
|
|
146
|
+
`SELECT id, message_id, session_id, time_created, time_updated, data
|
|
186
147
|
FROM part
|
|
187
148
|
WHERE message_id IN (__PLACEHOLDERS__)
|
|
188
149
|
ORDER BY time_created ASC`,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
150
|
+
messageIds
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Group parts by message ID.
|
|
154
|
+
const partsByMessage = new Map<string, OpenCodePart[]>();
|
|
155
|
+
for (const part of parts) {
|
|
156
|
+
if (!partsByMessage.has(part.message_id)) {
|
|
157
|
+
partsByMessage.set(part.message_id, []);
|
|
158
|
+
}
|
|
159
|
+
partsByMessage.get(part.message_id)!.push(part);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return this.buildMessages(messages, partsByMessage);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[opencode-watcher] Failed to read history:', error);
|
|
165
|
+
return [];
|
|
166
|
+
} finally {
|
|
167
|
+
db.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Stop watching a specific session. If a callback is provided, only that
|
|
173
|
+
* subscriber is removed — the interval keeps running while other subscribers
|
|
174
|
+
* remain. If no callback is provided, all subscribers and the interval are
|
|
175
|
+
* cleared (backward compat).
|
|
176
|
+
*/
|
|
177
|
+
stop(sessionId: string, callback?: (messages: ConversationMessage[]) => void): void {
|
|
178
|
+
const state = this.watchers.get(sessionId);
|
|
179
|
+
if (!state) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (callback) {
|
|
184
|
+
state.callbacks.delete(callback);
|
|
185
|
+
console.log(
|
|
186
|
+
`[opencode-watcher] Removed subscriber from session: ${sessionId} ` +
|
|
187
|
+
`(remaining=${state.callbacks.size})`
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Only tear down the interval when no subscribers remain.
|
|
191
|
+
if (state.callbacks.size > 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
clearInterval(state.intervalHandle);
|
|
197
|
+
this.watchers.delete(sessionId);
|
|
198
|
+
console.log(`[opencode-watcher] Stopped watching session: ${sessionId}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Stop all active watchers.
|
|
203
|
+
*/
|
|
204
|
+
stopAll(): void {
|
|
205
|
+
for (const [sessionId] of this.watchers) {
|
|
206
|
+
this.stop(sessionId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Start polling the SQLite DB every 2 seconds for new messages/parts
|
|
212
|
+
* in the given session. Converts new data to ConversationMessage
|
|
213
|
+
* format and invokes the callback.
|
|
214
|
+
*/
|
|
215
|
+
watch(sessionId: string, callback: (messages: ConversationMessage[]) => void): void {
|
|
216
|
+
const existing = this.watchers.get(sessionId);
|
|
217
|
+
if (existing) {
|
|
218
|
+
// Already watching — just add the new callback, don't create a new interval.
|
|
219
|
+
existing.callbacks.add(callback);
|
|
220
|
+
console.log(
|
|
221
|
+
`[opencode-watcher] Added subscriber to session: ${sessionId} ` +
|
|
222
|
+
`(total=${existing.callbacks.size})`
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Determine the initial high-water marks by scanning existing data.
|
|
228
|
+
const { emittedMessageIds, emittedPartUpdatedAt, lastMessageTime, lastPartTime } =
|
|
229
|
+
this.getHighWaterMarks(sessionId);
|
|
230
|
+
|
|
231
|
+
const intervalHandle = setInterval(() => {
|
|
232
|
+
this.poll(sessionId);
|
|
233
|
+
}, POLL_INTERVAL_MS);
|
|
234
|
+
|
|
235
|
+
const state: WatchState = {
|
|
236
|
+
callbacks: new Set([callback]),
|
|
237
|
+
emittedMessageIds,
|
|
238
|
+
emittedPartUpdatedAt,
|
|
239
|
+
intervalHandle,
|
|
240
|
+
lastMessageTime,
|
|
241
|
+
lastPartTime,
|
|
242
|
+
sessionId,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
this.watchers.set(sessionId, state);
|
|
246
|
+
console.log(
|
|
247
|
+
`[opencode-watcher] Watching session: ${sessionId} ` +
|
|
248
|
+
`(lastMsg=${lastMessageTime}, lastPart=${lastPartTime})`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Private Helpers ────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Convert OpenCode messages + their parts into ConversationMessage
|
|
256
|
+
* objects for consumption by the session handler.
|
|
257
|
+
*/
|
|
258
|
+
private buildMessages(
|
|
259
|
+
messages: OpenCodeMessage[],
|
|
260
|
+
partsByMessage: Map<string, OpenCodePart[]>
|
|
261
|
+
): ConversationMessage[] {
|
|
262
|
+
const result: ConversationMessage[] = [];
|
|
263
|
+
|
|
264
|
+
for (const msg of messages) {
|
|
265
|
+
// Parse message data to determine role.
|
|
266
|
+
let msgData: { agent?: string; role?: string } = {};
|
|
267
|
+
try {
|
|
268
|
+
msgData = JSON.parse(msg.data) as typeof msgData;
|
|
269
|
+
} catch {
|
|
270
|
+
// Skip unparseable message data.
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const role = msgData.role === 'user' ? 'user' : 'assistant';
|
|
275
|
+
const msgParts = partsByMessage.get(msg.id) || [];
|
|
276
|
+
|
|
277
|
+
// Convert each part to a MessagePart.
|
|
278
|
+
const parts: MessagePart[] = [];
|
|
279
|
+
|
|
280
|
+
for (const part of msgParts) {
|
|
281
|
+
let partData: OpenCodePartData;
|
|
282
|
+
try {
|
|
283
|
+
partData = JSON.parse(part.data) as OpenCodePartData;
|
|
284
|
+
} catch {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const converted = convertPartToMessagePart(partData);
|
|
289
|
+
if (converted) {
|
|
290
|
+
parts.push(converted);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Skip messages with no usable content.
|
|
295
|
+
if (parts.length === 0) {
|
|
296
|
+
console.debug(`[opencode-watcher] Skipping message ${msg.id} (no usable parts)`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
result.push({
|
|
301
|
+
id: msg.id,
|
|
302
|
+
parts,
|
|
303
|
+
role,
|
|
304
|
+
timestamp: new Date(toMillis(msg.time_created)).toISOString(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Scan existing messages/parts to determine the starting high-water
|
|
313
|
+
* marks for time-based polling. Also collects the set of already-seen
|
|
314
|
+
* message IDs so we do not re-emit them.
|
|
315
|
+
*/
|
|
316
|
+
private getHighWaterMarks(sessionId: string): {
|
|
317
|
+
emittedMessageIds: Set<string>;
|
|
318
|
+
emittedPartUpdatedAt: Map<string, number>;
|
|
319
|
+
lastMessageTime: number;
|
|
320
|
+
lastPartTime: number;
|
|
321
|
+
} {
|
|
322
|
+
const db = openDb();
|
|
323
|
+
if (!db) {
|
|
324
|
+
return {
|
|
325
|
+
emittedMessageIds: new Set(),
|
|
326
|
+
emittedPartUpdatedAt: new Map(),
|
|
327
|
+
lastMessageTime: 0,
|
|
328
|
+
lastPartTime: 0,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const msgRow = db
|
|
334
|
+
.prepare(
|
|
335
|
+
`
|
|
370
336
|
SELECT MAX(time_created) as maxTime
|
|
371
337
|
FROM message
|
|
372
338
|
WHERE session_id = ?
|
|
373
339
|
`
|
|
374
|
-
|
|
375
|
-
|
|
340
|
+
)
|
|
341
|
+
.get(sessionId) as undefined | { maxTime: null | number };
|
|
376
342
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
343
|
+
const partRow = db
|
|
344
|
+
.prepare(
|
|
345
|
+
`
|
|
380
346
|
SELECT MAX(time_updated) as maxTime
|
|
381
347
|
FROM part
|
|
382
348
|
WHERE session_id = ?
|
|
383
349
|
`
|
|
384
|
-
|
|
385
|
-
|
|
350
|
+
)
|
|
351
|
+
.get(sessionId) as undefined | { maxTime: null | number };
|
|
386
352
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
353
|
+
// Collect all existing message IDs.
|
|
354
|
+
const existingIds = db
|
|
355
|
+
.prepare(
|
|
356
|
+
`
|
|
391
357
|
SELECT id FROM message WHERE session_id = ?
|
|
392
358
|
`
|
|
393
|
-
|
|
394
|
-
|
|
359
|
+
)
|
|
360
|
+
.all(sessionId) as { id: string }[];
|
|
395
361
|
|
|
396
|
-
|
|
362
|
+
const emittedMessageIds = new Set(existingIds.map((r) => r.id));
|
|
397
363
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
SELECT id FROM part WHERE session_id = ?
|
|
364
|
+
// Collect all existing part IDs with their time_updated for change detection.
|
|
365
|
+
const existingParts = db
|
|
366
|
+
.prepare(
|
|
367
|
+
`
|
|
368
|
+
SELECT id, time_updated FROM part WHERE session_id = ?
|
|
403
369
|
`
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
370
|
+
)
|
|
371
|
+
.all(sessionId) as { id: string; time_updated: number }[];
|
|
372
|
+
|
|
373
|
+
const emittedPartUpdatedAt = new Map<string, number>();
|
|
374
|
+
for (const p of existingParts) {
|
|
375
|
+
emittedPartUpdatedAt.set(p.id, p.time_updated);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
emittedMessageIds,
|
|
380
|
+
emittedPartUpdatedAt,
|
|
381
|
+
lastMessageTime: msgRow?.maxTime ?? 0,
|
|
382
|
+
lastPartTime: partRow?.maxTime ?? 0,
|
|
383
|
+
};
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('[opencode-watcher] Failed to get high-water marks:', error);
|
|
386
|
+
return {
|
|
387
|
+
emittedMessageIds: new Set(),
|
|
388
|
+
emittedPartUpdatedAt: new Map(),
|
|
389
|
+
lastMessageTime: 0,
|
|
390
|
+
lastPartTime: 0,
|
|
391
|
+
};
|
|
392
|
+
} finally {
|
|
393
|
+
db.close();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Single poll iteration. Opens the DB, queries for new messages and
|
|
399
|
+
* updated parts, converts them to ConversationMessage[], and invokes
|
|
400
|
+
* the watcher callbacks.
|
|
401
|
+
*/
|
|
402
|
+
private poll(sessionId: string): void {
|
|
403
|
+
const state = this.watchers.get(sessionId);
|
|
404
|
+
if (!state) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const db = openDb();
|
|
409
|
+
if (!db) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const results: ConversationMessage[] = [];
|
|
415
|
+
|
|
416
|
+
// ── 1. Check for brand-new messages ──────────────────────────
|
|
417
|
+
const newMessages = db
|
|
418
|
+
.prepare(
|
|
419
|
+
`
|
|
446
420
|
SELECT id, session_id, time_created, time_updated, data
|
|
447
421
|
FROM message
|
|
448
422
|
WHERE session_id = ? AND time_created > ?
|
|
449
423
|
ORDER BY time_created ASC
|
|
450
424
|
`
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
db,
|
|
467
|
-
`SELECT id, message_id, session_id, time_created, time_updated, data
|
|
425
|
+
)
|
|
426
|
+
.all(sessionId, state.lastMessageTime) as OpenCodeMessage[];
|
|
427
|
+
|
|
428
|
+
if (newMessages.length > 0) {
|
|
429
|
+
// Deduplicate: skip messages we have already emitted (guards
|
|
430
|
+
// against clock-tie edge cases where time_created equals the
|
|
431
|
+
// high-water mark and the row slips through the > filter again).
|
|
432
|
+
const dedupedMessages = newMessages.filter((m) => !state.emittedMessageIds.has(m.id));
|
|
433
|
+
|
|
434
|
+
if (dedupedMessages.length > 0) {
|
|
435
|
+
// Fetch parts for these new messages (batched to avoid SQLite param limit).
|
|
436
|
+
const newMsgIds = dedupedMessages.map((m) => m.id);
|
|
437
|
+
const newParts = batchInQuery<OpenCodePart>(
|
|
438
|
+
db,
|
|
439
|
+
`SELECT id, message_id, session_id, time_created, time_updated, data
|
|
468
440
|
FROM part
|
|
469
441
|
WHERE message_id IN (__PLACEHOLDERS__)
|
|
470
442
|
ORDER BY time_created ASC`,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
443
|
+
newMsgIds
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Group parts by message.
|
|
447
|
+
const partsByMessage = new Map<string, OpenCodePart[]>();
|
|
448
|
+
for (const part of newParts) {
|
|
449
|
+
if (!partsByMessage.has(part.message_id)) {
|
|
450
|
+
partsByMessage.set(part.message_id, []);
|
|
451
|
+
}
|
|
452
|
+
partsByMessage.get(part.message_id)!.push(part);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const newEntries = this.buildMessages(dedupedMessages, partsByMessage);
|
|
456
|
+
results.push(...newEntries);
|
|
457
|
+
|
|
458
|
+
// Track emitted part timestamps and update part high-water mark.
|
|
459
|
+
for (const part of newParts) {
|
|
460
|
+
if (part.time_updated > state.lastPartTime) {
|
|
461
|
+
state.lastPartTime = part.time_updated;
|
|
462
|
+
}
|
|
463
|
+
state.emittedPartUpdatedAt.set(part.id, part.time_updated);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Always update message high-water marks (even for already-emitted messages).
|
|
468
|
+
for (const msg of newMessages) {
|
|
469
|
+
if (msg.time_created > state.lastMessageTime) {
|
|
470
|
+
state.lastMessageTime = msg.time_created;
|
|
471
|
+
}
|
|
472
|
+
state.emittedMessageIds.add(msg.id);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── 2. Check for updated parts on existing messages ──────────
|
|
477
|
+
// Parts may be added or updated after the initial message row is
|
|
478
|
+
// created (e.g., streaming assistant response). Look for parts
|
|
479
|
+
// whose time_updated exceeds our last-seen mark, but whose parent
|
|
480
|
+
// message is NOT in the new-messages set (those were handled above).
|
|
481
|
+
const updatedParts = db
|
|
482
|
+
.prepare(
|
|
483
|
+
`
|
|
512
484
|
SELECT id, message_id, session_id, time_created, time_updated, data
|
|
513
485
|
FROM part
|
|
514
486
|
WHERE session_id = ? AND time_updated > ?
|
|
515
487
|
ORDER BY time_created ASC
|
|
516
488
|
`
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
489
|
+
)
|
|
490
|
+
.all(sessionId, state.lastPartTime) as OpenCodePart[];
|
|
491
|
+
|
|
492
|
+
if (updatedParts.length > 0) {
|
|
493
|
+
// Filter out parts that belong to messages we just processed.
|
|
494
|
+
// For previously-emitted parts, allow re-emission only if
|
|
495
|
+
// time_updated has increased (in-place update detection).
|
|
496
|
+
const newMsgIdSet = new Set(newMessages.map((m) => m.id));
|
|
497
|
+
const newParts = updatedParts.filter((p) => {
|
|
498
|
+
if (newMsgIdSet.has(p.message_id)) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const lastSeen = state.emittedPartUpdatedAt.get(p.id);
|
|
502
|
+
// New part (never emitted) or updated since last emission
|
|
503
|
+
return lastSeen === undefined || p.time_updated > lastSeen;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
if (newParts.length > 0) {
|
|
507
|
+
// Group the NEW parts by message ID.
|
|
508
|
+
const partsByMessage = new Map<string, OpenCodePart[]>();
|
|
509
|
+
for (const part of newParts) {
|
|
510
|
+
if (!partsByMessage.has(part.message_id)) {
|
|
511
|
+
partsByMessage.set(part.message_id, []);
|
|
512
|
+
}
|
|
513
|
+
partsByMessage.get(part.message_id)!.push(part);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Fetch the parent messages for context (batched to avoid SQLite param limit).
|
|
517
|
+
const affectedMsgIds = [...partsByMessage.keys()];
|
|
518
|
+
const affectedMessages = batchInQuery<OpenCodeMessage>(
|
|
519
|
+
db,
|
|
520
|
+
`SELECT id, session_id, time_created, time_updated, data
|
|
543
521
|
FROM message
|
|
544
522
|
WHERE id IN (__PLACEHOLDERS__)
|
|
545
523
|
ORDER BY time_created ASC`,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
524
|
+
affectedMsgIds
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Build entries from ONLY the new parts (delta), not all
|
|
528
|
+
// parts for the message. This prevents re-emitting content
|
|
529
|
+
// the session handler has already seen.
|
|
530
|
+
const updatedEntries = this.buildMessages(affectedMessages, partsByMessage);
|
|
531
|
+
results.push(...updatedEntries);
|
|
532
|
+
|
|
533
|
+
// Track emitted part timestamps for change detection.
|
|
534
|
+
for (const part of newParts) {
|
|
535
|
+
state.emittedPartUpdatedAt.set(part.id, part.time_updated);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Update part high-water mark.
|
|
540
|
+
for (const part of updatedParts) {
|
|
541
|
+
if (part.time_updated > state.lastPartTime) {
|
|
542
|
+
state.lastPartTime = part.time_updated;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── 3. Invoke all callbacks if there are new entries ─────────
|
|
548
|
+
if (results.length > 0) {
|
|
549
|
+
for (const cb of state.callbacks) {
|
|
550
|
+
try {
|
|
551
|
+
cb(results);
|
|
552
|
+
} catch (cbError) {
|
|
553
|
+
console.error('[opencode-watcher] Callback error:', cbError);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
// Log but do not crash — the next poll will retry.
|
|
559
|
+
console.error('[opencode-watcher] Poll error:', error);
|
|
560
|
+
} finally {
|
|
561
|
+
db.close();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
586
564
|
}
|
|
587
565
|
|
|
588
|
-
// ──
|
|
566
|
+
// ── OpenCodeWatcher Class ────────────────────────────────────────────
|
|
589
567
|
|
|
590
568
|
/**
|
|
591
569
|
* Execute a SELECT query with an IN clause, batching in chunks of
|
|
592
570
|
* SQLITE_MAX_PARAMS to stay within SQLite's 999-parameter limit.
|
|
593
571
|
*/
|
|
594
|
-
function batchInQuery<T>(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
return results;
|
|
572
|
+
function batchInQuery<T>(db: Database.Database, sql: string, ids: string[]): T[] {
|
|
573
|
+
if (ids.length === 0) {
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const results: T[] = [];
|
|
578
|
+
for (let i = 0; i < ids.length; i += SQLITE_MAX_PARAMS) {
|
|
579
|
+
const chunk = ids.slice(i, i + SQLITE_MAX_PARAMS);
|
|
580
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
581
|
+
const query = sql.replace('__PLACEHOLDERS__', placeholders);
|
|
582
|
+
const rows = db.prepare(query).all(...chunk) as T[];
|
|
583
|
+
results.push(...rows);
|
|
584
|
+
}
|
|
585
|
+
return results;
|
|
610
586
|
}
|
|
611
587
|
|
|
612
|
-
// ──
|
|
613
|
-
// Maps OpenCode part types to MessagePart directly, skipping the
|
|
614
|
-
// intermediate Record<string, unknown> stage.
|
|
588
|
+
// ── Database Helpers ─────────────────────────────────────────────────
|
|
615
589
|
|
|
616
590
|
function convertPartToMessagePart(data: OpenCodePartData): MessagePart | null {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
591
|
+
switch (data.type) {
|
|
592
|
+
case 'reasoning':
|
|
593
|
+
return { content: data.text || '', type: 'thinking' };
|
|
594
|
+
|
|
595
|
+
case 'text':
|
|
596
|
+
return { content: data.text || '', type: 'text' };
|
|
597
|
+
|
|
598
|
+
case 'tool':
|
|
599
|
+
return {
|
|
600
|
+
id: data.callID || data.id || '',
|
|
601
|
+
input: data.state?.input || {},
|
|
602
|
+
toolName: data.tool || 'Unknown',
|
|
603
|
+
type: 'tool_use',
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
default:
|
|
607
|
+
// Skip snapshot, patch, step-start, step-finish, subtask, retry, compaction
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
636
610
|
}
|
|
637
611
|
|
|
612
|
+
// ── Part Conversion ──────────────────────────────────────────────────
|
|
613
|
+
// Maps OpenCode part types to MessagePart directly, skipping the
|
|
614
|
+
// intermediate Record<string, unknown> stage.
|
|
615
|
+
|
|
638
616
|
/**
|
|
639
617
|
* Open the OpenCode SQLite database in read-only mode.
|
|
640
618
|
* Returns null if the file does not exist or cannot be opened.
|
|
641
619
|
*/
|
|
642
620
|
function openDb(): Database.Database | null {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
621
|
+
if (!fs.existsSync(OPENCODE_DB_PATH)) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
return new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
626
|
+
} catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Normalise a timestamp from OpenCode's SQLite database to milliseconds.
|
|
633
|
+
*
|
|
634
|
+
* OpenCode currently stores `time_created` / `time_updated` as Unix
|
|
635
|
+
* **milliseconds**, but this is not formally documented and could change.
|
|
636
|
+
* A simple heuristic distinguishes seconds from milliseconds:
|
|
637
|
+
* - Values < 1e12 (~2001-09-09 in ms, ~33658 AD in seconds) are seconds.
|
|
638
|
+
* - Values >= 1e12 are already milliseconds.
|
|
639
|
+
*/
|
|
640
|
+
function toMillis(timestamp: number): number {
|
|
641
|
+
return timestamp < 1e12 ? timestamp * 1000 : timestamp;
|
|
651
642
|
}
|
|
652
643
|
|
|
653
644
|
// ── Singleton ────────────────────────────────────────────────────────
|
|
@@ -656,6 +647,6 @@ function openDb(): Database.Database | null {
|
|
|
656
647
|
|
|
657
648
|
const OW_GLOBAL_KEY = '__shooter_opencode_watcher';
|
|
658
649
|
export const openCodeWatcher: OpenCodeWatcher =
|
|
659
|
-
|
|
660
|
-
|
|
650
|
+
((globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] as OpenCodeWatcher) ||
|
|
651
|
+
new OpenCodeWatcher();
|
|
661
652
|
(globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] = openCodeWatcher;
|