@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,1431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified Shooter Notifier v3.0
|
|
5
|
+
* Works with both Claude Code and OpenCode
|
|
6
|
+
*
|
|
7
|
+
* Claude Code: Called via CLI with event type argument + JSON on stdin
|
|
8
|
+
* node notifier.cjs PreToolUse (stdin: { tool_name, tool_input, ... })
|
|
9
|
+
* node notifier.cjs Stop (stdin: { session_id, ... })
|
|
10
|
+
* node notifier.cjs Notification (stdin: { notification_type, message, title, ... })
|
|
11
|
+
* node notifier.cjs PermissionRequest (stdin: { tool_name, tool_input, ... })
|
|
12
|
+
*
|
|
13
|
+
* OpenCode: Import as plugin module
|
|
14
|
+
* Place in ~/.config/opencode/plugins/ or .opencode/plugins/
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const https = require('https');
|
|
18
|
+
const http = require('http');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// SECTION 1: Configuration & Runtime Detection
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
// Detect runtime environment
|
|
27
|
+
const IS_OPENCODE =
|
|
28
|
+
typeof process.env.OPENCODE_VERSION !== 'undefined' ||
|
|
29
|
+
require.main !== module ||
|
|
30
|
+
process.argv[1]?.includes('opencode');
|
|
31
|
+
const IS_CLAUDE_CODE = !IS_OPENCODE && require.main === module;
|
|
32
|
+
const RUNTIME = IS_OPENCODE ? 'opencode' : 'claude-code';
|
|
33
|
+
|
|
34
|
+
// Environment configuration
|
|
35
|
+
const USE_LOCAL = process.env.SHOOTER_USE_LOCAL === 'true';
|
|
36
|
+
const LOCAL_PORT = process.env.SHOOTER_LOCAL_PORT || '3000';
|
|
37
|
+
const REMOTE_BASE_URL = process.env.SHOOTER_API_URL || '';
|
|
38
|
+
const LOCAL_BASE_URL = `http://localhost:${LOCAL_PORT}`;
|
|
39
|
+
const BASE_URL = USE_LOCAL ? LOCAL_BASE_URL : REMOTE_BASE_URL;
|
|
40
|
+
const API_URL = `${BASE_URL}/api/notify`;
|
|
41
|
+
|
|
42
|
+
// Authentication
|
|
43
|
+
const API_KEY = process.env.API_KEY || process.env.SHOOTER_API_KEY;
|
|
44
|
+
const DEVICE_TOKEN = process.env.SHOOTER_DEVICE_TOKEN || null;
|
|
45
|
+
const AUTH_KEY = API_KEY || '';
|
|
46
|
+
|
|
47
|
+
// Validate required environment variables ONLY for Claude Code CLI mode
|
|
48
|
+
if (IS_CLAUDE_CODE && !API_KEY) {
|
|
49
|
+
console.error('API_KEY environment variable is required');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Completion detection timeout
|
|
54
|
+
const COMPLETION_TIMEOUT = 45000; // 45 seconds
|
|
55
|
+
|
|
56
|
+
// Bidirectional permission response polling
|
|
57
|
+
const PERMISSION_TIMEOUT = parseInt(process.env.SHOOTER_PERMISSION_TIMEOUT || '120') * 1000;
|
|
58
|
+
const POLL_INTERVAL = 2000; // 2 seconds between polls
|
|
59
|
+
const RESPONSE_URL = `${BASE_URL}/api/response`;
|
|
60
|
+
const STATE_DIR = `/tmp/claude_session_tracker`;
|
|
61
|
+
|
|
62
|
+
// Global timeout tracker per project (for OpenCode)
|
|
63
|
+
const completionTimers = new Map();
|
|
64
|
+
|
|
65
|
+
// Debug logging flag
|
|
66
|
+
const DEBUG_ENABLED = process.env.SHOOTER_DEBUG === 'true';
|
|
67
|
+
const DEBUG_LOG_FILE = '/tmp/shooter-debug.log';
|
|
68
|
+
|
|
69
|
+
// ============================================
|
|
70
|
+
// SECTION 1.5: WebSocket Client Detection
|
|
71
|
+
// ============================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if any WebSocket clients are connected to the events channel.
|
|
75
|
+
* When clients are connected, the WebSocket events broadcast handles
|
|
76
|
+
* permission-requested notifications, so we can skip push notifications.
|
|
77
|
+
*/
|
|
78
|
+
async function hasWebSocketClients() {
|
|
79
|
+
try {
|
|
80
|
+
const url = `${BASE_URL}/api/ws-status`;
|
|
81
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const req = protocol.request(url, {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
headers: { Authorization: `Bearer ${AUTH_KEY}` },
|
|
86
|
+
timeout: 3000,
|
|
87
|
+
}, (res) => {
|
|
88
|
+
let data = '';
|
|
89
|
+
res.on('data', (chunk) => (data += chunk));
|
|
90
|
+
res.on('end', () => {
|
|
91
|
+
if (res.statusCode === 200) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(data);
|
|
94
|
+
resolve(parsed.connectedClients > 0);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
resolve(false);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
resolve(false);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
req.on('error', () => resolve(false));
|
|
104
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
105
|
+
req.end();
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// If we can't reach the server, fall back to push
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================
|
|
114
|
+
// SECTION 2: Stdin Reader (Claude Code)
|
|
115
|
+
// ============================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read JSON data from stdin (Claude Code passes event data this way)
|
|
119
|
+
* Returns parsed JSON or null if stdin is empty/not JSON
|
|
120
|
+
*/
|
|
121
|
+
function readStdin() {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
// If stdin is a TTY (interactive terminal), no data to read
|
|
124
|
+
if (process.stdin.isTTY) {
|
|
125
|
+
resolve(null);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let data = '';
|
|
130
|
+
const timeout = setTimeout(() => {
|
|
131
|
+
// Timeout after 1 second - stdin may not have data
|
|
132
|
+
process.stdin.removeAllListeners('data');
|
|
133
|
+
process.stdin.removeAllListeners('end');
|
|
134
|
+
resolve(null);
|
|
135
|
+
}, 1000);
|
|
136
|
+
|
|
137
|
+
process.stdin.setEncoding('utf8');
|
|
138
|
+
process.stdin.on('data', (chunk) => {
|
|
139
|
+
data += chunk;
|
|
140
|
+
});
|
|
141
|
+
process.stdin.on('end', () => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
if (data.trim()) {
|
|
144
|
+
try {
|
|
145
|
+
resolve(JSON.parse(data.trim()));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
debugLog(`Failed to parse stdin JSON: ${e.message}`);
|
|
148
|
+
resolve(null);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
resolve(null);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
process.stdin.resume();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================
|
|
159
|
+
// SECTION 3: Common Event Format
|
|
160
|
+
// ============================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Common Event Format - all events normalized to this structure
|
|
164
|
+
*
|
|
165
|
+
* eventType values:
|
|
166
|
+
* 'tool.before' - Tool is about to execute (activity tracking)
|
|
167
|
+
* 'tool.after' - Tool finished executing (activity tracking)
|
|
168
|
+
* 'session.idle' - Agent finished responding (completion timer)
|
|
169
|
+
* 'session.start' - New session started
|
|
170
|
+
* 'permission' - Agent needs permission to run a tool
|
|
171
|
+
* 'question' - Agent is asking user a question / presenting options
|
|
172
|
+
* 'idle_input' - Agent is idle, waiting for user to type
|
|
173
|
+
* 'intervention' - Generic intervention needed (fallback)
|
|
174
|
+
* 'error' - An error occurred
|
|
175
|
+
* 'check.completion' - Manual completion check
|
|
176
|
+
* 'session.status' - Internal status update (ignored)
|
|
177
|
+
*/
|
|
178
|
+
function createCommonEvent(source, eventType, data = {}) {
|
|
179
|
+
return {
|
|
180
|
+
source, // 'claude-code' | 'opencode'
|
|
181
|
+
eventType,
|
|
182
|
+
timestamp: new Date().toISOString(),
|
|
183
|
+
projectName: getProjectName(),
|
|
184
|
+
data,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================
|
|
189
|
+
// SECTION 4: Event Adapters
|
|
190
|
+
// ============================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Adapter: Claude Code CLI + stdin JSON -> Common Event Format
|
|
194
|
+
*
|
|
195
|
+
* Claude Code passes rich JSON on stdin with fields like:
|
|
196
|
+
* - tool_name, tool_input (for PreToolUse, PermissionRequest)
|
|
197
|
+
* - notification_type, message, title (for Notification)
|
|
198
|
+
* - session_id, cwd, hook_event_name (common to all)
|
|
199
|
+
*/
|
|
200
|
+
function adaptClaudeCodeEvent(cliArg, stdinData) {
|
|
201
|
+
const data = {};
|
|
202
|
+
|
|
203
|
+
// --- PermissionRequest: Agent needs user permission to run a tool ---
|
|
204
|
+
if (cliArg === 'PermissionRequest') {
|
|
205
|
+
data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
|
|
206
|
+
data.toolInput = stdinData?.tool_input || {};
|
|
207
|
+
// Extract meaningful details from tool input
|
|
208
|
+
data.command = data.toolInput.command || '';
|
|
209
|
+
data.filePath = data.toolInput.file_path || '';
|
|
210
|
+
data.description = data.toolInput.description || '';
|
|
211
|
+
data.sessionId = stdinData?.session_id || '';
|
|
212
|
+
return createCommonEvent('claude-code', 'permission', data);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Notification: Different subtypes based on notification_type ---
|
|
216
|
+
if (cliArg === 'Notification') {
|
|
217
|
+
const notificationType = stdinData?.notification_type || '';
|
|
218
|
+
data.message = stdinData?.message || '';
|
|
219
|
+
data.title = stdinData?.title || '';
|
|
220
|
+
data.notificationType = notificationType;
|
|
221
|
+
|
|
222
|
+
switch (notificationType) {
|
|
223
|
+
case 'permission_prompt':
|
|
224
|
+
// Permission prompt notification - agent waiting for permission approval.
|
|
225
|
+
// Use 'permission_notification' (not 'permission') to avoid triggering
|
|
226
|
+
// the blocking bidirectional poll flow in handlePermission().
|
|
227
|
+
data.tool = ''; // Not available in notification event, just message
|
|
228
|
+
return createCommonEvent('claude-code', 'permission_notification', data);
|
|
229
|
+
|
|
230
|
+
case 'elicitation_dialog':
|
|
231
|
+
// Agent is presenting a question/dialog to the user
|
|
232
|
+
return createCommonEvent('claude-code', 'question', data);
|
|
233
|
+
|
|
234
|
+
case 'idle_prompt':
|
|
235
|
+
// Agent is idle, waiting for user to type something
|
|
236
|
+
return createCommonEvent('claude-code', 'idle_input', data);
|
|
237
|
+
|
|
238
|
+
case 'auth_success':
|
|
239
|
+
// Auth completed - not actionable, ignore
|
|
240
|
+
debugLog('auth_success notification - ignoring');
|
|
241
|
+
return createCommonEvent('claude-code', 'session.status', data);
|
|
242
|
+
|
|
243
|
+
default:
|
|
244
|
+
// Unknown notification type - send with whatever info we have
|
|
245
|
+
return createCommonEvent('claude-code', 'intervention', data);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- PreToolUse: Tool is about to execute (activity tracking only) ---
|
|
250
|
+
if (cliArg === 'PreToolUse') {
|
|
251
|
+
data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
|
|
252
|
+
data.files = process.env.CLAUDE_FILE_PATHS || '';
|
|
253
|
+
data.command = stdinData?.tool_input?.command || process.env.CLAUDE_COMMAND_LINE || '';
|
|
254
|
+
return createCommonEvent('claude-code', 'tool.before', data);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- PostToolUse: Tool finished executing (activity tracking) ---
|
|
258
|
+
if (cliArg === 'PostToolUse') {
|
|
259
|
+
data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
|
|
260
|
+
data.files = process.env.CLAUDE_FILE_PATHS || '';
|
|
261
|
+
data.command = stdinData?.tool_input?.command || process.env.CLAUDE_COMMAND_LINE || '';
|
|
262
|
+
return createCommonEvent('claude-code', 'tool.after', data);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- PostToolUseFailure: Tool execution failed ---
|
|
266
|
+
if (cliArg === 'PostToolUseFailure') {
|
|
267
|
+
data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
|
|
268
|
+
data.message = stdinData?.error || 'Tool execution failed';
|
|
269
|
+
data.files = process.env.CLAUDE_FILE_PATHS || '';
|
|
270
|
+
data.command = stdinData?.tool_input?.command || process.env.CLAUDE_COMMAND_LINE || '';
|
|
271
|
+
return createCommonEvent('claude-code', 'error', data);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- Stop: Agent finished responding ---
|
|
275
|
+
if (cliArg === 'Stop') {
|
|
276
|
+
return createCommonEvent('claude-code', 'session.idle', data);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- SessionStart: New session started ---
|
|
280
|
+
if (cliArg === 'SessionStart') {
|
|
281
|
+
return createCommonEvent('claude-code', 'session.start', data);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- SessionEnd: Session terminated ---
|
|
285
|
+
if (cliArg === 'SessionEnd') {
|
|
286
|
+
return createCommonEvent('claude-code', 'session.end', data);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// --- SubagentStart: Subagent spawned ---
|
|
290
|
+
if (cliArg === 'SubagentStart') {
|
|
291
|
+
data.agentType = stdinData?.agent_type || 'unknown';
|
|
292
|
+
return createCommonEvent('claude-code', 'subagent.start', data);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- SubagentStop: Subagent finished ---
|
|
296
|
+
if (cliArg === 'SubagentStop') {
|
|
297
|
+
data.agentType = stdinData?.agent_type || 'unknown';
|
|
298
|
+
return createCommonEvent('claude-code', 'subagent.stop', data);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- UserPromptSubmit: User submitted a prompt ---
|
|
302
|
+
if (cliArg === 'UserPromptSubmit') {
|
|
303
|
+
data.message = stdinData?.message || '';
|
|
304
|
+
return createCommonEvent('claude-code', 'user.prompt', data);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --- TeammateIdle: Agent teammate went idle ---
|
|
308
|
+
if (cliArg === 'TeammateIdle') {
|
|
309
|
+
data.teammate = stdinData?.agent_name || stdinData?.name || 'unknown';
|
|
310
|
+
return createCommonEvent('claude-code', 'teammate.idle', data);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- TaskCompleted: A task was marked complete ---
|
|
314
|
+
if (cliArg === 'TaskCompleted') {
|
|
315
|
+
data.taskId = stdinData?.task_id || '';
|
|
316
|
+
data.message = stdinData?.subject || '';
|
|
317
|
+
return createCommonEvent('claude-code', 'task.completed', data);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- PreCompact: Context about to be compacted ---
|
|
321
|
+
if (cliArg === 'PreCompact') {
|
|
322
|
+
return createCommonEvent('claude-code', 'context.compact', data);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- CheckCompletion: Manual check ---
|
|
326
|
+
if (cliArg === 'CheckCompletion') {
|
|
327
|
+
return createCommonEvent('claude-code', 'check.completion', data);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- Unknown event type ---
|
|
331
|
+
data.rawArg = cliArg;
|
|
332
|
+
return createCommonEvent('claude-code', 'unknown', data);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Adapter: OpenCode Hook Events -> Common Event Format
|
|
337
|
+
*/
|
|
338
|
+
function adaptOpenCodeEvent(hookEventType, hookData = {}) {
|
|
339
|
+
const eventTypeMap = {
|
|
340
|
+
'tool.execute.before': 'tool.before',
|
|
341
|
+
'tool.execute.after': 'tool.after',
|
|
342
|
+
'session.idle': 'session.idle',
|
|
343
|
+
'session.created': 'session.start',
|
|
344
|
+
'session.error': 'error',
|
|
345
|
+
'session.status': 'session.status',
|
|
346
|
+
'session.updated': 'session.status',
|
|
347
|
+
'session.diff': 'session.status',
|
|
348
|
+
'message.updated': 'session.status',
|
|
349
|
+
'message.part.updated': 'session.status',
|
|
350
|
+
'message.removed': 'session.status',
|
|
351
|
+
'message.part.removed': 'session.status',
|
|
352
|
+
'lsp.client.diagnostics': 'session.status',
|
|
353
|
+
'lsp.updated': 'session.status',
|
|
354
|
+
'permission.asked': 'permission',
|
|
355
|
+
'permission.replied': 'session.status',
|
|
356
|
+
'question.asked': 'question',
|
|
357
|
+
'question.replied': 'session.status',
|
|
358
|
+
'question.rejected': 'session.status',
|
|
359
|
+
'server.instance.disposed': 'session.status',
|
|
360
|
+
'server.connected': 'session.status',
|
|
361
|
+
'todo.updated': 'session.status',
|
|
362
|
+
'file.edited': 'session.status',
|
|
363
|
+
'file.watcher.updated': 'session.status',
|
|
364
|
+
'installation.updated': 'session.status',
|
|
365
|
+
'command.executed': 'session.status',
|
|
366
|
+
'shell.env': 'session.status',
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const eventType = eventTypeMap[hookEventType] || 'unknown';
|
|
370
|
+
const data = {
|
|
371
|
+
tool: hookData.tool || 'unknown',
|
|
372
|
+
toolInput: hookData.toolInput || {},
|
|
373
|
+
command: hookData.command || '',
|
|
374
|
+
filePath: hookData.filePath || '',
|
|
375
|
+
files: hookData.files || [],
|
|
376
|
+
message: hookData.message || hookData.error || '',
|
|
377
|
+
questions: hookData.questions || [],
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
return createCommonEvent('opencode', eventType, data);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ============================================
|
|
384
|
+
// SECTION 5: Session State Management
|
|
385
|
+
// ============================================
|
|
386
|
+
|
|
387
|
+
function ensureStateDir() {
|
|
388
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
389
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getProjectName() {
|
|
394
|
+
return path.basename(process.cwd()) || 'unknown';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getSessionIdentifier() {
|
|
398
|
+
const projectName = getProjectName();
|
|
399
|
+
const runtime = RUNTIME;
|
|
400
|
+
const pid = process.pid;
|
|
401
|
+
return `${projectName}_${runtime}_${pid}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getSessionState() {
|
|
405
|
+
ensureStateDir();
|
|
406
|
+
const sessionId = getSessionIdentifier();
|
|
407
|
+
const stateFile = path.join(STATE_DIR, `session_state_${sessionId}.json`);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
if (fs.existsSync(stateFile)) {
|
|
411
|
+
const data = fs.readFileSync(stateFile, 'utf8');
|
|
412
|
+
return JSON.parse(data);
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
debugLog(`Could not read session state: ${error.message}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const projectName = getProjectName();
|
|
419
|
+
return {
|
|
420
|
+
lastStopTime: null,
|
|
421
|
+
lastActivityTime: Date.now(),
|
|
422
|
+
sessionId: sessionId,
|
|
423
|
+
pendingCompletion: false,
|
|
424
|
+
project: projectName,
|
|
425
|
+
recentTools: [],
|
|
426
|
+
recentFiles: [],
|
|
427
|
+
totalToolUses: 0,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function saveSessionState(state) {
|
|
432
|
+
ensureStateDir();
|
|
433
|
+
const sessionId = getSessionIdentifier();
|
|
434
|
+
const stateFile = path.join(STATE_DIR, `session_state_${sessionId}.json`);
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
438
|
+
} catch (error) {
|
|
439
|
+
debugLog(`Could not save session state: ${error.message}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================
|
|
444
|
+
// SECTION 6: Event Processor (Source-Agnostic)
|
|
445
|
+
// ============================================
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Process common events - NO source-specific logic here
|
|
449
|
+
*/
|
|
450
|
+
async function processEvent(event) {
|
|
451
|
+
debugLog(`Processing event: ${event.eventType} from ${event.source}`);
|
|
452
|
+
|
|
453
|
+
switch (event.eventType) {
|
|
454
|
+
case 'tool.before':
|
|
455
|
+
handleToolStart(event);
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case 'tool.after':
|
|
459
|
+
handleToolEnd(event);
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
case 'session.idle':
|
|
463
|
+
handleSessionIdle(event);
|
|
464
|
+
break;
|
|
465
|
+
|
|
466
|
+
case 'session.start':
|
|
467
|
+
handleSessionStart(event);
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case 'permission':
|
|
471
|
+
await handlePermission(event);
|
|
472
|
+
break;
|
|
473
|
+
|
|
474
|
+
case 'permission_notification':
|
|
475
|
+
// Fire-and-forget: a Notification event with permission_prompt type.
|
|
476
|
+
// Does NOT block or poll — just informs the user that a permission dialog is open.
|
|
477
|
+
handlePermissionNotification(event);
|
|
478
|
+
break;
|
|
479
|
+
|
|
480
|
+
case 'question':
|
|
481
|
+
handleQuestion(event);
|
|
482
|
+
break;
|
|
483
|
+
|
|
484
|
+
case 'idle_input':
|
|
485
|
+
handleIdleInput(event);
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case 'intervention':
|
|
489
|
+
handleIntervention(event);
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case 'error':
|
|
493
|
+
handleError(event);
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case 'check.completion':
|
|
497
|
+
handleCheckCompletion(event);
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case 'session.end':
|
|
501
|
+
handleSessionEnd(event);
|
|
502
|
+
break;
|
|
503
|
+
|
|
504
|
+
case 'subagent.start':
|
|
505
|
+
handleSubagentStart(event);
|
|
506
|
+
break;
|
|
507
|
+
|
|
508
|
+
case 'subagent.stop':
|
|
509
|
+
handleSubagentStop(event);
|
|
510
|
+
break;
|
|
511
|
+
|
|
512
|
+
case 'user.prompt':
|
|
513
|
+
handleUserPrompt(event);
|
|
514
|
+
break;
|
|
515
|
+
|
|
516
|
+
case 'teammate.idle':
|
|
517
|
+
handleTeammateIdle(event);
|
|
518
|
+
break;
|
|
519
|
+
|
|
520
|
+
case 'task.completed':
|
|
521
|
+
handleTaskCompleted(event);
|
|
522
|
+
break;
|
|
523
|
+
|
|
524
|
+
case 'context.compact':
|
|
525
|
+
debugLog('Context compact event - tracking only');
|
|
526
|
+
break;
|
|
527
|
+
|
|
528
|
+
case 'session.status':
|
|
529
|
+
// CRITICAL: session.status is NOT real activity (internal updates)
|
|
530
|
+
debugLog('session.status event - ignoring (not real activity)');
|
|
531
|
+
break;
|
|
532
|
+
|
|
533
|
+
default:
|
|
534
|
+
debugLog(`Ignoring ${event.eventType} event (not relevant)`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ============================================
|
|
539
|
+
// SECTION 7: Event Handlers
|
|
540
|
+
// ============================================
|
|
541
|
+
|
|
542
|
+
function handleToolStart(event) {
|
|
543
|
+
const state = getSessionState();
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
|
|
546
|
+
debugLog(`Tool starting: ${event.data.tool || 'unknown'}`);
|
|
547
|
+
|
|
548
|
+
state.lastActivityTime = now;
|
|
549
|
+
state.pendingCompletion = false;
|
|
550
|
+
|
|
551
|
+
if (!state.recentTools) state.recentTools = [];
|
|
552
|
+
if (!state.totalToolUses) state.totalToolUses = 0;
|
|
553
|
+
|
|
554
|
+
state.recentTools.unshift(event.data.tool || 'unknown');
|
|
555
|
+
state.recentTools = state.recentTools.slice(0, 10);
|
|
556
|
+
state.totalToolUses++;
|
|
557
|
+
|
|
558
|
+
saveSessionState(state);
|
|
559
|
+
|
|
560
|
+
cancelCompletionTimer(event.projectName);
|
|
561
|
+
debugLog(`Activity detected, completion timer cancelled (${state.totalToolUses} tools total)`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function handleToolEnd(event) {
|
|
565
|
+
const state = getSessionState();
|
|
566
|
+
state.lastActivityTime = Date.now();
|
|
567
|
+
saveSessionState(state);
|
|
568
|
+
debugLog(`Tool complete: ${event.data.tool || 'unknown'}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function handleSessionIdle(event) {
|
|
572
|
+
const state = getSessionState();
|
|
573
|
+
const now = Date.now();
|
|
574
|
+
|
|
575
|
+
debugLog(`Session idle detected - starting ${COMPLETION_TIMEOUT / 1000}s completion timer`);
|
|
576
|
+
|
|
577
|
+
state.lastStopTime = now;
|
|
578
|
+
state.pendingCompletion = true;
|
|
579
|
+
saveSessionState(state);
|
|
580
|
+
|
|
581
|
+
scheduleCompletionTimer(event.projectName);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function handleSessionStart(event) {
|
|
585
|
+
const state = getSessionState();
|
|
586
|
+
state.sessionId = Date.now().toString();
|
|
587
|
+
state.lastActivityTime = Date.now();
|
|
588
|
+
state.pendingCompletion = false;
|
|
589
|
+
saveSessionState(state);
|
|
590
|
+
cancelCompletionTimer(event.projectName);
|
|
591
|
+
debugLog(`New session started: ${state.sessionId}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Handle permission events (agent needs user to approve a tool)
|
|
596
|
+
*
|
|
597
|
+
* Builds a rich notification with tool name + details when available,
|
|
598
|
+
* falls back to the message text when tool details aren't available.
|
|
599
|
+
* Content is identical between Claude Code and OpenCode.
|
|
600
|
+
*/
|
|
601
|
+
async function handlePermission(event) {
|
|
602
|
+
const d = event.data;
|
|
603
|
+
debugLog(`Permission event: tool=${d.tool}, message=${d.message}`);
|
|
604
|
+
|
|
605
|
+
const { title, body } = buildPermissionNotification(event);
|
|
606
|
+
|
|
607
|
+
// Check if WebSocket clients are connected — if so, the events channel
|
|
608
|
+
// will broadcast the permission-requested event and we skip the push notification
|
|
609
|
+
const wsActive = await hasWebSocketClients();
|
|
610
|
+
|
|
611
|
+
// For Claude Code PermissionRequest: block and poll for iPhone response
|
|
612
|
+
if (IS_CLAUDE_CODE && event.source === 'claude-code') {
|
|
613
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
614
|
+
debugLog(`Starting bidirectional permission flow (requestId: ${requestId})`);
|
|
615
|
+
|
|
616
|
+
let result;
|
|
617
|
+
if (wsActive) {
|
|
618
|
+
// WebSocket clients connected — skip push notification, but still register
|
|
619
|
+
// the pending request on the server so polling can find it.
|
|
620
|
+
debugLog(`[Notifier] WebSocket clients connected, skipping push notification`);
|
|
621
|
+
if (IS_CLAUDE_CODE) {
|
|
622
|
+
console.error(`\n=== WEBSOCKET ACTIVE — SKIPPING PUSH [${requestId}] ===`);
|
|
623
|
+
console.error(`Title: ${title}`);
|
|
624
|
+
console.error(`Message: ${body}`);
|
|
625
|
+
console.error(`=== REGISTERING REQUEST & POLLING VIA WEBSOCKET CHANNEL ===\n`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// POST to /api/notify with waitForResponse so the server creates a pending
|
|
629
|
+
// request entry. Without this, GET /api/response returns 404 every time.
|
|
630
|
+
result = await sendNotificationAndPoll(
|
|
631
|
+
title,
|
|
632
|
+
body,
|
|
633
|
+
'permission',
|
|
634
|
+
event.source,
|
|
635
|
+
requestId,
|
|
636
|
+
d
|
|
637
|
+
);
|
|
638
|
+
} else {
|
|
639
|
+
// No WebSocket clients — send push notification and poll
|
|
640
|
+
result = await sendNotificationAndPoll(
|
|
641
|
+
title,
|
|
642
|
+
body,
|
|
643
|
+
'permission',
|
|
644
|
+
event.source,
|
|
645
|
+
requestId,
|
|
646
|
+
d
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (result && result.decision) {
|
|
651
|
+
const hookResponse = {
|
|
652
|
+
hookSpecificOutput: {
|
|
653
|
+
hookEventName: 'PermissionRequest',
|
|
654
|
+
permissionDecision: result.decision,
|
|
655
|
+
permissionDecisionReason: `User ${result.decision === 'allow' ? 'approved' : 'denied'} via ${wsActive ? 'WebSocket' : 'iPhone notification'}`,
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
// Write decision to stdout for Claude Code to read
|
|
659
|
+
process.stdout.write(JSON.stringify(hookResponse));
|
|
660
|
+
debugLog(`Wrote hook decision to stdout: ${result.decision}`);
|
|
661
|
+
} else {
|
|
662
|
+
debugLog('No response received - falling through to local permission dialog');
|
|
663
|
+
// Output nothing → Claude Code shows normal permission dialog
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// For OpenCode or non-blocking: fire-and-forget as before
|
|
669
|
+
if (wsActive) {
|
|
670
|
+
debugLog(`[Notifier] WebSocket clients connected, skipping push notification for permission`);
|
|
671
|
+
} else {
|
|
672
|
+
sendNotification(title, body, 'permission', event.source);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Handle permission_notification events (Notification hook with permission_prompt type).
|
|
678
|
+
*
|
|
679
|
+
* Unlike handlePermission(), this does NOT block or poll for a response.
|
|
680
|
+
* It just sends a fire-and-forget notification to inform the user that
|
|
681
|
+
* Claude Code's local permission dialog is open.
|
|
682
|
+
*/
|
|
683
|
+
async function handlePermissionNotification(event) {
|
|
684
|
+
const d = event.data;
|
|
685
|
+
debugLog(`Permission notification event (non-blocking): message=${d.message}`);
|
|
686
|
+
|
|
687
|
+
// Skip push if WebSocket clients are connected (they get the event via the events channel)
|
|
688
|
+
const wsActive = await hasWebSocketClients();
|
|
689
|
+
if (wsActive) {
|
|
690
|
+
debugLog(`[Notifier] WebSocket clients connected, skipping push for permission_notification`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const { title, body } = buildPermissionNotification(event);
|
|
695
|
+
sendNotification(title, body, 'permission', event.source);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Handle question/elicitation events (agent is asking user a question)
|
|
700
|
+
*
|
|
701
|
+
* Includes the question text and options when available.
|
|
702
|
+
* Content is identical between Claude Code and OpenCode.
|
|
703
|
+
*/
|
|
704
|
+
function handleQuestion(event) {
|
|
705
|
+
const d = event.data;
|
|
706
|
+
debugLog(`Question event: message=${d.message}`);
|
|
707
|
+
|
|
708
|
+
const { title, body } = buildQuestionNotification(event);
|
|
709
|
+
|
|
710
|
+
sendNotification(title, body, 'question', event.source);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Handle idle input events (agent is idle, waiting for user to type)
|
|
715
|
+
*
|
|
716
|
+
* Lighter notification - just tells user the agent is waiting.
|
|
717
|
+
*/
|
|
718
|
+
function handleIdleInput(event) {
|
|
719
|
+
const d = event.data;
|
|
720
|
+
debugLog(`Idle input event: message=${d.message}`);
|
|
721
|
+
|
|
722
|
+
const title = `Waiting for Input`;
|
|
723
|
+
const body = d.message || `Agent is waiting for your input in ${event.projectName}`;
|
|
724
|
+
|
|
725
|
+
sendNotification(title, body, 'idle_input', event.source);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Handle generic intervention (fallback for unrecognizable events)
|
|
730
|
+
*
|
|
731
|
+
* Sends whatever information is available - never drops a notification.
|
|
732
|
+
*/
|
|
733
|
+
function handleIntervention(event) {
|
|
734
|
+
const d = event.data;
|
|
735
|
+
debugLog(`Intervention event: message=${d.message}`);
|
|
736
|
+
|
|
737
|
+
const title = d.title || `Needs Attention`;
|
|
738
|
+
const body = d.message || `Needs your attention in ${event.projectName}`;
|
|
739
|
+
|
|
740
|
+
sendNotification(title, body, 'intervention', event.source);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function handleError(event) {
|
|
744
|
+
debugLog(`Error detected: ${event.data.message}`);
|
|
745
|
+
sendNotification(
|
|
746
|
+
`Error in ${event.projectName}`,
|
|
747
|
+
event.data.message || 'An error occurred',
|
|
748
|
+
'error',
|
|
749
|
+
event.source
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function handleCheckCompletion(event) {
|
|
754
|
+
debugLog(`Manual completion check requested`);
|
|
755
|
+
checkCompletion(event.projectName, event.source);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function handleSessionEnd(event) {
|
|
759
|
+
const state = getSessionState();
|
|
760
|
+
state.pendingCompletion = false;
|
|
761
|
+
saveSessionState(state);
|
|
762
|
+
cancelCompletionTimer(event.projectName);
|
|
763
|
+
debugLog('Session ended - cleaned up state');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function handleSubagentStart(event) {
|
|
767
|
+
const state = getSessionState();
|
|
768
|
+
state.lastActivityTime = Date.now();
|
|
769
|
+
state.pendingCompletion = false;
|
|
770
|
+
saveSessionState(state);
|
|
771
|
+
cancelCompletionTimer(event.projectName);
|
|
772
|
+
debugLog(`Subagent started: ${event.data.agentType}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function handleSubagentStop(event) {
|
|
776
|
+
const state = getSessionState();
|
|
777
|
+
state.lastActivityTime = Date.now();
|
|
778
|
+
saveSessionState(state);
|
|
779
|
+
debugLog(`Subagent stopped: ${event.data.agentType}`);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function handleUserPrompt(event) {
|
|
783
|
+
const state = getSessionState();
|
|
784
|
+
state.lastActivityTime = Date.now();
|
|
785
|
+
state.pendingCompletion = false;
|
|
786
|
+
saveSessionState(state);
|
|
787
|
+
cancelCompletionTimer(event.projectName);
|
|
788
|
+
debugLog('User prompt submitted - activity detected');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function handleTeammateIdle(event) {
|
|
792
|
+
debugLog(`Teammate idle: ${event.data.teammate}`);
|
|
793
|
+
sendNotification(
|
|
794
|
+
`Teammate Idle`,
|
|
795
|
+
`${event.data.teammate} is idle in ${event.projectName}`,
|
|
796
|
+
'teammate_idle',
|
|
797
|
+
event.source
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function handleTaskCompleted(event) {
|
|
802
|
+
debugLog(`Task completed: ${event.data.message}`);
|
|
803
|
+
sendNotification(
|
|
804
|
+
`Task Completed`,
|
|
805
|
+
event.data.message || `A task was completed in ${event.projectName}`,
|
|
806
|
+
'task_completed',
|
|
807
|
+
event.source
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ============================================
|
|
812
|
+
// SECTION 8: Notification Message Builders
|
|
813
|
+
// ============================================
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Build permission notification content.
|
|
817
|
+
* Same structure regardless of source (Claude Code or OpenCode).
|
|
818
|
+
*
|
|
819
|
+
* When we have tool details:
|
|
820
|
+
* Title: "Permission: Bash"
|
|
821
|
+
* Body: "npm test" or "Allow: rm -rf /tmp/build"
|
|
822
|
+
*
|
|
823
|
+
* When we only have a message:
|
|
824
|
+
* Title: "Permission Needed"
|
|
825
|
+
* Body: "Claude needs your permission to use Bash"
|
|
826
|
+
*/
|
|
827
|
+
function buildPermissionNotification(event) {
|
|
828
|
+
const d = event.data;
|
|
829
|
+
const toolName = d.tool || '';
|
|
830
|
+
const command = d.command || '';
|
|
831
|
+
const filePath = d.filePath || '';
|
|
832
|
+
const description = d.description || '';
|
|
833
|
+
const message = d.message || '';
|
|
834
|
+
|
|
835
|
+
// Case 1: We know the tool name and have details
|
|
836
|
+
if (toolName && toolName !== 'Unknown' && toolName !== '') {
|
|
837
|
+
const title = `Permission: ${toolName}`;
|
|
838
|
+
let body = '';
|
|
839
|
+
|
|
840
|
+
if (toolName === 'Bash' && command) {
|
|
841
|
+
// For Bash, show the command
|
|
842
|
+
body = command.length > 200 ? command.substring(0, 200) + '...' : command;
|
|
843
|
+
} else if ((toolName === 'Edit' || toolName === 'Write' || toolName === 'Read') && filePath) {
|
|
844
|
+
// For file operations, show the file path
|
|
845
|
+
body = filePath;
|
|
846
|
+
} else if (description) {
|
|
847
|
+
body = description;
|
|
848
|
+
} else if (command) {
|
|
849
|
+
body = command;
|
|
850
|
+
} else if (filePath) {
|
|
851
|
+
body = filePath;
|
|
852
|
+
} else {
|
|
853
|
+
body = `Approve ${toolName} in ${event.projectName}`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return { title, body };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Case 2: We only have a message (e.g., from Notification event)
|
|
860
|
+
if (message) {
|
|
861
|
+
return {
|
|
862
|
+
title: `Permission Needed`,
|
|
863
|
+
body: message,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Case 3: Minimal fallback
|
|
868
|
+
return {
|
|
869
|
+
title: `Permission Needed`,
|
|
870
|
+
body: `Agent needs permission in ${event.projectName}`,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Build question/elicitation notification content.
|
|
876
|
+
* Same structure regardless of source.
|
|
877
|
+
*
|
|
878
|
+
* Handles two formats:
|
|
879
|
+
* 1. Claude Code: { message: "question text", title: "..." }
|
|
880
|
+
* 2. OpenCode: { questions: [{ header, options: [{ label, description }] }] }
|
|
881
|
+
*
|
|
882
|
+
* Output is always:
|
|
883
|
+
* Title: "Question: <header>" or "Question"
|
|
884
|
+
* Body: "<question text> | Options: A / B / C"
|
|
885
|
+
*/
|
|
886
|
+
function buildQuestionNotification(event) {
|
|
887
|
+
const d = event.data;
|
|
888
|
+
const message = d.message || '';
|
|
889
|
+
const title = d.title || '';
|
|
890
|
+
const questions = d.questions || [];
|
|
891
|
+
|
|
892
|
+
// Case 1: OpenCode question.asked with structured questions array
|
|
893
|
+
if (questions.length > 0) {
|
|
894
|
+
const q = questions[0]; // Use first question
|
|
895
|
+
const header = q.header || q.question || '';
|
|
896
|
+
const options = (q.options || []).map((o) => o.label).filter(Boolean);
|
|
897
|
+
|
|
898
|
+
const notifTitle = header ? `Question: ${header}` : 'Question';
|
|
899
|
+
let body = q.question || header || '';
|
|
900
|
+
|
|
901
|
+
if (options.length > 0) {
|
|
902
|
+
body = body ? `${body} | Options: ${options.join(' / ')}` : `Options: ${options.join(' / ')}`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (!body) {
|
|
906
|
+
body = `Agent is asking a question in ${event.projectName}`;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return {
|
|
910
|
+
title: notifTitle,
|
|
911
|
+
body: body.length > 300 ? body.substring(0, 300) + '...' : body,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Case 2: Claude Code notification with message text
|
|
916
|
+
const notifTitle = title && title !== 'Permission needed' ? title : 'Question';
|
|
917
|
+
|
|
918
|
+
if (message) {
|
|
919
|
+
return {
|
|
920
|
+
title: notifTitle,
|
|
921
|
+
body: message.length > 300 ? message.substring(0, 300) + '...' : message,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Case 3: Minimal fallback
|
|
926
|
+
return {
|
|
927
|
+
title: 'Question',
|
|
928
|
+
body: `Agent is asking a question in ${event.projectName}`,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ============================================
|
|
933
|
+
// SECTION 9: Completion Timer Management
|
|
934
|
+
// ============================================
|
|
935
|
+
|
|
936
|
+
function scheduleCompletionTimer(projectName) {
|
|
937
|
+
if (IS_CLAUDE_CODE) {
|
|
938
|
+
// Completion timer cannot work in Claude Code (each hook is a separate process)
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
debugLog(`Scheduling completion check for ${projectName}`);
|
|
942
|
+
cancelCompletionTimer(projectName);
|
|
943
|
+
|
|
944
|
+
const timer = setTimeout(() => {
|
|
945
|
+
debugLog(`Completion timer fired for ${projectName}`);
|
|
946
|
+
checkCompletion(projectName, RUNTIME);
|
|
947
|
+
}, COMPLETION_TIMEOUT);
|
|
948
|
+
|
|
949
|
+
completionTimers.set(projectName, timer);
|
|
950
|
+
debugLog(`Completion timer scheduled (45s)`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function cancelCompletionTimer(projectName) {
|
|
954
|
+
const existingTimer = completionTimers.get(projectName);
|
|
955
|
+
if (existingTimer) {
|
|
956
|
+
clearTimeout(existingTimer);
|
|
957
|
+
completionTimers.delete(projectName);
|
|
958
|
+
debugLog(`Completion timer cancelled for ${projectName}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function checkCompletion(projectName, source) {
|
|
963
|
+
if (IS_CLAUDE_CODE) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const state = getSessionState();
|
|
967
|
+
const now = Date.now();
|
|
968
|
+
|
|
969
|
+
debugLog(`Checking completion status for ${projectName}`);
|
|
970
|
+
debugLog(` pendingCompletion: ${state.pendingCompletion}`);
|
|
971
|
+
debugLog(` lastStopTime: ${state.lastStopTime}`);
|
|
972
|
+
debugLog(` lastActivityTime: ${state.lastActivityTime}`);
|
|
973
|
+
|
|
974
|
+
if (
|
|
975
|
+
state.pendingCompletion &&
|
|
976
|
+
state.lastStopTime &&
|
|
977
|
+
state.lastActivityTime <= state.lastStopTime &&
|
|
978
|
+
now - state.lastStopTime >= COMPLETION_TIMEOUT
|
|
979
|
+
) {
|
|
980
|
+
debugLog(`Conditions met - sending completion notification`);
|
|
981
|
+
|
|
982
|
+
const message = createCompletionMessage(state, projectName);
|
|
983
|
+
|
|
984
|
+
sendNotification(`${projectName} Complete`, message, 'completion', source);
|
|
985
|
+
|
|
986
|
+
state.pendingCompletion = false;
|
|
987
|
+
saveSessionState(state);
|
|
988
|
+
} else {
|
|
989
|
+
debugLog(`No completion notification needed`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function createCompletionMessage(state, projectName) {
|
|
994
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
995
|
+
let message = `Session completed in ${projectName} at ${timestamp}`;
|
|
996
|
+
|
|
997
|
+
const totalTools = state.totalToolUses || 0;
|
|
998
|
+
if (totalTools > 0) {
|
|
999
|
+
message += ` | ${totalTools} tools used`;
|
|
1000
|
+
|
|
1001
|
+
if (state.recentTools && state.recentTools.length > 0) {
|
|
1002
|
+
const toolSummary = state.recentTools.slice(0, 3).join(', ');
|
|
1003
|
+
message += ` | Recent: ${toolSummary}`;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (state.recentFiles && state.recentFiles.length > 0) {
|
|
1007
|
+
const fileSummary = state.recentFiles.slice(0, 3).join(', ');
|
|
1008
|
+
message += ` | Files: ${fileSummary}`;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return message;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ============================================
|
|
1016
|
+
// SECTION 10: Notification Service
|
|
1017
|
+
// ============================================
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Send a notification and poll the server for a user response.
|
|
1021
|
+
* Used for PermissionRequest bidirectional flow.
|
|
1022
|
+
*
|
|
1023
|
+
* Returns { decision: 'allow' | 'deny' } or null on timeout.
|
|
1024
|
+
*/
|
|
1025
|
+
function sendNotificationAndPoll(title, body, category, source, requestId, eventData) {
|
|
1026
|
+
return new Promise((resolve) => {
|
|
1027
|
+
const timestamp = new Date().toISOString();
|
|
1028
|
+
|
|
1029
|
+
const runtimePrefix = source === 'opencode' ? '[OpenCode]' : '[Claude]';
|
|
1030
|
+
const envPrefix = USE_LOCAL ? '[LOCAL]' : '';
|
|
1031
|
+
const finalTitle = `${runtimePrefix}${envPrefix ? ' ' + envPrefix : ''} ${title}`;
|
|
1032
|
+
|
|
1033
|
+
debugLog(`Sending bidirectional notification: "${finalTitle}" (requestId: ${requestId})`);
|
|
1034
|
+
|
|
1035
|
+
const payload = JSON.stringify({
|
|
1036
|
+
title: finalTitle,
|
|
1037
|
+
message: body,
|
|
1038
|
+
waitForResponse: true,
|
|
1039
|
+
...(DEVICE_TOKEN && { deviceToken: DEVICE_TOKEN }),
|
|
1040
|
+
data: {
|
|
1041
|
+
category,
|
|
1042
|
+
project: getProjectName(),
|
|
1043
|
+
timestamp,
|
|
1044
|
+
requestId,
|
|
1045
|
+
clientTimestamp: timestamp,
|
|
1046
|
+
source: 'shooter-completion-detector',
|
|
1047
|
+
environment: USE_LOCAL ? 'local' : 'remote',
|
|
1048
|
+
runtime: source,
|
|
1049
|
+
toolName: eventData.tool || '',
|
|
1050
|
+
toolInput: eventData.toolInput || {},
|
|
1051
|
+
sessionId: eventData.sessionId || '',
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
const options = {
|
|
1056
|
+
method: 'POST',
|
|
1057
|
+
headers: {
|
|
1058
|
+
'Content-Type': 'application/json',
|
|
1059
|
+
Authorization: `Bearer ${AUTH_KEY}`,
|
|
1060
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
1061
|
+
'User-Agent': `Shooter-Notifier/3.0 ${source}`,
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
// Step 1: Send the notification
|
|
1066
|
+
const protocol = API_URL.startsWith('https') ? https : http;
|
|
1067
|
+
const req = protocol.request(API_URL, options, (res) => {
|
|
1068
|
+
let responseData = '';
|
|
1069
|
+
res.on('data', (chunk) => (responseData += chunk));
|
|
1070
|
+
res.on('end', () => {
|
|
1071
|
+
if (IS_CLAUDE_CODE) {
|
|
1072
|
+
console.error(`\n=== BIDIRECTIONAL NOTIFICATION SENT [${requestId}] ===`);
|
|
1073
|
+
console.error(`Title: ${finalTitle}`);
|
|
1074
|
+
console.error(`Message: ${body}`);
|
|
1075
|
+
console.error(`Status: ${res.statusCode}`);
|
|
1076
|
+
console.error(`=== NOW POLLING FOR RESPONSE ===\n`);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (res.statusCode !== 200) {
|
|
1080
|
+
debugLog(`Notification send failed: ${res.statusCode} - falling through to local dialog`);
|
|
1081
|
+
resolve(null);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Step 2: Start polling for user response
|
|
1086
|
+
startPolling(requestId, resolve);
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
req.on('error', (error) => {
|
|
1091
|
+
debugLog(`Notification request error: ${error.message} - falling through to local dialog`);
|
|
1092
|
+
resolve(null);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
req.setTimeout(10000, () => {
|
|
1096
|
+
req.destroy(new Error('Request timeout'));
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
req.write(payload);
|
|
1100
|
+
req.end();
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Poll GET /api/response?requestId=xxx every POLL_INTERVAL until decided or timeout.
|
|
1106
|
+
*/
|
|
1107
|
+
function startPolling(requestId, resolve) {
|
|
1108
|
+
const startTime = Date.now();
|
|
1109
|
+
let resolved = false;
|
|
1110
|
+
|
|
1111
|
+
const overallTimeout = setTimeout(() => {
|
|
1112
|
+
if (!resolved) {
|
|
1113
|
+
resolved = true;
|
|
1114
|
+
clearInterval(pollTimer);
|
|
1115
|
+
debugLog(
|
|
1116
|
+
`Permission polling timed out after ${PERMISSION_TIMEOUT / 1000}s - falling through to local dialog`
|
|
1117
|
+
);
|
|
1118
|
+
if (IS_CLAUDE_CODE) {
|
|
1119
|
+
console.error(`\n=== PERMISSION TIMEOUT [${requestId}] ===`);
|
|
1120
|
+
console.error(
|
|
1121
|
+
`No response after ${PERMISSION_TIMEOUT / 1000}s - falling through to local dialog`
|
|
1122
|
+
);
|
|
1123
|
+
console.error(`=== END ===\n`);
|
|
1124
|
+
}
|
|
1125
|
+
resolve(null);
|
|
1126
|
+
}
|
|
1127
|
+
}, PERMISSION_TIMEOUT);
|
|
1128
|
+
|
|
1129
|
+
const pollTimer = setInterval(() => {
|
|
1130
|
+
if (resolved) {
|
|
1131
|
+
clearInterval(pollTimer);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
1136
|
+
debugLog(`Polling for response (${elapsed}s elapsed)...`);
|
|
1137
|
+
|
|
1138
|
+
const pollUrl = `${RESPONSE_URL}?requestId=${encodeURIComponent(requestId)}`;
|
|
1139
|
+
const pollOptions = {
|
|
1140
|
+
method: 'GET',
|
|
1141
|
+
headers: {
|
|
1142
|
+
Authorization: `Bearer ${AUTH_KEY}`,
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const protocol = RESPONSE_URL.startsWith('https') ? https : http;
|
|
1147
|
+
const pollReq = protocol.request(pollUrl, pollOptions, (res) => {
|
|
1148
|
+
let data = '';
|
|
1149
|
+
res.on('data', (chunk) => (data += chunk));
|
|
1150
|
+
res.on('end', () => {
|
|
1151
|
+
if (resolved) return;
|
|
1152
|
+
|
|
1153
|
+
try {
|
|
1154
|
+
const result = JSON.parse(data);
|
|
1155
|
+
if (result.status === 'decided' && result.decision) {
|
|
1156
|
+
resolved = true;
|
|
1157
|
+
clearInterval(pollTimer);
|
|
1158
|
+
clearTimeout(overallTimeout);
|
|
1159
|
+
|
|
1160
|
+
debugLog(`Decision received: ${result.decision} (after ${elapsed}s)`);
|
|
1161
|
+
if (IS_CLAUDE_CODE) {
|
|
1162
|
+
console.error(`\n=== DECISION RECEIVED [${requestId}] ===`);
|
|
1163
|
+
console.error(`Decision: ${result.decision}`);
|
|
1164
|
+
console.error(`Elapsed: ${elapsed}s`);
|
|
1165
|
+
console.error(`=== END ===\n`);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
resolve({ decision: result.decision });
|
|
1169
|
+
}
|
|
1170
|
+
// status === 'pending' → keep polling
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
debugLog(`Poll parse error: ${e.message}`);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
pollReq.on('error', (error) => {
|
|
1178
|
+
debugLog(`Poll request error: ${error.message}`);
|
|
1179
|
+
// Don't resolve on poll error — keep trying until timeout
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
pollReq.setTimeout(10000, () => {
|
|
1183
|
+
pollReq.destroy(new Error('Request timeout'));
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
pollReq.end();
|
|
1187
|
+
}, POLL_INTERVAL);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function sendNotification(title, body, category = 'completion', source = RUNTIME) {
|
|
1191
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
1192
|
+
const timestamp = new Date().toISOString();
|
|
1193
|
+
|
|
1194
|
+
// Prefix: [Claude] or [OpenCode], optionally [LOCAL]
|
|
1195
|
+
const runtimePrefix = source === 'opencode' ? '[OpenCode]' : '[Claude]';
|
|
1196
|
+
const envPrefix = USE_LOCAL ? '[LOCAL]' : '';
|
|
1197
|
+
const finalTitle = `${runtimePrefix}${envPrefix ? ' ' + envPrefix : ''} ${title}`;
|
|
1198
|
+
|
|
1199
|
+
debugLog(`Sending notification: "${finalTitle}"`);
|
|
1200
|
+
debugLog(` Message: "${body.substring(0, 100)}..."`);
|
|
1201
|
+
debugLog(` Category: ${category}, RequestID: ${requestId}`);
|
|
1202
|
+
|
|
1203
|
+
const payload = JSON.stringify({
|
|
1204
|
+
title: finalTitle,
|
|
1205
|
+
message: body,
|
|
1206
|
+
...(DEVICE_TOKEN && { deviceToken: DEVICE_TOKEN }),
|
|
1207
|
+
data: {
|
|
1208
|
+
category,
|
|
1209
|
+
project: getProjectName(),
|
|
1210
|
+
timestamp,
|
|
1211
|
+
requestId,
|
|
1212
|
+
clientTimestamp: timestamp,
|
|
1213
|
+
source: 'shooter-completion-detector',
|
|
1214
|
+
environment: USE_LOCAL ? 'local' : 'remote',
|
|
1215
|
+
runtime: source,
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
const options = {
|
|
1220
|
+
method: 'POST',
|
|
1221
|
+
headers: {
|
|
1222
|
+
'Content-Type': 'application/json',
|
|
1223
|
+
Authorization: `Bearer ${AUTH_KEY}`,
|
|
1224
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
1225
|
+
'User-Agent': `Shooter-Notifier/3.0 ${source}`,
|
|
1226
|
+
},
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
const protocol = API_URL.startsWith('https') ? https : http;
|
|
1230
|
+
const req = protocol.request(API_URL, options, (res) => {
|
|
1231
|
+
let responseData = '';
|
|
1232
|
+
res.on('data', (chunk) => (responseData += chunk));
|
|
1233
|
+
res.on('end', () => {
|
|
1234
|
+
if (IS_CLAUDE_CODE) {
|
|
1235
|
+
console.error(`\n=== NOTIFICATION SENT [${requestId}] @ ${timestamp} ===`);
|
|
1236
|
+
console.error(`Project: ${getProjectName()}`);
|
|
1237
|
+
console.error(`Category: ${category}`);
|
|
1238
|
+
console.error(`Title: ${finalTitle}`);
|
|
1239
|
+
console.error(`Message: ${body}`);
|
|
1240
|
+
console.error(`API URL: ${API_URL} (${USE_LOCAL ? 'LOCAL' : 'REMOTE'})`);
|
|
1241
|
+
console.error(`Status Code: ${res.statusCode}`);
|
|
1242
|
+
console.error(`Response: ${responseData}`);
|
|
1243
|
+
console.error(`=== END NOTIFICATION ===\n`);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (res.statusCode !== 200) {
|
|
1247
|
+
debugLog(`HTTP ERROR: ${res.statusCode} ${responseData}`);
|
|
1248
|
+
} else {
|
|
1249
|
+
debugLog(`Notification sent successfully`);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
req.on('error', (error) => {
|
|
1255
|
+
debugLog(`Request error: ${error.message}`);
|
|
1256
|
+
if (IS_CLAUDE_CODE) {
|
|
1257
|
+
console.error('Notification request error:', error.message);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
req.setTimeout(10000, () => {
|
|
1262
|
+
req.destroy(new Error('Request timeout'));
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
req.write(payload);
|
|
1266
|
+
req.end();
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ============================================
|
|
1270
|
+
// Utility: Debug Logging
|
|
1271
|
+
// ============================================
|
|
1272
|
+
|
|
1273
|
+
function debugLog(msg) {
|
|
1274
|
+
if (!DEBUG_ENABLED) return;
|
|
1275
|
+
|
|
1276
|
+
try {
|
|
1277
|
+
const timestamp = new Date().toISOString();
|
|
1278
|
+
const logFile = IS_CLAUDE_CODE ? DEBUG_LOG_FILE : '/tmp/shooter-opencode-debug.log';
|
|
1279
|
+
fs.writeFileSync(logFile, `[${timestamp}] ${msg}\n`, { flag: 'a' });
|
|
1280
|
+
} catch (e) {
|
|
1281
|
+
// Silent fail
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// ============================================
|
|
1286
|
+
// SECTION 11: Entry Points
|
|
1287
|
+
// ============================================
|
|
1288
|
+
|
|
1289
|
+
// ============================================
|
|
1290
|
+
// 11A: Claude Code CLI Entry Point
|
|
1291
|
+
// ============================================
|
|
1292
|
+
|
|
1293
|
+
async function claudeCodeMain() {
|
|
1294
|
+
// Validate required environment variables (only in Claude Code CLI mode)
|
|
1295
|
+
if (!USE_LOCAL && !REMOTE_BASE_URL) {
|
|
1296
|
+
console.error(
|
|
1297
|
+
'SHOOTER_API_URL environment variable is required when SHOOTER_USE_LOCAL is not true'
|
|
1298
|
+
);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const cliArg = process.argv[2] || 'Unknown';
|
|
1303
|
+
|
|
1304
|
+
debugLog(`Shooter Notifier CLI invoked: ${cliArg}`);
|
|
1305
|
+
debugLog(` Runtime: ${RUNTIME}`);
|
|
1306
|
+
debugLog(` Environment: ${USE_LOCAL ? 'LOCAL' : 'REMOTE'}`);
|
|
1307
|
+
debugLog(` Session ID: ${getSessionIdentifier()}`);
|
|
1308
|
+
|
|
1309
|
+
// Read stdin JSON (Claude Code passes event data here)
|
|
1310
|
+
const stdinData = await readStdin();
|
|
1311
|
+
if (stdinData) {
|
|
1312
|
+
debugLog(` Stdin data received: ${JSON.stringify(stdinData).substring(0, 500)}`);
|
|
1313
|
+
} else {
|
|
1314
|
+
debugLog(` No stdin data (legacy mode or TTY)`);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Adapt CLI event + stdin data to common format
|
|
1318
|
+
const event = adaptClaudeCodeEvent(cliArg, stdinData);
|
|
1319
|
+
|
|
1320
|
+
// Process the event (await for blocking handlers like PermissionRequest)
|
|
1321
|
+
await processEvent(event);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ============================================
|
|
1325
|
+
// 11B: OpenCode Plugin Entry Point
|
|
1326
|
+
// ============================================
|
|
1327
|
+
|
|
1328
|
+
const OpenCodePlugin = async (ctx) => {
|
|
1329
|
+
debugLog('Shooter Notifier plugin loaded');
|
|
1330
|
+
debugLog(` Runtime: ${RUNTIME}`);
|
|
1331
|
+
debugLog(` Environment: ${USE_LOCAL ? 'LOCAL' : 'REMOTE'}`);
|
|
1332
|
+
debugLog(` API URL: ${API_URL}`);
|
|
1333
|
+
debugLog(` Session ID: ${getSessionIdentifier()}`);
|
|
1334
|
+
|
|
1335
|
+
// Extract project name from context
|
|
1336
|
+
let projectName = getProjectName();
|
|
1337
|
+
if (ctx?.project && typeof ctx.project === 'string') {
|
|
1338
|
+
projectName = ctx.project;
|
|
1339
|
+
} else if (ctx?.directory && typeof ctx.directory === 'string') {
|
|
1340
|
+
projectName = path.basename(ctx.directory);
|
|
1341
|
+
} else if (ctx?.project?.name && typeof ctx.project.name === 'string') {
|
|
1342
|
+
projectName = ctx.project.name;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
debugLog(`Project name: ${projectName}`);
|
|
1346
|
+
|
|
1347
|
+
return {
|
|
1348
|
+
// Generic event handler - catches ALL OpenCode events
|
|
1349
|
+
event: async ({ event }) => {
|
|
1350
|
+
if (!event || !event.type) return;
|
|
1351
|
+
|
|
1352
|
+
// Log ALL raw event types so we can discover what OpenCode sends
|
|
1353
|
+
debugLog(`[RAW EVENT] type=${event.type} keys=${Object.keys(event).join(',')}`);
|
|
1354
|
+
if (event.properties) {
|
|
1355
|
+
debugLog(`[RAW EVENT] properties=${JSON.stringify(event.properties).substring(0, 300)}`);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Extract properties from OpenCode event
|
|
1359
|
+
const props = event.properties || {};
|
|
1360
|
+
|
|
1361
|
+
const commonEvent = adaptOpenCodeEvent(event.type, {
|
|
1362
|
+
tool: event.tool || props.tool,
|
|
1363
|
+
toolInput: event.toolInput || event.args || {},
|
|
1364
|
+
command: event.command || event.args?.command || '',
|
|
1365
|
+
filePath: event.filePath || event.args?.filePath || '',
|
|
1366
|
+
files: event.files,
|
|
1367
|
+
message: event.message || event.error || props.message || '',
|
|
1368
|
+
questions: props.questions || [],
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
processEvent(commonEvent);
|
|
1372
|
+
},
|
|
1373
|
+
|
|
1374
|
+
// Specific hook: Before tool execution
|
|
1375
|
+
'tool.execute.before': async (input, output) => {
|
|
1376
|
+
const commonEvent = adaptOpenCodeEvent('tool.execute.before', {
|
|
1377
|
+
tool: input?.tool || 'unknown',
|
|
1378
|
+
toolInput: output?.args || {},
|
|
1379
|
+
command: output?.args?.command || '',
|
|
1380
|
+
filePath: output?.args?.filePath || '',
|
|
1381
|
+
});
|
|
1382
|
+
processEvent(commonEvent);
|
|
1383
|
+
},
|
|
1384
|
+
|
|
1385
|
+
// Specific hook: After tool execution
|
|
1386
|
+
'tool.execute.after': async (input, output) => {
|
|
1387
|
+
const commonEvent = adaptOpenCodeEvent('tool.execute.after', {
|
|
1388
|
+
tool: input?.tool || 'unknown',
|
|
1389
|
+
});
|
|
1390
|
+
processEvent(commonEvent);
|
|
1391
|
+
},
|
|
1392
|
+
|
|
1393
|
+
// Specific hook: Permission asked (agent needs user approval)
|
|
1394
|
+
'permission.asked': async (input, output) => {
|
|
1395
|
+
debugLog(`OpenCode permission.asked: tool=${input?.tool}`);
|
|
1396
|
+
const commonEvent = adaptOpenCodeEvent('permission.asked', {
|
|
1397
|
+
tool: input?.tool || 'unknown',
|
|
1398
|
+
toolInput: input?.args || output?.args || {},
|
|
1399
|
+
command: input?.args?.command || output?.args?.command || '',
|
|
1400
|
+
filePath: input?.args?.filePath || output?.args?.filePath || '',
|
|
1401
|
+
message: input?.message || '',
|
|
1402
|
+
});
|
|
1403
|
+
processEvent(commonEvent);
|
|
1404
|
+
},
|
|
1405
|
+
};
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// ============================================
|
|
1409
|
+
// Exports and Main Execution
|
|
1410
|
+
// ============================================
|
|
1411
|
+
|
|
1412
|
+
// Export for OpenCode plugin system
|
|
1413
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1414
|
+
module.exports = OpenCodePlugin;
|
|
1415
|
+
module.exports.OpenCodePlugin = OpenCodePlugin;
|
|
1416
|
+
module.exports.ShooterNotifier = OpenCodePlugin;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Run main() when called directly from CLI (Claude Code)
|
|
1420
|
+
if (IS_CLAUDE_CODE) {
|
|
1421
|
+
// Handle process cleanup (Claude Code CLI only)
|
|
1422
|
+
process.on('SIGINT', () => {
|
|
1423
|
+
process.exit(0);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
process.on('SIGTERM', () => {
|
|
1427
|
+
process.exit(0);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
claudeCodeMain();
|
|
1431
|
+
}
|