@juspay/shooter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/notifier.cjs +1431 -0
- package/.claude/settings.json +162 -0
- package/README.md +515 -0
- package/bin/shooter.cjs +141 -0
- package/build/client/_app/immutable/assets/0.CM9Hl6d-.css +1 -0
- 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/2.CAShZ7lQ.css +1 -0
- package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.br +1 -0
- package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.gz +0 -0
- package/build/client/_app/immutable/assets/3.C0uFg0IS.css +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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/assets/xterm.DFuMZ0ql.css +1 -0
- package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.br +0 -0
- package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BNJphC1q.js +56 -0
- 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/BTGVxaYV.js +9 -0
- package/build/client/_app/immutable/chunks/BTGVxaYV.js.br +0 -0
- package/build/client/_app/immutable/chunks/BTGVxaYV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BlxrFPDK.js +1 -0
- package/build/client/_app/immutable/chunks/BlxrFPDK.js.br +0 -0
- package/build/client/_app/immutable/chunks/BlxrFPDK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bvk7mfPM.js +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +6 -0
- 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 +11 -0
- 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 +1 -0
- 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 +1 -0
- 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/DLu6yJIZ.js +1 -0
- package/build/client/_app/immutable/chunks/DLu6yJIZ.js.br +0 -0
- package/build/client/_app/immutable/chunks/DLu6yJIZ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dkkpz_4D.js +126 -0
- 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 +1 -0
- 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/PPVm8Dsz.js +1 -0
- package/build/client/_app/immutable/chunks/PPVm8Dsz.js.br +0 -0
- package/build/client/_app/immutable/chunks/PPVm8Dsz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/RpcNruLP.js +2 -0
- 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 +1 -0
- 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 +1 -0
- 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 +2 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +3 -0
- 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 +1 -0
- 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 +1 -0
- 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 +2 -0
- 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 +2 -0
- 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/_app/version.json +1 -0
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/client/app-icon.png +0 -0
- package/build/client/apple-touch-icon.png +0 -0
- package/build/client/favicon.png +0 -0
- package/build/client/favicon.svg +10 -0
- package/build/client/favicon.svg.br +0 -0
- package/build/client/favicon.svg.gz +0 -0
- package/build/client/manifest.webmanifest +1 -0
- package/build/client/pwa-192x192.png +0 -0
- package/build/client/pwa-512x512.png +0 -0
- package/build/client/registerSW.js +1 -0
- package/build/client/registerSW.js.br +0 -0
- package/build/client/registerSW.js.gz +0 -0
- package/build/client/sw.js +222 -0
- package/build/client/sw.js.br +0 -0
- package/build/client/sw.js.gz +0 -0
- package/build/client/workbox-5119daf5.js +3395 -0
- package/build/client/workbox-5119daf5.js.br +0 -0
- package/build/client/workbox-5119daf5.js.gz +0 -0
- package/build/env.js +94 -0
- package/build/handler.js +1494 -0
- package/build/index.js +345 -0
- package/build/pty-holder.cjs +510 -0
- package/build/server/chunks/0-q2IUp76Y.js +9 -0
- package/build/server/chunks/0-q2IUp76Y.js.map +1 -0
- package/build/server/chunks/1-CU50G5wZ.js +9 -0
- package/build/server/chunks/1-CU50G5wZ.js.map +1 -0
- package/build/server/chunks/2-D01t9s8T.js +9 -0
- package/build/server/chunks/2-D01t9s8T.js.map +1 -0
- package/build/server/chunks/3-5PUQ04wC.js +9 -0
- package/build/server/chunks/3-5PUQ04wC.js.map +1 -0
- package/build/server/chunks/4-e7gywnSG.js +9 -0
- package/build/server/chunks/4-e7gywnSG.js.map +1 -0
- package/build/server/chunks/5-CA1SA6KZ.js +9 -0
- package/build/server/chunks/5-CA1SA6KZ.js.map +1 -0
- package/build/server/chunks/6-71H221sV.js +9 -0
- package/build/server/chunks/6-71H221sV.js.map +1 -0
- package/build/server/chunks/7-Bo-vmdyz.js +9 -0
- package/build/server/chunks/7-Bo-vmdyz.js.map +1 -0
- package/build/server/chunks/_layout.svelte-SFHOxs74.js +132 -0
- package/build/server/chunks/_layout.svelte-SFHOxs74.js.map +1 -0
- package/build/server/chunks/_page.svelte-B4w-2wD-.js +120 -0
- package/build/server/chunks/_page.svelte-B4w-2wD-.js.map +1 -0
- package/build/server/chunks/_page.svelte-B_qAXjkh.js +213 -0
- package/build/server/chunks/_page.svelte-B_qAXjkh.js.map +1 -0
- package/build/server/chunks/_page.svelte-CsF1_TRG.js +50 -0
- package/build/server/chunks/_page.svelte-CsF1_TRG.js.map +1 -0
- package/build/server/chunks/_page.svelte-DJC6U-P0.js +68 -0
- package/build/server/chunks/_page.svelte-DJC6U-P0.js.map +1 -0
- package/build/server/chunks/_page.svelte-DQ6HBtsz.js +407 -0
- package/build/server/chunks/_page.svelte-DQ6HBtsz.js.map +1 -0
- package/build/server/chunks/_page.svelte-LbhhjP21.js +148 -0
- package/build/server/chunks/_page.svelte-LbhhjP21.js.map +1 -0
- package/build/server/chunks/_server.ts-BL2FGb5Z.js +387 -0
- package/build/server/chunks/_server.ts-BL2FGb5Z.js.map +1 -0
- package/build/server/chunks/_server.ts-BgdjBZco.js +47 -0
- package/build/server/chunks/_server.ts-BgdjBZco.js.map +1 -0
- package/build/server/chunks/_server.ts-BihKSdj_.js +59 -0
- package/build/server/chunks/_server.ts-BihKSdj_.js.map +1 -0
- package/build/server/chunks/_server.ts-BjOJsoy4.js +63 -0
- package/build/server/chunks/_server.ts-BjOJsoy4.js.map +1 -0
- package/build/server/chunks/_server.ts-C29xzfaw.js +77 -0
- package/build/server/chunks/_server.ts-C29xzfaw.js.map +1 -0
- package/build/server/chunks/_server.ts-CPa6DgIt.js +71 -0
- package/build/server/chunks/_server.ts-CPa6DgIt.js.map +1 -0
- package/build/server/chunks/_server.ts-CbDRDIoP.js +36 -0
- package/build/server/chunks/_server.ts-CbDRDIoP.js.map +1 -0
- package/build/server/chunks/_server.ts-Cl1OEWL4.js +54 -0
- package/build/server/chunks/_server.ts-Cl1OEWL4.js.map +1 -0
- package/build/server/chunks/_server.ts-ColfDHW8.js +60 -0
- package/build/server/chunks/_server.ts-ColfDHW8.js.map +1 -0
- package/build/server/chunks/_server.ts-Cv_OrRuL.js +494 -0
- package/build/server/chunks/_server.ts-Cv_OrRuL.js.map +1 -0
- package/build/server/chunks/_server.ts-D4MNi4cD.js +25 -0
- package/build/server/chunks/_server.ts-D4MNi4cD.js.map +1 -0
- package/build/server/chunks/_server.ts-DRVbgm6k.js +125 -0
- package/build/server/chunks/_server.ts-DRVbgm6k.js.map +1 -0
- package/build/server/chunks/_server.ts-DfajWaqh.js +39 -0
- package/build/server/chunks/_server.ts-DfajWaqh.js.map +1 -0
- package/build/server/chunks/_server.ts-y9-WYDMa.js +35 -0
- package/build/server/chunks/_server.ts-y9-WYDMa.js.map +1 -0
- package/build/server/chunks/auth-CEgFis71.js +32 -0
- package/build/server/chunks/auth-CEgFis71.js.map +1 -0
- package/build/server/chunks/client-CxCatAKr.js +255 -0
- package/build/server/chunks/client-CxCatAKr.js.map +1 -0
- package/build/server/chunks/error.svelte-BqdwMWdK.js +26 -0
- package/build/server/chunks/error.svelte-BqdwMWdK.js.map +1 -0
- package/build/server/chunks/exports-CJ0Q5XmL.js +4081 -0
- package/build/server/chunks/exports-CJ0Q5XmL.js.map +1 -0
- package/build/server/chunks/index2-DAxIoAO-.js +36 -0
- package/build/server/chunks/index2-DAxIoAO-.js.map +1 -0
- package/build/server/chunks/jsonl-parser-dmZU_Hyu.js +137 -0
- package/build/server/chunks/jsonl-parser-dmZU_Hyu.js.map +1 -0
- package/build/server/chunks/library-apns-BHxLmuIx.js +104 -0
- package/build/server/chunks/library-apns-BHxLmuIx.js.map +1 -0
- package/build/server/chunks/markdown-Bxrl3cCF.js +1241 -0
- package/build/server/chunks/markdown-Bxrl3cCF.js.map +1 -0
- package/build/server/chunks/pending-requests-D8UiTw7L.js +44 -0
- package/build/server/chunks/pending-requests-D8UiTw7L.js.map +1 -0
- package/build/server/chunks/pty-manager-C0FhBiVq.js +1697 -0
- package/build/server/chunks/pty-manager-C0FhBiVq.js.map +1 -0
- package/build/server/chunks/shared-server-BDY8jh20.js +200 -0
- package/build/server/chunks/shared-server-BDY8jh20.js.map +1 -0
- package/build/server/chunks/stores-D0HorpgL.js +36 -0
- package/build/server/chunks/stores-D0HorpgL.js.map +1 -0
- package/build/server/index.js +6466 -0
- package/build/server/index.js.map +1 -0
- package/build/server/manifest.js +184 -0
- package/build/server/manifest.js.map +1 -0
- package/build/shims.js +32 -0
- package/package.json +94 -0
- package/scripts/clipboard-shims/wl-paste +48 -0
- package/scripts/clipboard-shims/xclip +31 -0
- package/scripts/install.sh +477 -0
- package/scripts/setup-node-pty.sh +63 -0
- package/scripts/setup.cjs +571 -0
- package/scripts/test-runner.ts +243 -0
- package/scripts/vercel-env-commands.sh +60 -0
- package/server.ts +139 -0
- package/src/app.css +1835 -0
- package/src/app.d.ts +31 -0
- package/src/app.html +24 -0
- package/src/generated/types/APN.ts +305 -0
- package/src/generated/types/CLI.ts +52 -0
- package/src/generated/types/JWT.ts +92 -0
- package/src/generated/types/Terminal.ts +2736 -0
- package/src/generated/types/index.ts +6 -0
- package/src/lib/assets/icons/alert-triangle.svg +5 -0
- package/src/lib/assets/icons/bell.svg +4 -0
- package/src/lib/assets/icons/check-circle.svg +4 -0
- package/src/lib/assets/icons/file.svg +4 -0
- package/src/lib/assets/icons/folder.svg +3 -0
- package/src/lib/assets/icons/play.svg +3 -0
- package/src/lib/assets/icons/refresh.svg +4 -0
- package/src/lib/assets/icons/settings.svg +4 -0
- package/src/lib/assets/icons/terminal.svg +1 -0
- package/src/lib/assets/icons/tool.svg +3 -0
- package/src/lib/assets/icons/x-circle.svg +5 -0
- package/src/lib/modules/client/common/Card.svelte +26 -0
- package/src/lib/modules/client/common/EmptyState.svelte +36 -0
- package/src/lib/modules/client/common/Icon.svelte +61 -0
- package/src/lib/modules/client/common/StatusBadge.svelte +38 -0
- package/src/lib/modules/client/common/cache.ts +31 -0
- package/src/lib/modules/client/common/config-guard.ts +18 -0
- package/src/lib/modules/client/common/index.ts +12 -0
- package/src/lib/modules/client/common/markdown.ts +23 -0
- package/src/lib/modules/client/common/native-bridge.ts +50 -0
- package/src/lib/modules/client/common/time.ts +22 -0
- package/src/lib/modules/client/common/tool-title.ts +28 -0
- package/src/lib/modules/client/terminal/ChatView.svelte +400 -0
- package/src/lib/modules/client/terminal/CommandPalette.svelte +60 -0
- package/src/lib/modules/client/terminal/ConnectionStatus.svelte +99 -0
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +294 -0
- package/src/lib/modules/client/terminal/QuickKeys.svelte +71 -0
- package/src/lib/modules/client/terminal/ShortcutsHelp.svelte +79 -0
- package/src/lib/modules/client/terminal/keyboard-shortcuts.ts +70 -0
- package/src/lib/modules/client/terminal/xterm-wrapper.ts +243 -0
- package/src/lib/modules/server/apn/library-apns.ts +137 -0
- package/src/lib/modules/server/apn/notification-history.ts +35 -0
- package/src/lib/modules/server/apn/notification-sessions.ts +117 -0
- package/src/lib/modules/server/apn/pending-requests.ts +65 -0
- package/src/lib/modules/server/apn/types.ts +51 -0
- package/src/lib/modules/server/auth.ts +34 -0
- package/src/lib/modules/server/cli/index.ts +79 -0
- package/src/lib/modules/server/cli/runner.ts +162 -0
- package/src/lib/modules/server/fcm/fcm-service.ts +72 -0
- package/src/lib/modules/server/sessions/jsonl-parser.ts +197 -0
- package/src/lib/modules/server/sessions/jsonl-reader.ts +301 -0
- package/src/lib/modules/server/sessions/opencode-reader.ts +264 -0
- package/src/lib/modules/server/sessions/types.ts +53 -0
- package/src/lib/modules/server/terminal/holder-client.ts +273 -0
- package/src/lib/modules/server/terminal/opencode-watcher.ts +661 -0
- package/src/lib/modules/server/terminal/pty-holder.cjs +510 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +1012 -0
- package/src/lib/modules/server/terminal/session-watcher.ts +320 -0
- package/src/lib/modules/server/terminal/terminal-store.ts +198 -0
- package/src/lib/modules/server/ws/events-handler.ts +73 -0
- package/src/lib/modules/server/ws/keepalive.ts +108 -0
- package/src/lib/modules/server/ws/server.ts +93 -0
- package/src/lib/modules/server/ws/session-handler.ts +462 -0
- package/src/lib/modules/server/ws/terminal-handler.ts +197 -0
- package/src/lib/modules/server/ws/ticket-store.ts +58 -0
- package/src/lib/theme.css +529 -0
- package/src/lib/types/config.ts +6 -0
- package/src/routes/+layout.svelte +218 -0
- package/src/routes/+page.svelte +261 -0
- package/src/routes/api/debug/+server.ts +33 -0
- package/src/routes/api/device-token/+server.ts +85 -0
- package/src/routes/api/health/+server.ts +100 -0
- package/src/routes/api/notify/+server.ts +418 -0
- package/src/routes/api/qr-config/+server.ts +45 -0
- package/src/routes/api/response/+server.ts +73 -0
- package/src/routes/api/sessions/+server.ts +120 -0
- package/src/routes/api/terminals/+server.ts +141 -0
- package/src/routes/api/terminals/[id]/+server.ts +75 -0
- package/src/routes/api/terminals/[id]/paste-image/+server.ts +61 -0
- package/src/routes/api/terminals/[id]/resize/+server.ts +60 -0
- package/src/routes/api/webhook/+server.ts +42 -0
- package/src/routes/api/ws-status/+server.ts +23 -0
- package/src/routes/api/ws-ticket/+server.ts +86 -0
- package/src/routes/config/+page.svelte +600 -0
- package/src/routes/project/+page.svelte +274 -0
- package/src/routes/session/[id]/+page.svelte +434 -0
- package/src/routes/terminals/+page.svelte +618 -0
- package/src/routes/terminals/[id]/+page.svelte +968 -0
- package/svelte.config.js +18 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Session Watcher
|
|
3
|
+
*
|
|
4
|
+
* Polls the OpenCode SQLite database for new messages in a session and
|
|
5
|
+
* streams them as ConversationMessage objects that the session handler
|
|
6
|
+
* can consume directly for both history and live updates.
|
|
7
|
+
*
|
|
8
|
+
* Unlike the JSONL-based SessionWatcher (which uses chokidar for file
|
|
9
|
+
* change detection), this module polls SQLite because OpenCode holds a
|
|
10
|
+
* write lock on the database and we must open/close quickly in read-only
|
|
11
|
+
* mode to avoid contention.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import Database from 'better-sqlite3';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
|
|
18
|
+
import type { ConversationMessage, MessagePart } from '../sessions/types';
|
|
19
|
+
|
|
20
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
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
|
+
})();
|
|
29
|
+
|
|
30
|
+
/** Poll interval in milliseconds. */
|
|
31
|
+
const POLL_INTERVAL_MS = 2000;
|
|
32
|
+
|
|
33
|
+
/** Maximum parameters per SQLite IN clause (SQLite limit is 999). */
|
|
34
|
+
const SQLITE_MAX_PARAMS = 500;
|
|
35
|
+
|
|
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
|
+
// ── Per-session Watcher State ────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
interface OpenCodeSession {
|
|
81
|
+
directory: string;
|
|
82
|
+
id: string;
|
|
83
|
+
time_created: number;
|
|
84
|
+
time_updated: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface WatchState {
|
|
88
|
+
callbacks: Set<(messages: ConversationMessage[]) => void>;
|
|
89
|
+
/** Set of message IDs we have already emitted, to avoid duplicates. */
|
|
90
|
+
emittedMessageIds: Set<string>;
|
|
91
|
+
/** Set of part IDs we have already emitted, to avoid duplicates on update. */
|
|
92
|
+
emittedPartIds: Set<string>;
|
|
93
|
+
intervalHandle: ReturnType<typeof setInterval>;
|
|
94
|
+
/** Highest time_created we have seen for messages (milliseconds). */
|
|
95
|
+
lastMessageTime: number;
|
|
96
|
+
/** Highest time_updated we have seen for parts (milliseconds). */
|
|
97
|
+
lastPartTime: number;
|
|
98
|
+
sessionId: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── OpenCodeWatcher Class ────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
class OpenCodeWatcher {
|
|
104
|
+
private watchers = new Map<string, WatchState>();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find the most recent non-archived OpenCode session that matches
|
|
108
|
+
* the given working directory. Checks session.directory equals or
|
|
109
|
+
* starts with `cwd`.
|
|
110
|
+
*
|
|
111
|
+
* Returns the session ID or null if none found.
|
|
112
|
+
*/
|
|
113
|
+
findSessionId(cwd: string, createdAfter?: number): null | string {
|
|
114
|
+
const db = openDb();
|
|
115
|
+
if (!db) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Match sessions that were active (updated) after the terminal was launched.
|
|
121
|
+
// OpenCode resumes existing sessions rather than always creating new ones,
|
|
122
|
+
// so we filter on time_updated (not time_created) to find the session
|
|
123
|
+
// that's being actively used by this terminal instance.
|
|
124
|
+
//
|
|
125
|
+
// createdAfter is JS milliseconds (Date.getTime()), but OpenCode may
|
|
126
|
+
// store time_updated in seconds. Use the smaller of the two
|
|
127
|
+
// representations so the filter works regardless of DB unit.
|
|
128
|
+
const timeFilter = createdAfter
|
|
129
|
+
? Math.min(createdAfter, Math.floor(createdAfter / 1000))
|
|
130
|
+
: 0;
|
|
131
|
+
const row = db
|
|
132
|
+
.prepare(
|
|
133
|
+
`
|
|
134
|
+
SELECT id
|
|
135
|
+
FROM session
|
|
136
|
+
WHERE (time_archived IS NULL OR time_archived = 0)
|
|
137
|
+
AND (directory = ? OR directory LIKE ? || '/%')
|
|
138
|
+
AND time_updated > ?
|
|
139
|
+
ORDER BY time_updated DESC
|
|
140
|
+
LIMIT 1
|
|
141
|
+
`
|
|
142
|
+
)
|
|
143
|
+
.get(cwd, cwd, timeFilter) as OpenCodeSession | undefined;
|
|
144
|
+
|
|
145
|
+
return row?.id ?? null;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('[opencode-watcher] Failed to find session:', error);
|
|
148
|
+
return null;
|
|
149
|
+
} finally {
|
|
150
|
+
db.close();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Read all messages and parts for a session from SQLite, converting
|
|
156
|
+
* them to ConversationMessage format.
|
|
157
|
+
*/
|
|
158
|
+
getHistory(sessionId: string): ConversationMessage[] {
|
|
159
|
+
const db = openDb();
|
|
160
|
+
if (!db) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Fetch all messages for this session, ordered chronologically.
|
|
166
|
+
const messages = db
|
|
167
|
+
.prepare(
|
|
168
|
+
`
|
|
169
|
+
SELECT id, session_id, time_created, time_updated, data
|
|
170
|
+
FROM message
|
|
171
|
+
WHERE session_id = ?
|
|
172
|
+
ORDER BY time_created ASC
|
|
173
|
+
`
|
|
174
|
+
)
|
|
175
|
+
.all(sessionId) as OpenCodeMessage[];
|
|
176
|
+
|
|
177
|
+
if (messages.length === 0) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fetch all parts for these messages (batched to avoid SQLite param limit).
|
|
182
|
+
const messageIds = messages.map((m) => m.id);
|
|
183
|
+
const parts = batchInQuery<OpenCodePart>(
|
|
184
|
+
db,
|
|
185
|
+
`SELECT id, message_id, session_id, time_created, time_updated, data
|
|
186
|
+
FROM part
|
|
187
|
+
WHERE message_id IN (__PLACEHOLDERS__)
|
|
188
|
+
ORDER BY time_created ASC`,
|
|
189
|
+
messageIds
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Group parts by message ID.
|
|
193
|
+
const partsByMessage = new Map<string, OpenCodePart[]>();
|
|
194
|
+
for (const part of parts) {
|
|
195
|
+
if (!partsByMessage.has(part.message_id)) {
|
|
196
|
+
partsByMessage.set(part.message_id, []);
|
|
197
|
+
}
|
|
198
|
+
partsByMessage.get(part.message_id)!.push(part);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return this.buildMessages(messages, partsByMessage);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('[opencode-watcher] Failed to read history:', error);
|
|
204
|
+
return [];
|
|
205
|
+
} finally {
|
|
206
|
+
db.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Stop watching a specific session. If a callback is provided, only that
|
|
212
|
+
* subscriber is removed — the interval keeps running while other subscribers
|
|
213
|
+
* remain. If no callback is provided, all subscribers and the interval are
|
|
214
|
+
* cleared (backward compat).
|
|
215
|
+
*/
|
|
216
|
+
stop(sessionId: string, callback?: (messages: ConversationMessage[]) => void): void {
|
|
217
|
+
const state = this.watchers.get(sessionId);
|
|
218
|
+
if (!state) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (callback) {
|
|
223
|
+
state.callbacks.delete(callback);
|
|
224
|
+
console.log(
|
|
225
|
+
`[opencode-watcher] Removed subscriber from session: ${sessionId} ` +
|
|
226
|
+
`(remaining=${state.callbacks.size})`
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Only tear down the interval when no subscribers remain.
|
|
230
|
+
if (state.callbacks.size > 0) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
clearInterval(state.intervalHandle);
|
|
236
|
+
this.watchers.delete(sessionId);
|
|
237
|
+
console.log(`[opencode-watcher] Stopped watching session: ${sessionId}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Stop all active watchers.
|
|
242
|
+
*/
|
|
243
|
+
stopAll(): void {
|
|
244
|
+
for (const [sessionId] of this.watchers) {
|
|
245
|
+
this.stop(sessionId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Start polling the SQLite DB every 2 seconds for new messages/parts
|
|
251
|
+
* in the given session. Converts new data to ConversationMessage
|
|
252
|
+
* format and invokes the callback.
|
|
253
|
+
*/
|
|
254
|
+
watch(sessionId: string, callback: (messages: ConversationMessage[]) => void): void {
|
|
255
|
+
const existing = this.watchers.get(sessionId);
|
|
256
|
+
if (existing) {
|
|
257
|
+
// Already watching — just add the new callback, don't create a new interval.
|
|
258
|
+
existing.callbacks.add(callback);
|
|
259
|
+
console.log(
|
|
260
|
+
`[opencode-watcher] Added subscriber to session: ${sessionId} ` +
|
|
261
|
+
`(total=${existing.callbacks.size})`
|
|
262
|
+
);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Determine the initial high-water marks by scanning existing data.
|
|
267
|
+
const { emittedMessageIds, emittedPartIds, lastMessageTime, lastPartTime } =
|
|
268
|
+
this.getHighWaterMarks(sessionId);
|
|
269
|
+
|
|
270
|
+
const intervalHandle = setInterval(() => {
|
|
271
|
+
this.poll(sessionId);
|
|
272
|
+
}, POLL_INTERVAL_MS);
|
|
273
|
+
|
|
274
|
+
const state: WatchState = {
|
|
275
|
+
callbacks: new Set([callback]),
|
|
276
|
+
emittedMessageIds,
|
|
277
|
+
emittedPartIds,
|
|
278
|
+
intervalHandle,
|
|
279
|
+
lastMessageTime,
|
|
280
|
+
lastPartTime,
|
|
281
|
+
sessionId
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
this.watchers.set(sessionId, state);
|
|
285
|
+
console.log(
|
|
286
|
+
`[opencode-watcher] Watching session: ${sessionId} ` +
|
|
287
|
+
`(lastMsg=${lastMessageTime}, lastPart=${lastPartTime})`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Private Helpers ────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Convert OpenCode messages + their parts into ConversationMessage
|
|
295
|
+
* objects for consumption by the session handler.
|
|
296
|
+
*/
|
|
297
|
+
private buildMessages(
|
|
298
|
+
messages: OpenCodeMessage[],
|
|
299
|
+
partsByMessage: Map<string, OpenCodePart[]>
|
|
300
|
+
): ConversationMessage[] {
|
|
301
|
+
const result: ConversationMessage[] = [];
|
|
302
|
+
|
|
303
|
+
for (const msg of messages) {
|
|
304
|
+
// Parse message data to determine role.
|
|
305
|
+
let msgData: { agent?: string; role?: string } = {};
|
|
306
|
+
try {
|
|
307
|
+
msgData = JSON.parse(msg.data) as typeof msgData;
|
|
308
|
+
} catch {
|
|
309
|
+
// Skip unparseable message data.
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const role = msgData.role === 'user' ? 'user' : 'assistant';
|
|
314
|
+
const msgParts = partsByMessage.get(msg.id) || [];
|
|
315
|
+
|
|
316
|
+
// Convert each part to a MessagePart.
|
|
317
|
+
const parts: MessagePart[] = [];
|
|
318
|
+
|
|
319
|
+
for (const part of msgParts) {
|
|
320
|
+
let partData: OpenCodePartData;
|
|
321
|
+
try {
|
|
322
|
+
partData = JSON.parse(part.data) as OpenCodePartData;
|
|
323
|
+
} catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const converted = convertPartToMessagePart(partData);
|
|
328
|
+
if (converted) {
|
|
329
|
+
parts.push(converted);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Skip messages with no usable content.
|
|
334
|
+
if (parts.length === 0) {
|
|
335
|
+
console.debug(`[opencode-watcher] Skipping message ${msg.id} (no usable parts)`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
result.push({
|
|
340
|
+
id: msg.id,
|
|
341
|
+
parts,
|
|
342
|
+
role,
|
|
343
|
+
timestamp: new Date(toMillis(msg.time_created)).toISOString(),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Scan existing messages/parts to determine the starting high-water
|
|
352
|
+
* marks for time-based polling. Also collects the set of already-seen
|
|
353
|
+
* message IDs so we do not re-emit them.
|
|
354
|
+
*/
|
|
355
|
+
private getHighWaterMarks(sessionId: string): {
|
|
356
|
+
emittedMessageIds: Set<string>;
|
|
357
|
+
emittedPartIds: Set<string>;
|
|
358
|
+
lastMessageTime: number;
|
|
359
|
+
lastPartTime: number;
|
|
360
|
+
} {
|
|
361
|
+
const db = openDb();
|
|
362
|
+
if (!db) {
|
|
363
|
+
return { emittedMessageIds: new Set(), emittedPartIds: new Set(), lastMessageTime: 0, lastPartTime: 0 };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const msgRow = db
|
|
368
|
+
.prepare(
|
|
369
|
+
`
|
|
370
|
+
SELECT MAX(time_created) as maxTime
|
|
371
|
+
FROM message
|
|
372
|
+
WHERE session_id = ?
|
|
373
|
+
`
|
|
374
|
+
)
|
|
375
|
+
.get(sessionId) as undefined | { maxTime: null | number };
|
|
376
|
+
|
|
377
|
+
const partRow = db
|
|
378
|
+
.prepare(
|
|
379
|
+
`
|
|
380
|
+
SELECT MAX(time_updated) as maxTime
|
|
381
|
+
FROM part
|
|
382
|
+
WHERE session_id = ?
|
|
383
|
+
`
|
|
384
|
+
)
|
|
385
|
+
.get(sessionId) as undefined | { maxTime: null | number };
|
|
386
|
+
|
|
387
|
+
// Collect all existing message IDs.
|
|
388
|
+
const existingIds = db
|
|
389
|
+
.prepare(
|
|
390
|
+
`
|
|
391
|
+
SELECT id FROM message WHERE session_id = ?
|
|
392
|
+
`
|
|
393
|
+
)
|
|
394
|
+
.all(sessionId) as { id: string }[];
|
|
395
|
+
|
|
396
|
+
const emittedMessageIds = new Set(existingIds.map((r) => r.id));
|
|
397
|
+
|
|
398
|
+
// Collect all existing part IDs.
|
|
399
|
+
const existingPartIds = db
|
|
400
|
+
.prepare(
|
|
401
|
+
`
|
|
402
|
+
SELECT id FROM part WHERE session_id = ?
|
|
403
|
+
`
|
|
404
|
+
)
|
|
405
|
+
.all(sessionId) as { id: string }[];
|
|
406
|
+
|
|
407
|
+
const emittedPartIds = new Set(existingPartIds.map((r) => r.id));
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
emittedMessageIds,
|
|
411
|
+
emittedPartIds,
|
|
412
|
+
lastMessageTime: msgRow?.maxTime ?? 0,
|
|
413
|
+
lastPartTime: partRow?.maxTime ?? 0
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error('[opencode-watcher] Failed to get high-water marks:', error);
|
|
417
|
+
return { emittedMessageIds: new Set(), emittedPartIds: new Set(), lastMessageTime: 0, lastPartTime: 0 };
|
|
418
|
+
} finally {
|
|
419
|
+
db.close();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Single poll iteration. Opens the DB, queries for new messages and
|
|
425
|
+
* updated parts, converts them to ConversationMessage[], and invokes
|
|
426
|
+
* the watcher callbacks.
|
|
427
|
+
*/
|
|
428
|
+
private poll(sessionId: string): void {
|
|
429
|
+
const state = this.watchers.get(sessionId);
|
|
430
|
+
if (!state) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const db = openDb();
|
|
435
|
+
if (!db) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const results: ConversationMessage[] = [];
|
|
441
|
+
|
|
442
|
+
// ── 1. Check for brand-new messages ──────────────────────────
|
|
443
|
+
const newMessages = db
|
|
444
|
+
.prepare(
|
|
445
|
+
`
|
|
446
|
+
SELECT id, session_id, time_created, time_updated, data
|
|
447
|
+
FROM message
|
|
448
|
+
WHERE session_id = ? AND time_created > ?
|
|
449
|
+
ORDER BY time_created ASC
|
|
450
|
+
`
|
|
451
|
+
)
|
|
452
|
+
.all(sessionId, state.lastMessageTime) as OpenCodeMessage[];
|
|
453
|
+
|
|
454
|
+
if (newMessages.length > 0) {
|
|
455
|
+
// Deduplicate: skip messages we have already emitted (guards
|
|
456
|
+
// against clock-tie edge cases where time_created equals the
|
|
457
|
+
// high-water mark and the row slips through the > filter again).
|
|
458
|
+
const dedupedMessages = newMessages.filter(
|
|
459
|
+
(m) => !state.emittedMessageIds.has(m.id)
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (dedupedMessages.length > 0) {
|
|
463
|
+
// Fetch parts for these new messages (batched to avoid SQLite param limit).
|
|
464
|
+
const newMsgIds = dedupedMessages.map((m) => m.id);
|
|
465
|
+
const newParts = batchInQuery<OpenCodePart>(
|
|
466
|
+
db,
|
|
467
|
+
`SELECT id, message_id, session_id, time_created, time_updated, data
|
|
468
|
+
FROM part
|
|
469
|
+
WHERE message_id IN (__PLACEHOLDERS__)
|
|
470
|
+
ORDER BY time_created ASC`,
|
|
471
|
+
newMsgIds
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// Group parts by message.
|
|
475
|
+
const partsByMessage = new Map<string, OpenCodePart[]>();
|
|
476
|
+
for (const part of newParts) {
|
|
477
|
+
if (!partsByMessage.has(part.message_id)) {
|
|
478
|
+
partsByMessage.set(part.message_id, []);
|
|
479
|
+
}
|
|
480
|
+
partsByMessage.get(part.message_id)!.push(part);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const newEntries = this.buildMessages(dedupedMessages, partsByMessage);
|
|
484
|
+
results.push(...newEntries);
|
|
485
|
+
|
|
486
|
+
// Track emitted part IDs and update part high-water mark.
|
|
487
|
+
for (const part of newParts) {
|
|
488
|
+
if (part.time_updated > state.lastPartTime) {
|
|
489
|
+
state.lastPartTime = part.time_updated;
|
|
490
|
+
}
|
|
491
|
+
state.emittedPartIds.add(part.id);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Always update message high-water marks (even for already-emitted messages).
|
|
496
|
+
for (const msg of newMessages) {
|
|
497
|
+
if (msg.time_created > state.lastMessageTime) {
|
|
498
|
+
state.lastMessageTime = msg.time_created;
|
|
499
|
+
}
|
|
500
|
+
state.emittedMessageIds.add(msg.id);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── 2. Check for updated parts on existing messages ──────────
|
|
505
|
+
// Parts may be added or updated after the initial message row is
|
|
506
|
+
// created (e.g., streaming assistant response). Look for parts
|
|
507
|
+
// whose time_updated exceeds our last-seen mark, but whose parent
|
|
508
|
+
// message is NOT in the new-messages set (those were handled above).
|
|
509
|
+
const updatedParts = db
|
|
510
|
+
.prepare(
|
|
511
|
+
`
|
|
512
|
+
SELECT id, message_id, session_id, time_created, time_updated, data
|
|
513
|
+
FROM part
|
|
514
|
+
WHERE session_id = ? AND time_updated > ?
|
|
515
|
+
ORDER BY time_created ASC
|
|
516
|
+
`
|
|
517
|
+
)
|
|
518
|
+
.all(sessionId, state.lastPartTime) as OpenCodePart[];
|
|
519
|
+
|
|
520
|
+
if (updatedParts.length > 0) {
|
|
521
|
+
// Filter out parts that belong to messages we just processed,
|
|
522
|
+
// AND parts we have already emitted (avoid duplicates).
|
|
523
|
+
const newMsgIdSet = new Set(newMessages.map((m) => m.id));
|
|
524
|
+
const newParts = updatedParts.filter(
|
|
525
|
+
(p) => !newMsgIdSet.has(p.message_id) && !state.emittedPartIds.has(p.id)
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
if (newParts.length > 0) {
|
|
529
|
+
// Group the NEW parts by message ID.
|
|
530
|
+
const partsByMessage = new Map<string, OpenCodePart[]>();
|
|
531
|
+
for (const part of newParts) {
|
|
532
|
+
if (!partsByMessage.has(part.message_id)) {
|
|
533
|
+
partsByMessage.set(part.message_id, []);
|
|
534
|
+
}
|
|
535
|
+
partsByMessage.get(part.message_id)!.push(part);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Fetch the parent messages for context (batched to avoid SQLite param limit).
|
|
539
|
+
const affectedMsgIds = [...partsByMessage.keys()];
|
|
540
|
+
const affectedMessages = batchInQuery<OpenCodeMessage>(
|
|
541
|
+
db,
|
|
542
|
+
`SELECT id, session_id, time_created, time_updated, data
|
|
543
|
+
FROM message
|
|
544
|
+
WHERE id IN (__PLACEHOLDERS__)
|
|
545
|
+
ORDER BY time_created ASC`,
|
|
546
|
+
affectedMsgIds
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Build entries from ONLY the new parts (delta), not all
|
|
550
|
+
// parts for the message. This prevents re-emitting content
|
|
551
|
+
// the session handler has already seen.
|
|
552
|
+
const updatedEntries = this.buildMessages(affectedMessages, partsByMessage);
|
|
553
|
+
results.push(...updatedEntries);
|
|
554
|
+
|
|
555
|
+
// Track emitted part IDs.
|
|
556
|
+
for (const part of newParts) {
|
|
557
|
+
state.emittedPartIds.add(part.id);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Update part high-water mark.
|
|
562
|
+
for (const part of updatedParts) {
|
|
563
|
+
if (part.time_updated > state.lastPartTime) {
|
|
564
|
+
state.lastPartTime = part.time_updated;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── 3. Invoke all callbacks if there are new entries ─────────
|
|
570
|
+
if (results.length > 0) {
|
|
571
|
+
for (const cb of state.callbacks) {
|
|
572
|
+
try {
|
|
573
|
+
cb(results);
|
|
574
|
+
} catch (cbError) {
|
|
575
|
+
console.error('[opencode-watcher] Callback error:', cbError);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
// Log but do not crash — the next poll will retry.
|
|
581
|
+
console.error('[opencode-watcher] Poll error:', error);
|
|
582
|
+
} finally {
|
|
583
|
+
db.close();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ── Database Helpers ─────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Execute a SELECT query with an IN clause, batching in chunks of
|
|
592
|
+
* SQLITE_MAX_PARAMS to stay within SQLite's 999-parameter limit.
|
|
593
|
+
*/
|
|
594
|
+
function batchInQuery<T>(
|
|
595
|
+
db: Database.Database,
|
|
596
|
+
sql: string,
|
|
597
|
+
ids: string[]
|
|
598
|
+
): T[] {
|
|
599
|
+
if (ids.length === 0) return [];
|
|
600
|
+
|
|
601
|
+
const results: T[] = [];
|
|
602
|
+
for (let i = 0; i < ids.length; i += SQLITE_MAX_PARAMS) {
|
|
603
|
+
const chunk = ids.slice(i, i + SQLITE_MAX_PARAMS);
|
|
604
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
605
|
+
const query = sql.replace('__PLACEHOLDERS__', placeholders);
|
|
606
|
+
const rows = db.prepare(query).all(...chunk) as T[];
|
|
607
|
+
results.push(...rows);
|
|
608
|
+
}
|
|
609
|
+
return results;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── Part Conversion ──────────────────────────────────────────────────
|
|
613
|
+
// Maps OpenCode part types to MessagePart directly, skipping the
|
|
614
|
+
// intermediate Record<string, unknown> stage.
|
|
615
|
+
|
|
616
|
+
function convertPartToMessagePart(data: OpenCodePartData): MessagePart | null {
|
|
617
|
+
switch (data.type) {
|
|
618
|
+
case 'reasoning':
|
|
619
|
+
return { content: data.text || '', type: 'thinking' };
|
|
620
|
+
|
|
621
|
+
case 'text':
|
|
622
|
+
return { content: data.text || '', type: 'text' };
|
|
623
|
+
|
|
624
|
+
case 'tool':
|
|
625
|
+
return {
|
|
626
|
+
id: data.callID || data.id || '',
|
|
627
|
+
input: data.state?.input || {},
|
|
628
|
+
toolName: data.tool || 'Unknown',
|
|
629
|
+
type: 'tool_use'
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
default:
|
|
633
|
+
// Skip snapshot, patch, step-start, step-finish, subtask, retry, compaction
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Open the OpenCode SQLite database in read-only mode.
|
|
640
|
+
* Returns null if the file does not exist or cannot be opened.
|
|
641
|
+
*/
|
|
642
|
+
function openDb(): Database.Database | null {
|
|
643
|
+
if (!fs.existsSync(OPENCODE_DB_PATH)) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
return new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ── Singleton ────────────────────────────────────────────────────────
|
|
654
|
+
// Use globalThis to ensure a single shared instance across module
|
|
655
|
+
// loaders (same pattern as pty-manager and session-watcher).
|
|
656
|
+
|
|
657
|
+
const OW_GLOBAL_KEY = '__shooter_opencode_watcher';
|
|
658
|
+
export const openCodeWatcher: OpenCodeWatcher =
|
|
659
|
+
((globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] as OpenCodeWatcher) ||
|
|
660
|
+
new OpenCodeWatcher();
|
|
661
|
+
(globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] = openCodeWatcher;
|