@ken-jo/agent-connector 0.4.97 → 0.4.99
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/LICENSE +202 -202
- package/NOTICE +6 -6
- package/README.md +604 -596
- package/dist/{action-IPFDPOZU.js → action-5OEM3QC4.js} +17 -16
- package/dist/action-5OEM3QC4.js.map +1 -0
- package/dist/{amazon-q-IT44LEWV.js → amazon-q-7DPVJXJN.js} +6 -6
- package/dist/amazon-q-7DPVJXJN.js.map +1 -0
- package/dist/{amp-CEWOSWQO.js → amp-BQZOL5GR.js} +4 -4
- package/dist/amp-BQZOL5GR.js.map +1 -0
- package/dist/{amp-P35FRU2Z.js → amp-VYXZWHXW.js} +7 -7
- package/dist/amp-VYXZWHXW.js.map +1 -0
- package/dist/{antigravity-LHFEFNMC.js → antigravity-EA4L225L.js} +5 -5
- package/dist/antigravity-EA4L225L.js.map +1 -0
- package/dist/antigravity-VXHL6ZPM.js +18 -0
- package/dist/{antigravity-cli-MOKFSWOU.js → antigravity-cli-B2HXZFYQ.js} +5 -5
- package/dist/antigravity-cli-B2HXZFYQ.js.map +1 -0
- package/dist/{antigravity-cli-MPE27LPI.js → antigravity-cli-JHQ7JGNX.js} +10 -9
- package/dist/antigravity-cli-JHQ7JGNX.js.map +1 -0
- package/dist/{audit-3E5JAOP2.js → audit-BO7JQYIH.js} +8 -8
- package/dist/{chunk-7C2IPIIB.js → chunk-2Y2MZRLP.js} +3 -3
- package/dist/chunk-2Y2MZRLP.js.map +1 -0
- package/dist/{chunk-GZZCHJDP.js → chunk-4KIXXJ7B.js} +2 -2
- package/dist/chunk-4KIXXJ7B.js.map +1 -0
- package/dist/chunk-4NFDNY3C.js +34 -0
- package/dist/chunk-4NFDNY3C.js.map +1 -0
- package/dist/{chunk-C63AAXK7.js → chunk-4QXCNZMD.js} +5 -5
- package/dist/chunk-4QXCNZMD.js.map +1 -0
- package/dist/{chunk-Q7PR2WGT.js → chunk-6TF7MHG5.js} +2 -2
- package/dist/chunk-6TF7MHG5.js.map +1 -0
- package/dist/{chunk-7ZFK2WEO.js → chunk-6WN37NVN.js} +113 -10
- package/dist/chunk-6WN37NVN.js.map +1 -0
- package/dist/{chunk-GCPEPUD7.js → chunk-6XMV2OOA.js} +6 -6
- package/dist/chunk-6XMV2OOA.js.map +1 -0
- package/dist/{chunk-XXLCMSD3.js → chunk-BEFVJ2OE.js} +1 -1
- package/dist/chunk-BEFVJ2OE.js.map +1 -0
- package/dist/{chunk-N4ENLZEU.js → chunk-BV5WK6VQ.js} +19 -19
- package/dist/chunk-BV5WK6VQ.js.map +1 -0
- package/dist/{chunk-WPTNMYYM.js → chunk-BXPSDNIB.js} +6 -6
- package/dist/chunk-BXPSDNIB.js.map +1 -0
- package/dist/{chunk-Q4ISQHUQ.js → chunk-FBJQGRET.js} +1 -1
- package/dist/chunk-FBJQGRET.js.map +1 -0
- package/dist/{chunk-THI4PI4H.js → chunk-FJCUKXGX.js} +2 -2
- package/dist/chunk-FJCUKXGX.js.map +1 -0
- package/dist/{chunk-L2C5UMKW.js → chunk-FKW5A6HE.js} +23 -7
- package/dist/chunk-FKW5A6HE.js.map +1 -0
- package/dist/{chunk-NDMNMAKP.js → chunk-G5HAIYRM.js} +43 -43
- package/dist/chunk-G5HAIYRM.js.map +1 -0
- package/dist/{chunk-SLJI5BOM.js → chunk-GDIAHWNY.js} +2 -2
- package/dist/chunk-GDIAHWNY.js.map +1 -0
- package/dist/{chunk-QYOUQCC6.js → chunk-GZGLCCEE.js} +1 -1
- package/dist/chunk-GZGLCCEE.js.map +1 -0
- package/dist/{chunk-PSGTHJQU.js → chunk-H53QZQJI.js} +1 -1
- package/dist/chunk-H53QZQJI.js.map +1 -0
- package/dist/{chunk-7HHW23AC.js → chunk-IRUYJD23.js} +29 -29
- package/dist/chunk-IRUYJD23.js.map +1 -0
- package/dist/{chunk-IOJDZJ72.js → chunk-IVAWP6HH.js} +2 -2
- package/dist/chunk-IVAWP6HH.js.map +1 -0
- package/dist/{chunk-3FINQ2ZF.js → chunk-JTN3AKZO.js} +30 -20
- package/dist/chunk-JTN3AKZO.js.map +1 -0
- package/dist/{chunk-4GH2KA7V.js → chunk-KHO5WNTP.js} +1 -1
- package/dist/chunk-KHO5WNTP.js.map +1 -0
- package/dist/{chunk-MLU7NDAZ.js → chunk-KLZBRNFP.js} +12 -12
- package/dist/chunk-KLZBRNFP.js.map +1 -0
- package/dist/{chunk-3EP5V2F4.js → chunk-M6OS4OLU.js} +3 -3
- package/dist/chunk-M6OS4OLU.js.map +1 -0
- package/dist/{chunk-UZZTWWGG.js → chunk-MQPNU2CY.js} +1 -1
- package/dist/chunk-MQPNU2CY.js.map +1 -0
- package/dist/{chunk-LQ55E7QI.js → chunk-N4STCQGP.js} +7 -7
- package/dist/chunk-N4STCQGP.js.map +1 -0
- package/dist/{chunk-KFPSJRHR.js → chunk-NBYKLUF6.js} +2 -2
- package/dist/chunk-NBYKLUF6.js.map +1 -0
- package/dist/{chunk-KM2YXO6V.js → chunk-O7SDRBVB.js} +1 -1
- package/dist/chunk-O7SDRBVB.js.map +1 -0
- package/dist/{chunk-LFWYYWLV.js → chunk-OP4KBXUO.js} +1 -1
- package/dist/chunk-OP4KBXUO.js.map +1 -0
- package/dist/{chunk-XN7S44U4.js → chunk-OPXAP3AC.js} +1 -1
- package/dist/chunk-OPXAP3AC.js.map +1 -0
- package/dist/{chunk-TGZFDXNH.js → chunk-PWCBWAM4.js} +6 -4
- package/dist/chunk-PWCBWAM4.js.map +1 -0
- package/dist/{chunk-Y3K6KJMS.js → chunk-QSY74NAP.js} +2 -2
- package/dist/chunk-QSY74NAP.js.map +1 -0
- package/dist/{chunk-ZSOS2KOR.js → chunk-RK6VBLXP.js} +1 -1
- package/dist/chunk-RK6VBLXP.js.map +1 -0
- package/dist/{chunk-FHYR5Q5T.js → chunk-SFY6YLVK.js} +19 -12
- package/dist/chunk-SFY6YLVK.js.map +1 -0
- package/dist/{chunk-C747DYNT.js → chunk-UM5S7DKK.js} +1 -1
- package/dist/chunk-UM5S7DKK.js.map +1 -0
- package/dist/{chunk-ILWHLGIC.js → chunk-UMQ6NBQR.js} +2 -2
- package/dist/chunk-UMQ6NBQR.js.map +1 -0
- package/dist/{chunk-7PSNXUFK.js → chunk-XD7MY6HB.js} +1 -1
- package/dist/chunk-XD7MY6HB.js.map +1 -0
- package/dist/{chunk-XVEJAFKW.js → chunk-XY3N2U6L.js} +5 -5
- package/dist/chunk-XY3N2U6L.js.map +1 -0
- package/dist/{chunk-XZWMJLIT.js → chunk-YT43MUSJ.js} +1 -1
- package/dist/chunk-YT43MUSJ.js.map +1 -0
- package/dist/{claude-code-QBWDT5RT.js → claude-code-LJ6DVXWQ.js} +24 -14
- package/dist/claude-code-LJ6DVXWQ.js.map +1 -0
- package/dist/{claude-code-FS42PXZO.js → claude-code-NMRKTAP7.js} +4 -4
- package/dist/claude-code-NMRKTAP7.js.map +1 -0
- package/dist/cli/sdk.js +7 -7
- package/dist/cli/sdk.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/{cline-JKEAYWVR.js → cline-JAPJNOZQ.js} +6 -6
- package/dist/cline-JAPJNOZQ.js.map +1 -0
- package/dist/{codebuddy-EB6EYPC3.js → codebuddy-GC7VAE2Y.js} +7 -7
- package/dist/codebuddy-GC7VAE2Y.js.map +1 -0
- package/dist/{codebuff-5RSXFU6G.js → codebuff-3VODFPD4.js} +6 -6
- package/dist/codebuff-3VODFPD4.js.map +1 -0
- package/dist/{codebuff-6KWBW6CT.js → codebuff-ETYT5UBL.js} +4 -4
- package/dist/codebuff-ETYT5UBL.js.map +1 -0
- package/dist/{codex-ART46CDD.js → codex-RXIS5BHM.js} +9 -9
- package/dist/codex-RXIS5BHM.js.map +1 -0
- package/dist/{codex-DKBAAUCO.js → codex-YAORPCOD.js} +4 -4
- package/dist/codex-YAORPCOD.js.map +1 -0
- package/dist/{continue-JHZCMSPH.js → continue-MECVDV4Z.js} +8 -8
- package/dist/continue-MECVDV4Z.js.map +1 -0
- package/dist/{copilot-cli-CLBJACQ5.js → copilot-cli-MZTAWMPW.js} +4 -4
- package/dist/copilot-cli-MZTAWMPW.js.map +1 -0
- package/dist/{copilot-cli-QVAU5CFM.js → copilot-cli-WVWAWNSM.js} +7 -7
- package/dist/copilot-cli-WVWAWNSM.js.map +1 -0
- package/dist/{crush-A5C5USRL.js → crush-QBXLNN7T.js} +4 -4
- package/dist/crush-QBXLNN7T.js.map +1 -0
- package/dist/{crush-7TORBYUL.js → crush-YB3SK6NK.js} +7 -7
- package/dist/crush-YB3SK6NK.js.map +1 -0
- package/dist/{cursor-D5SWDVSW.js → cursor-DIW43TRZ.js} +3 -3
- package/dist/cursor-DIW43TRZ.js.map +1 -0
- package/dist/cursor-RSLHA2CY.js +22 -0
- package/dist/{define-connector-DTWb46UB.d.ts → define-connector-odRHEC4H.d.ts} +1 -1
- package/dist/{detect-JHSA3G3U.js → detect-ALP4G7WG.js} +4 -4
- package/dist/detect-ALP4G7WG.js.map +1 -0
- package/dist/{devin-SSR7S4WX.js → devin-WPDN3MXE.js} +7 -7
- package/dist/devin-WPDN3MXE.js.map +1 -0
- package/dist/{doctor-CQJ7TZPA.js → doctor-YEQWCWZJ.js} +27 -26
- package/dist/doctor-YEQWCWZJ.js.map +1 -0
- package/dist/{droid-45PJ7JGF.js → droid-HS6MX3IL.js} +10 -8
- package/dist/droid-HS6MX3IL.js.map +1 -0
- package/dist/{droid-PFQ2PZSF.js → droid-U3ZBLQFO.js} +4 -4
- package/dist/droid-U3ZBLQFO.js.map +1 -0
- package/dist/{gemini-cli-Y6RJMU24.js → gemini-cli-NMVVBO3K.js} +4 -4
- package/dist/gemini-cli-NMVVBO3K.js.map +1 -0
- package/dist/{gemini-cli-24PFDK7D.js → gemini-cli-VJ6X5DNF.js} +8 -8
- package/dist/gemini-cli-VJ6X5DNF.js.map +1 -0
- package/dist/{goose-U5D3LCPC.js → goose-3X7ID7YW.js} +9 -9
- package/dist/goose-3X7ID7YW.js.map +1 -0
- package/dist/{goose-K2HKUTBK.js → goose-64V46Y2W.js} +4 -4
- package/dist/goose-64V46Y2W.js.map +1 -0
- package/dist/{grok-cli-L2MHAPBY.js → grok-cli-4VFVF2CQ.js} +7 -7
- package/dist/grok-cli-4VFVF2CQ.js.map +1 -0
- package/dist/{hermes-2S5JUTTJ.js → hermes-AIVUE7LX.js} +10 -8
- package/dist/hermes-AIVUE7LX.js.map +1 -0
- package/dist/{hermes-MF3NX4ZY.js → hermes-F3EGTK6N.js} +4 -4
- package/dist/hermes-F3EGTK6N.js.map +1 -0
- package/dist/{hook-VHICXHXL.js → hook-F63I3SD5.js} +17 -16
- package/dist/hook-F63I3SD5.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -5
- package/dist/{install-WLENR4KF.js → install-23WRRFBY.js} +21 -21
- package/dist/install-23WRRFBY.js.map +1 -0
- package/dist/{introspect-dNekqPDY.d.ts → introspect-gTx6GzpW.d.ts} +1 -1
- package/dist/{jetbrains-copilot-Z4FC3RM3.js → jetbrains-copilot-XBO2JZRO.js} +6 -6
- package/dist/jetbrains-copilot-XBO2JZRO.js.map +1 -0
- package/dist/{junie-ULGJJIHB.js → junie-UCOS6Q37.js} +6 -6
- package/dist/junie-UCOS6Q37.js.map +1 -0
- package/dist/{kilo-SMHPOHJ5.js → kilo-SW7BH6EG.js} +4 -4
- package/dist/kilo-SW7BH6EG.js.map +1 -0
- package/dist/{kilo-WWIJOVWD.js → kilo-T4OXNUYY.js} +8 -8
- package/dist/kilo-T4OXNUYY.js.map +1 -0
- package/dist/{kilo-cli-QKE2EQLS.js → kilo-cli-I2S3W245.js} +8 -8
- package/dist/kilo-cli-I2S3W245.js.map +1 -0
- package/dist/{kilo-cli-T2SRRRJS.js → kilo-cli-ZF272ZHN.js} +5 -5
- package/dist/kilo-cli-ZF272ZHN.js.map +1 -0
- package/dist/{kimi-MSIDEMCI.js → kimi-5FHEXGZD.js} +4 -4
- package/dist/kimi-5FHEXGZD.js.map +1 -0
- package/dist/{kimi-JMUFTVSF.js → kimi-VLSUFLYV.js} +7 -7
- package/dist/kimi-VLSUFLYV.js.map +1 -0
- package/dist/{kiro-UWUCRKR3.js → kiro-AQFXGOJX.js} +10 -8
- package/dist/kiro-AQFXGOJX.js.map +1 -0
- package/dist/{kiro-NYRRBIKW.js → kiro-WGOI5CGT.js} +4 -4
- package/dist/kiro-WGOI5CGT.js.map +1 -0
- package/dist/{leaderboard-NJWKGMXP.js → leaderboard-YBD3IKC2.js} +7 -7
- package/dist/leaderboard-YBD3IKC2.js.map +1 -0
- package/dist/{mimo-code-BPNYYIVM.js → mimo-code-KETL7W7D.js} +7 -7
- package/dist/mimo-code-KETL7W7D.js.map +1 -0
- package/dist/{mistral-vibe-LT3DAJ3L.js → mistral-vibe-3AU7N2CA.js} +6 -6
- package/dist/mistral-vibe-3AU7N2CA.js.map +1 -0
- package/dist/{mux-AHNUAY5Z.js → mux-BO2L5CTW.js} +6 -6
- package/dist/mux-BO2L5CTW.js.map +1 -0
- package/dist/{mux-7PIJ7HUQ.js → mux-XM6AP35L.js} +4 -4
- package/dist/mux-XM6AP35L.js.map +1 -0
- package/dist/{nemoclaw-RE5RZASZ.js → nemoclaw-WA7RSKF2.js} +7 -7
- package/dist/nemoclaw-WA7RSKF2.js.map +1 -0
- package/dist/{omp-D2EXEPH7.js → omp-UDYJ2PYY.js} +9 -7
- package/dist/omp-UDYJ2PYY.js.map +1 -0
- package/dist/{open-interpreter-SSGBEUKX.js → open-interpreter-WMBNM4MQ.js} +6 -6
- package/dist/open-interpreter-WMBNM4MQ.js.map +1 -0
- package/dist/openclaw-3DHVHX5P.js +17 -0
- package/dist/{openclaw-KAFTNLR2.js → openclaw-5EQFGOWL.js} +4 -4
- package/dist/openclaw-5EQFGOWL.js.map +1 -0
- package/dist/{opencode-YKUKZY3J.js → opencode-IKMI3RI7.js} +7 -7
- package/dist/opencode-IKMI3RI7.js.map +1 -0
- package/dist/{opencode-Z65JYILL.js → opencode-QGP4ALQY.js} +4 -4
- package/dist/opencode-QGP4ALQY.js.map +1 -0
- package/dist/{openhands-SEZMCUC3.js → openhands-QGXMQR6I.js} +7 -7
- package/dist/openhands-QGXMQR6I.js.map +1 -0
- package/dist/{package-JECVQEFT.js → package-TAUTPP7Z.js} +15 -15
- package/dist/package-TAUTPP7Z.js.map +1 -0
- package/dist/{pi-YPYV2ULH.js → pi-A2HCGRJG.js} +9 -7
- package/dist/pi-A2HCGRJG.js.map +1 -0
- package/dist/{pi-DLKCPOJE.js → pi-GGDQREVG.js} +4 -4
- package/dist/pi-GGDQREVG.js.map +1 -0
- package/dist/{qwen-code-ZHCLCYMC.js → qwen-code-DNQTGJOF.js} +35 -17
- package/dist/qwen-code-DNQTGJOF.js.map +1 -0
- package/dist/{qwen-code-26CSFDX6.js → qwen-code-GQILBJ5B.js} +4 -4
- package/dist/qwen-code-GQILBJ5B.js.map +1 -0
- package/dist/{roo-code-HSAE2CJU.js → roo-code-SUWI4BBN.js} +4 -4
- package/dist/roo-code-SUWI4BBN.js.map +1 -0
- package/dist/{roo-code-7F4HYB34.js → roo-code-VI2ZKTZC.js} +6 -6
- package/dist/roo-code-VI2ZKTZC.js.map +1 -0
- package/dist/runtime/index.d.ts +9 -8
- package/dist/runtime/index.js +15 -14
- package/dist/sdk/index.d.ts +4 -4
- package/dist/sdk/index.js +6 -6
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/test.d.ts +2 -2
- package/dist/sdk/test.js +12 -11
- package/dist/{serve-LYSBH43G.js → serve-6MDLRQS2.js} +17 -16
- package/dist/serve-6MDLRQS2.js.map +1 -0
- package/dist/{status-PHKKGEW5.js → status-IUP75XHP.js} +11 -11
- package/dist/status-IUP75XHP.js.map +1 -0
- package/dist/{statusline-ZDD4OZ4F.js → statusline-PRODZV2J.js} +16 -15
- package/dist/statusline-PRODZV2J.js.map +1 -0
- package/dist/{synthetic-LUT6V6SD.js → synthetic-6FRA7P5Q.js} +4 -4
- package/dist/synthetic-6FRA7P5Q.js.map +1 -0
- package/dist/{telemetry-HDGMI2BK.js → telemetry-X4PLZEDB.js} +10 -10
- package/dist/telemetry-X4PLZEDB.js.map +1 -0
- package/dist/{trae-SIQI3C66.js → trae-CPPYASR2.js} +4 -4
- package/dist/trae-CPPYASR2.js.map +1 -0
- package/dist/{trae-Y67HSDOW.js → trae-VOJXUYRN.js} +6 -6
- package/dist/trae-VOJXUYRN.js.map +1 -0
- package/dist/{types-JyhpJHJa.d.ts → types-DWXnnx-N.d.ts} +96 -22
- package/dist/{uninstall-RWO2GM2I.js → uninstall-BSIJ4QXN.js} +21 -21
- package/dist/uninstall-BSIJ4QXN.js.map +1 -0
- package/dist/{upgrade-YJCXNUEE.js → upgrade-ZSICU3NE.js} +21 -21
- package/dist/upgrade-ZSICU3NE.js.map +1 -0
- package/dist/{usage-IWO4RQN6.js → usage-EQUNVSCT.js} +4 -4
- package/dist/usage-EQUNVSCT.js.map +1 -0
- package/dist/{usage-event-AFFKCLHE.js → usage-event-J5CQ6LNT.js} +16 -15
- package/dist/usage-event-J5CQ6LNT.js.map +1 -0
- package/dist/{vscode-copilot-5CI5B3X6.js → vscode-copilot-ZBXRPKGG.js} +7 -7
- package/dist/vscode-copilot-ZBXRPKGG.js.map +1 -0
- package/dist/{warp-5YBSP7FP.js → warp-NRBBJQR2.js} +4 -4
- package/dist/warp-NRBBJQR2.js.map +1 -0
- package/dist/{warp-INAKVGBV.js → warp-S444B2S3.js} +9 -7
- package/dist/warp-S444B2S3.js.map +1 -0
- package/dist/{windsurf-4HM57QAY.js → windsurf-R6D2JUTN.js} +6 -6
- package/dist/windsurf-R6D2JUTN.js.map +1 -0
- package/dist/{zed-YN5X37PQ.js → zed-DQV3ZUCU.js} +10 -8
- package/dist/zed-DQV3ZUCU.js.map +1 -0
- package/dist/{zed-XXCNYOIN.js → zed-XJFOOKCO.js} +4 -4
- package/dist/zed-XJFOOKCO.js.map +1 -0
- package/package.json +88 -88
- package/dist/action-IPFDPOZU.js.map +0 -1
- package/dist/amazon-q-IT44LEWV.js.map +0 -1
- package/dist/amp-CEWOSWQO.js.map +0 -1
- package/dist/amp-P35FRU2Z.js.map +0 -1
- package/dist/antigravity-LHFEFNMC.js.map +0 -1
- package/dist/antigravity-TWIWXTQH.js +0 -18
- package/dist/antigravity-cli-MOKFSWOU.js.map +0 -1
- package/dist/antigravity-cli-MPE27LPI.js.map +0 -1
- package/dist/chunk-3EP5V2F4.js.map +0 -1
- package/dist/chunk-3FINQ2ZF.js.map +0 -1
- package/dist/chunk-4GH2KA7V.js.map +0 -1
- package/dist/chunk-7C2IPIIB.js.map +0 -1
- package/dist/chunk-7HHW23AC.js.map +0 -1
- package/dist/chunk-7PSNXUFK.js.map +0 -1
- package/dist/chunk-7ZFK2WEO.js.map +0 -1
- package/dist/chunk-C63AAXK7.js.map +0 -1
- package/dist/chunk-C747DYNT.js.map +0 -1
- package/dist/chunk-FHYR5Q5T.js.map +0 -1
- package/dist/chunk-GCPEPUD7.js.map +0 -1
- package/dist/chunk-GZZCHJDP.js.map +0 -1
- package/dist/chunk-ILWHLGIC.js.map +0 -1
- package/dist/chunk-IOJDZJ72.js.map +0 -1
- package/dist/chunk-KFPSJRHR.js.map +0 -1
- package/dist/chunk-KM2YXO6V.js.map +0 -1
- package/dist/chunk-L2C5UMKW.js.map +0 -1
- package/dist/chunk-LFWYYWLV.js.map +0 -1
- package/dist/chunk-LQ55E7QI.js.map +0 -1
- package/dist/chunk-MLU7NDAZ.js.map +0 -1
- package/dist/chunk-N4ENLZEU.js.map +0 -1
- package/dist/chunk-NDMNMAKP.js.map +0 -1
- package/dist/chunk-PSGTHJQU.js.map +0 -1
- package/dist/chunk-Q4ISQHUQ.js.map +0 -1
- package/dist/chunk-Q7PR2WGT.js.map +0 -1
- package/dist/chunk-QYOUQCC6.js.map +0 -1
- package/dist/chunk-SLJI5BOM.js.map +0 -1
- package/dist/chunk-TGZFDXNH.js.map +0 -1
- package/dist/chunk-THI4PI4H.js.map +0 -1
- package/dist/chunk-UZZTWWGG.js.map +0 -1
- package/dist/chunk-WPTNMYYM.js.map +0 -1
- package/dist/chunk-XN7S44U4.js.map +0 -1
- package/dist/chunk-XVEJAFKW.js.map +0 -1
- package/dist/chunk-XXLCMSD3.js.map +0 -1
- package/dist/chunk-XZWMJLIT.js.map +0 -1
- package/dist/chunk-Y3K6KJMS.js.map +0 -1
- package/dist/chunk-ZSOS2KOR.js.map +0 -1
- package/dist/claude-code-FS42PXZO.js.map +0 -1
- package/dist/claude-code-QBWDT5RT.js.map +0 -1
- package/dist/cline-JKEAYWVR.js.map +0 -1
- package/dist/codebuddy-EB6EYPC3.js.map +0 -1
- package/dist/codebuff-5RSXFU6G.js.map +0 -1
- package/dist/codebuff-6KWBW6CT.js.map +0 -1
- package/dist/codex-ART46CDD.js.map +0 -1
- package/dist/codex-DKBAAUCO.js.map +0 -1
- package/dist/continue-JHZCMSPH.js.map +0 -1
- package/dist/copilot-cli-CLBJACQ5.js.map +0 -1
- package/dist/copilot-cli-QVAU5CFM.js.map +0 -1
- package/dist/crush-7TORBYUL.js.map +0 -1
- package/dist/crush-A5C5USRL.js.map +0 -1
- package/dist/cursor-D5SWDVSW.js.map +0 -1
- package/dist/cursor-W7I4QUFO.js +0 -22
- package/dist/detect-JHSA3G3U.js.map +0 -1
- package/dist/devin-SSR7S4WX.js.map +0 -1
- package/dist/doctor-CQJ7TZPA.js.map +0 -1
- package/dist/droid-45PJ7JGF.js.map +0 -1
- package/dist/droid-PFQ2PZSF.js.map +0 -1
- package/dist/gemini-cli-24PFDK7D.js.map +0 -1
- package/dist/gemini-cli-Y6RJMU24.js.map +0 -1
- package/dist/goose-K2HKUTBK.js.map +0 -1
- package/dist/goose-U5D3LCPC.js.map +0 -1
- package/dist/grok-cli-L2MHAPBY.js.map +0 -1
- package/dist/hermes-2S5JUTTJ.js.map +0 -1
- package/dist/hermes-MF3NX4ZY.js.map +0 -1
- package/dist/hook-VHICXHXL.js.map +0 -1
- package/dist/install-WLENR4KF.js.map +0 -1
- package/dist/jetbrains-copilot-Z4FC3RM3.js.map +0 -1
- package/dist/junie-ULGJJIHB.js.map +0 -1
- package/dist/kilo-SMHPOHJ5.js.map +0 -1
- package/dist/kilo-WWIJOVWD.js.map +0 -1
- package/dist/kilo-cli-QKE2EQLS.js.map +0 -1
- package/dist/kilo-cli-T2SRRRJS.js.map +0 -1
- package/dist/kimi-JMUFTVSF.js.map +0 -1
- package/dist/kimi-MSIDEMCI.js.map +0 -1
- package/dist/kiro-NYRRBIKW.js.map +0 -1
- package/dist/kiro-UWUCRKR3.js.map +0 -1
- package/dist/leaderboard-NJWKGMXP.js.map +0 -1
- package/dist/mimo-code-BPNYYIVM.js.map +0 -1
- package/dist/mistral-vibe-LT3DAJ3L.js.map +0 -1
- package/dist/mux-7PIJ7HUQ.js.map +0 -1
- package/dist/mux-AHNUAY5Z.js.map +0 -1
- package/dist/nemoclaw-RE5RZASZ.js.map +0 -1
- package/dist/omp-D2EXEPH7.js.map +0 -1
- package/dist/open-interpreter-SSGBEUKX.js.map +0 -1
- package/dist/openclaw-IJ33K45M.js +0 -17
- package/dist/openclaw-KAFTNLR2.js.map +0 -1
- package/dist/opencode-YKUKZY3J.js.map +0 -1
- package/dist/opencode-Z65JYILL.js.map +0 -1
- package/dist/openhands-SEZMCUC3.js.map +0 -1
- package/dist/package-JECVQEFT.js.map +0 -1
- package/dist/pi-DLKCPOJE.js.map +0 -1
- package/dist/pi-YPYV2ULH.js.map +0 -1
- package/dist/qwen-code-26CSFDX6.js.map +0 -1
- package/dist/qwen-code-ZHCLCYMC.js.map +0 -1
- package/dist/roo-code-7F4HYB34.js.map +0 -1
- package/dist/roo-code-HSAE2CJU.js.map +0 -1
- package/dist/serve-LYSBH43G.js.map +0 -1
- package/dist/status-PHKKGEW5.js.map +0 -1
- package/dist/statusline-ZDD4OZ4F.js.map +0 -1
- package/dist/synthetic-LUT6V6SD.js.map +0 -1
- package/dist/telemetry-HDGMI2BK.js.map +0 -1
- package/dist/trae-SIQI3C66.js.map +0 -1
- package/dist/trae-Y67HSDOW.js.map +0 -1
- package/dist/uninstall-RWO2GM2I.js.map +0 -1
- package/dist/upgrade-YJCXNUEE.js.map +0 -1
- package/dist/usage-IWO4RQN6.js.map +0 -1
- package/dist/usage-event-AFFKCLHE.js.map +0 -1
- package/dist/vscode-copilot-5CI5B3X6.js.map +0 -1
- package/dist/warp-5YBSP7FP.js.map +0 -1
- package/dist/warp-INAKVGBV.js.map +0 -1
- package/dist/windsurf-4HM57QAY.js.map +0 -1
- package/dist/zed-XXCNYOIN.js.map +0 -1
- package/dist/zed-YN5X37PQ.js.map +0 -1
- /package/dist/{antigravity-TWIWXTQH.js.map → antigravity-VXHL6ZPM.js.map} +0 -0
- /package/dist/{audit-3E5JAOP2.js.map → audit-BO7JQYIH.js.map} +0 -0
- /package/dist/{cursor-W7I4QUFO.js.map → cursor-RSLHA2CY.js.map} +0 -0
- /package/dist/{openclaw-IJ33K45M.js.map → openclaw-3DHVHX5P.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime/telemetry-accessor.ts","../src/telemetry/measure.ts","../src/runtime/hook-entrypoint.ts"],"sourcesContent":["/**\r\n * runtime/telemetry-accessor — build the handler-facing {@link TelemetryAccessor}.\r\n *\r\n * Every runtime entrypoint (hook / statusline / action) stamps `ctx.telemetry`\r\n * (or `evt.telemetry`) with the accessor built here so a connector handler can\r\n * read ITS OWN recorded usage on demand (`await ctx.telemetry?.()`), without the\r\n * handler reaching into the telemetry store directly.\r\n *\r\n * The accessor is LAZY (it opens the store only when called) and DEFENSIVE:\r\n * - AGENT_CONNECTOR_TELEMETRY=0 → resolves zeros without touching disk;\r\n * - any read error → resolves zeros (NEVER throws into a handler — the hook\r\n * runtime is fail-open, the statusline fail-safe, and even the action\r\n * surface must not have a usage read crash an otherwise-fine run).\r\n *\r\n * COST: each accessor call, or the first read of a lazy `ctx.usage`, reads the\r\n * connector's telemetry file in full (the NDJSON store has no since/limit bound)\r\n * and linear-scans it. Cheap for normal stores, but a handler on a HOT path\r\n * should call it only when it needs usage.\r\n */\r\n\r\nimport type { TelemetryAccessor, TelemetryUsageSummary } from \"../core/types.js\";\r\nimport { openStore } from \"../telemetry/store.js\";\r\n\r\n/** The empty summary returned when telemetry is off or a read fails. */\r\nconst ZERO: TelemetryUsageSummary = {\r\n inputTokens: 0,\r\n outputTokens: 0,\r\n totalTokens: 0,\r\n calls: 0,\r\n};\r\n\r\n/**\r\n * Synchronously sum every stored row's inputTokens/outputTokens for the\r\n * connector and count rows (`calls`). Honors the AGENT_CONNECTOR_TELEMETRY=0\r\n * kill switch (zeros, no disk touch) and swallows any error (zeros).\r\n */\r\nexport function readTelemetryUsageSummary(connectorId: string): TelemetryUsageSummary {\r\n if (process.env.AGENT_CONNECTOR_TELEMETRY === \"0\") return { ...ZERO };\r\n try {\r\n const store = openStore({});\r\n try {\r\n let inputTokens = 0;\r\n let outputTokens = 0;\r\n let calls = 0;\r\n // COERCE each addend to a finite number: query() tolerates malformed\r\n // LINES but does not validate field shapes, so a legacy/hand-edited row\r\n // with a missing (→ NaN) or string (→ concat) token field must not\r\n // poison the summary's numeric contract. A non-finite token reads as 0.\r\n for (const row of store.query({ connectorId })) {\r\n inputTokens += Number.isFinite(row.inputTokens) ? row.inputTokens : 0;\r\n outputTokens += Number.isFinite(row.outputTokens) ? row.outputTokens : 0;\r\n calls += 1;\r\n }\r\n return {\r\n inputTokens,\r\n outputTokens,\r\n totalTokens: inputTokens + outputTokens,\r\n calls,\r\n };\r\n } finally {\r\n store.close();\r\n }\r\n } catch {\r\n // Never throw into a handler — a usage read failure resolves to zeros.\r\n return { ...ZERO };\r\n }\r\n}\r\n\r\n/**\r\n * Build a {@link TelemetryAccessor} bound to `connectorId`. The returned async\r\n * function uses {@link readTelemetryUsageSummary}; it never throws.\r\n */\r\nexport function buildTelemetryAccessor(connectorId: string): TelemetryAccessor {\r\n return async (): Promise<TelemetryUsageSummary> => {\r\n return readTelemetryUsageSummary(connectorId);\r\n };\r\n}\r\n\r\n/**\r\n * Install a cached lazy `usage` property on a statusline context. This preserves\r\n * synchronous `ctx.usage` reads for render functions without forcing every\r\n * status refresh to scan the telemetry store when the render never uses usage.\r\n */\r\nexport function attachLazyTelemetryUsage(\r\n ctx: { usage?: TelemetryUsageSummary },\r\n connectorId: string,\r\n read: (connectorId: string) => TelemetryUsageSummary = readTelemetryUsageSummary,\r\n): void {\r\n let cached: TelemetryUsageSummary | undefined;\r\n let hasCached = false;\r\n\r\n Object.defineProperty(ctx, \"usage\", {\r\n configurable: true,\r\n enumerable: true,\r\n get() {\r\n if (!hasCached) {\r\n cached = read(connectorId);\r\n hasCached = true;\r\n }\r\n return cached;\r\n },\r\n set(value: TelemetryUsageSummary | undefined) {\r\n cached = value;\r\n hasCached = true;\r\n },\r\n });\r\n}\r\n","/**\r\n * telemetry/measure — pure measurement helpers over a single MCP tool call.\r\n *\r\n * The framework tokenizes its OWN bytes: the tool-call arguments going in, the\r\n * tool result coming out, and the fixed tool-definition schemas. These helpers\r\n * turn those into {@link TokenCount}s using the default {@link Tokenizer}.\r\n *\r\n * Honesty rules:\r\n * • Binary modalities (image/audio/resource blobs) are NOT tokenized — base64\r\n * would massively over-count. Each gets a flat per-modality estimate, and\r\n * because that estimate is itself an approximation the combined confidence\r\n * is pulled down to at least \"tokenizer-approx\".\r\n * • When combining several counts, the reported confidence is the WORST\r\n * (least-confident) of the inputs — never report \"exact\" for a mixed bag.\r\n *\r\n * Everything here is pure: no IO, no mutation of inputs.\r\n */\r\n\r\nimport { getTokenizer } from \"./tokenizer.js\";\r\nimport { worstConfidence } from \"./types.js\";\r\nimport type {\r\n ConfidenceSource,\r\n ModelFamily,\r\n TokenCount,\r\n Tokenizer,\r\n} from \"./types.js\";\r\n\r\n/** Per-call measurement: tokens in, tokens out, and combined confidence. */\r\nexport interface ToolCallMeasurement {\r\n inputTokens: number;\r\n outputTokens: number;\r\n source: ConfidenceSource;\r\n}\r\n\r\n/**\r\n * Flat token estimates for non-text content blocks. These mirror common host\r\n * accounting (an image tile is roughly ~85 tokens at low detail); they are\r\n * deliberately coarse — we never tokenize the underlying base64 payload.\r\n */\r\nconst MODALITY_ESTIMATE: Record<string, number> = {\r\n image: 85,\r\n audio: 85,\r\n resource: 85,\r\n};\r\n/** Fallback estimate for an unknown non-text block type. */\r\nconst UNKNOWN_BLOCK_ESTIMATE = 85;\r\n\r\n// Confidence ordering + the worst-of comparison live in ./types (the single\r\n// source of truth, imported as `worstConfidence` above). Re-exported here so\r\n// callers that already import `worstConfidence` from this module keep working\r\n// as a new ConfidenceSource value (e.g. tokenizer-calibrated) slots into the\r\n// shared ordering.\r\nexport { worstConfidence };\r\n\r\n/** Reduce a list of sources to the single worst (least-confident) one. */\r\nfunction combineSources(sources: readonly ConfidenceSource[]): ConfidenceSource {\r\n // Default to the most-confident floor so an empty list combines to the only\r\n // other source it is paired with; in practice callers always pass ≥1 source.\r\n let worst: ConfidenceSource = \"host-native\";\r\n for (const s of sources) worst = worstConfidence(worst, s);\r\n return worst;\r\n}\r\n\r\n// ── Structural shapes we read from a tool result (kept local + narrow) ───────\r\n\r\n/** A text content block: `{ type: \"text\", text: \"...\" }`. */\r\ninterface TextBlock {\r\n type: \"text\";\r\n text?: unknown;\r\n}\r\n/** Any content block; only text blocks expose a `text` field we tokenize. */\r\ninterface ContentBlock {\r\n type?: unknown;\r\n text?: unknown;\r\n}\r\n/** The portion of an MCP tool result we measure. */\r\ninterface ToolResultLike {\r\n content?: unknown;\r\n structuredContent?: unknown;\r\n}\r\n\r\nfunction isObject(v: unknown): v is Record<string, unknown> {\r\n return typeof v === \"object\" && v !== null;\r\n}\r\n\r\nfunction isTextBlock(block: ContentBlock): block is TextBlock {\r\n return block.type === \"text\";\r\n}\r\n\r\n/**\r\n * Measure one tool call.\r\n *\r\n * Input = canonical JSON of `args`.\r\n * Output = tokens over the TEXTUAL parts of `result`:\r\n * • If `result.content` is an array of blocks, text blocks are tokenized via\r\n * their `text` field and non-text blocks add a flat per-modality estimate\r\n * (no base64 tokenization).\r\n * • `result.structuredContent`, when present, is tokenized as canonical JSON.\r\n * • Anything else (string / arbitrary object) is tokenized as canonical JSON.\r\n *\r\n * The reported `source` is the worst of the input and output sources.\r\n */\r\nexport function measureToolCall(\r\n args: unknown,\r\n result: unknown,\r\n family: ModelFamily,\r\n tok: Tokenizer = getTokenizer(),\r\n): ToolCallMeasurement {\r\n const input = tok.countValue(args, family);\r\n const output = measureResult(result, family, tok);\r\n return {\r\n inputTokens: input.tokens,\r\n outputTokens: output.tokens,\r\n source: worstConfidence(input.source, output.source),\r\n };\r\n}\r\n\r\n/** Tokenize the textual + structured parts of a tool result. */\r\nfunction measureResult(\r\n result: unknown,\r\n family: ModelFamily,\r\n tok: Tokenizer,\r\n): TokenCount {\r\n if (!isObject(result)) {\r\n // Plain string or scalar result — count its canonical form directly.\r\n return tok.countValue(result, family);\r\n }\r\n\r\n const res = result as ToolResultLike;\r\n const sources: ConfidenceSource[] = [];\r\n let tokens = 0;\r\n\r\n if (Array.isArray(res.content)) {\r\n for (const raw of res.content) {\r\n if (!isObject(raw)) continue;\r\n const block = raw as ContentBlock;\r\n if (isTextBlock(block)) {\r\n const text = typeof block.text === \"string\" ? block.text : \"\";\r\n const c = tok.count(text, family);\r\n tokens += c.tokens;\r\n sources.push(c.source);\r\n } else {\r\n // image / audio / resource / unknown → flat estimate, no base64 BPE.\r\n const key = typeof block.type === \"string\" ? block.type : \"\";\r\n tokens += MODALITY_ESTIMATE[key] ?? UNKNOWN_BLOCK_ESTIMATE;\r\n // A flat estimate is an approximation, never \"exact\".\r\n sources.push(\"tokenizer-approx\");\r\n }\r\n }\r\n } else if (res.content !== undefined) {\r\n // content present but not an array (e.g. a bare string) → count it whole.\r\n const c = tok.countValue(res.content, family);\r\n tokens += c.tokens;\r\n sources.push(c.source);\r\n }\r\n\r\n if (res.structuredContent !== undefined) {\r\n const c = tok.countValue(res.structuredContent, family);\r\n tokens += c.tokens;\r\n sources.push(c.source);\r\n }\r\n\r\n // Nothing measurable found → count the whole object as a fallback so we never\r\n // silently report zero for a non-empty result.\r\n if (sources.length === 0) {\r\n return tok.countValue(result, family);\r\n }\r\n\r\n return { tokens, source: combineSources(sources) };\r\n}\r\n\r\n/**\r\n * Measure the fixed tool-definition overhead: tokenize the JSON of the entire\r\n * `tools/list` array once. `tools` is the raw array of tool descriptors as the\r\n * server advertises them (shape is host/SDK-specific, so it is treated as\r\n * opaque JSON here).\r\n */\r\nexport function measureToolDefs(\r\n tools: readonly unknown[],\r\n family: ModelFamily,\r\n tok: Tokenizer = getTokenizer(),\r\n): TokenCount {\r\n return tok.countValue(tools, family);\r\n}\r\n\r\n/**\r\n * Measure one RUNTIME hook dispatch (the new `hook` developer-axis surface).\r\n *\r\n * Input = canonical JSON of the inbound normalized event payload the handler\r\n * reads (the host-provided fields: toolInput / prompt / raw / etc.).\r\n * Output = canonical JSON of what the handler RETURNS that becomes context or a\r\n * decision the host consumes (additionalContext / updatedInput / reason\r\n * and the rest of the normalized {@link HookResponse}).\r\n *\r\n * Uses the SAME tokenizer + worst-of confidence combination as the proxy. Pure:\r\n * no IO, no mutation. The two values are tokenized whole (a hook payload has no\r\n * binary content blocks to special-case, unlike a tool result).\r\n */\r\nexport function measureHook(\r\n eventPayload: unknown,\r\n handlerReturn: unknown,\r\n family: ModelFamily,\r\n tok: Tokenizer = getTokenizer(),\r\n): ToolCallMeasurement {\r\n const input = tok.countValue(eventPayload, family);\r\n const output = tok.countValue(handlerReturn, family);\r\n return {\r\n inputTokens: input.tokens,\r\n outputTokens: output.tokens,\r\n source: worstConfidence(input.source, output.source),\r\n };\r\n}\r\n","/**\r\n * runtime/hook-entrypoint — the universal json-stdio hook dispatcher.\r\n *\r\n * Every json-stdio host points its hook config at the single stable home binary\r\n * (`<homeBin> hook <platformId> <event> --connector <id>`, built by\r\n * core/spawn.buildHomeBinHookCommand). The CLI reads stdin and calls\r\n * {@link runHook} with the parsed flags + the raw stdin string. This module:\r\n *\r\n * 1. Loads the registered connector (live handlers) and the host adapter.\r\n * 2. Parses the host's stdin payload into a normalized event via the adapter.\r\n * 3. Finds the connector's handler for the event, honoring the tool matcher.\r\n * 4. Runs the handler, normalizes its return, and formats the adapter's native\r\n * reply.\r\n *\r\n * Fail-open is the safety contract: ANY error degrades to exit 0 (\"allow\") so a\r\n * framework or handler bug can never wedge a host's tool call — EXCEPT when the\r\n * event is PreToolUse or PermissionRequest and the handler explicitly denied,\r\n * in which case the deny (a deliberate security decision) is preserved rather\r\n * than swallowed.\r\n */\r\n\r\nimport type {\r\n HookEventName,\r\n HookResponse,\r\n NativeHookDef,\r\n NativeHookEvent,\r\n PlatformId,\r\n ResolvedConnector,\r\n} from \"../core/types.js\";\r\nimport { log } from \"../core/logger.js\";\r\nimport {\r\n loadRegisteredConnector,\r\n readRegisteredMeta,\r\n} from \"../core/load-connector.js\";\r\nimport { loadAdapter, REGISTERED_PLATFORM_IDS } from \"../adapters/registry.js\";\r\nimport { buildTelemetryAccessor } from \"./telemetry-accessor.js\";\r\nimport type { NormalizedEvent } from \"../adapters/spi.js\";\r\nimport { projectIdentity } from \"../core/paths.js\";\r\nimport { measureHook } from \"../telemetry/measure.js\";\r\nimport { openStore, newRecordId } from \"../telemetry/store.js\";\r\nimport { getTokenizer, inferModelFamily } from \"../telemetry/tokenizer.js\";\r\n\r\n/** Flags + stdin the CLI hands to {@link runHook}. */\r\nexport interface RunHookOptions {\r\n /** Host platform id from the hook command (`hook <platformId> ...`). */\r\n platformId: string;\r\n /** Canonical lifecycle event name from the hook command. */\r\n event: HookEventName;\r\n /** Connector id from `--connector <id>`. */\r\n connectorId: string;\r\n /** Raw stdin payload (host-native JSON). Empty string is tolerated → `{}`. */\r\n stdin: string;\r\n}\r\n\r\n/** Process-level result the CLI translates into exit code + stdout/stderr. */\r\nexport interface RunHookResult {\r\n exitCode: number;\r\n stdout?: string;\r\n stderr?: string;\r\n}\r\n\r\n/** A pass-through \"allow\" reply with exit 0 and no payload. */\r\nconst ALLOW: RunHookResult = { exitCode: 0 };\r\n\r\n/**\r\n * The string a connector matcher filters on, when the event carries one:\r\n * tool events (PreToolUse / PostToolUse / PermissionRequest /\r\n * PostToolUseFailure) expose `toolName`; subagent events expose `agentType`.\r\n * Returns undefined when the event has neither (no filtering) — including a\r\n * SubagentStop arriving WITHOUT agent_type, which some hosts fail to populate;\r\n * filtering out such an event would silently drop a real stop, so we run the\r\n * handler instead (fail-open).\r\n */\r\nexport function eventMatcherSubject(evt: NormalizedEvent): string | undefined {\r\n const name = (evt as { toolName?: unknown }).toolName;\r\n if (typeof name === \"string\") return name;\r\n const agentType = (evt as { agentType?: unknown }).agentType;\r\n if (typeof agentType === \"string\") return agentType;\r\n return undefined;\r\n}\r\n\r\n/**\r\n * Tolerantly parse the host's stdin payload. Empty / whitespace-only input is a\r\n * legitimate \"no payload\" signal from some hosts and resolves to `{}`. Malformed\r\n * JSON also degrades to `{}` rather than throwing — fail-open is the contract.\r\n */\r\nfunction parseStdin(stdin: string): unknown {\r\n const trimmed = stdin.trim();\r\n if (trimmed === \"\") return {};\r\n try {\r\n return JSON.parse(trimmed);\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\n/**\r\n * Compile the connector's matcher (a regex string) for a tool event. A missing\r\n * or empty matcher matches everything (returns null → no filtering). An invalid\r\n * regex degrades to \"match everything\" so a typo never blocks tool calls.\r\n */\r\nexport function compileMatcher(matcher: string | undefined): RegExp | null {\r\n if (!matcher || matcher === \"\") return null;\r\n try {\r\n return new RegExp(matcher);\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Normalize a handler return into a concrete HookResponse. A void/non-object\r\n * return becomes `{}` — NO decision — which every adapter formats as\r\n * pass-through allow (`response.decision ?? \"allow\"`). Deliberately NOT\r\n * `{decision:\"allow\"}`: on PermissionRequest an explicit \"allow\" is an ACTIVE\r\n * grant that suppresses the host's permission dialog, and a handler that\r\n * returns nothing must fall through to the dialog, never silently grant.\r\n */\r\nfunction normalizeResponse(value: HookResponse | void): HookResponse {\r\n if (value == null || typeof value !== \"object\") return {};\r\n return value;\r\n}\r\n\r\n/**\r\n * Resolve the host platform to stamp on a hook telemetry row. Mirrors the serve\r\n * `--host` plumbing: the install-target platform id is baked into the hook\r\n * command (`hook <platformId> <event> …`) so `opts.platformId` is authoritative,\r\n * but an explicit env override (AGENT_CONNECTOR_HOST) wins when it names a known\r\n * platform. Falls back to the event's adapter-stamped hostPlatform, then\r\n * \"unknown\" — never throws.\r\n */\r\nfunction resolveHookHostPlatform(\r\n platformId: string,\r\n evt: NormalizedEvent | NativeHookEvent,\r\n): PlatformId {\r\n const override = process.env.AGENT_CONNECTOR_HOST;\r\n if (\r\n override !== undefined &&\r\n REGISTERED_PLATFORM_IDS.has(override as PlatformId)\r\n ) {\r\n return override as PlatformId;\r\n }\r\n if (REGISTERED_PLATFORM_IDS.has(platformId as PlatformId)) {\r\n return platformId as PlatformId;\r\n }\r\n const fromEvent = (evt as { hostPlatform?: unknown }).hostPlatform;\r\n if (\r\n typeof fromEvent === \"string\" &&\r\n REGISTERED_PLATFORM_IDS.has(fromEvent as PlatformId)\r\n ) {\r\n return fromEvent as PlatformId;\r\n }\r\n return \"unknown\";\r\n}\r\n\r\n/**\r\n * Record one RUNTIME hook dispatch as a `hook` developer-axis telemetry row.\r\n *\r\n * input = the inbound event payload the handler reads (normalized event, or\r\n * the {@link NativeHookEvent} envelope for native passthrough hooks);\r\n * output = what the handler returned (a normalized {@link HookResponse}, or the\r\n * verbatim native reply object). Tokenized with the SAME tokenizer the\r\n * proxy uses. For a native hook `opts.event` is the host-native name.\r\n *\r\n * MUST be fail-open (the hook runtime is fail-open by contract): any error here\r\n * is swallowed so a measurement bug can NEVER break a host's hook. Honors the\r\n * AGENT_CONNECTOR_TELEMETRY=0 kill switch (skips entirely; the store's append is\r\n * already a no-op under it, but we also skip the tokenize work).\r\n */\r\nfunction recordHookTelemetry(\r\n opts: { platformId: string; event: string; connectorId: string },\r\n connector: ResolvedConnector,\r\n evt: NormalizedEvent | NativeHookEvent,\r\n response: unknown,\r\n): void {\r\n if (process.env.AGENT_CONNECTOR_TELEMETRY === \"0\") return;\r\n try {\r\n const family = inferModelFamily(\"\", connector.telemetry.modelFamilyHint);\r\n const measurement = measureHook(evt, response, family, getTokenizer());\r\n\r\n const sessionId =\r\n typeof (evt as { sessionId?: unknown }).sessionId === \"string\"\r\n ? ((evt as { sessionId: string }).sessionId)\r\n : \"\";\r\n const evtProjectDir = (evt as { projectDir?: unknown }).projectDir;\r\n const projectDir =\r\n typeof evtProjectDir === \"string\" && evtProjectDir !== \"\"\r\n ? evtProjectDir\r\n : process.cwd();\r\n const id = projectIdentity(projectDir);\r\n\r\n const store = openStore({});\r\n try {\r\n store.append({\r\n id: newRecordId(0),\r\n ts: Date.now(),\r\n connectorId: opts.connectorId,\r\n toolName: opts.event, // for a hook row the per-item name IS the event\r\n scope: \"hook\",\r\n surfaceKind: \"hook\",\r\n hostPlatform: resolveHookHostPlatform(opts.platformId, evt),\r\n sessionId,\r\n projectKey: id.key,\r\n projectDir: id.dir,\r\n inputTokens: measurement.inputTokens,\r\n outputTokens: measurement.outputTokens,\r\n confidenceSource: measurement.source,\r\n isError: false,\r\n });\r\n } finally {\r\n store.close();\r\n }\r\n } catch {\r\n // Fail-open: a telemetry error must NEVER break the hook runtime.\r\n }\r\n}\r\n\r\n/**\r\n * Dispatch one host hook invocation. Always resolves (never rejects): every\r\n * failure path returns a concrete {@link RunHookResult} so the CLI can exit\r\n * cleanly. See the module header for the fail-open contract.\r\n */\r\nexport async function runHook(opts: RunHookOptions): Promise<RunHookResult> {\r\n const { platformId, event, connectorId, stdin } = opts;\r\n\r\n try {\r\n const connector = await loadRegisteredConnector(connectorId);\r\n const adapter = await loadAdapter(platformId);\r\n\r\n // mcp-only hosts (and any adapter without runtime dispatch) cannot parse a\r\n // hook payload — nothing to do, allow.\r\n if (!adapter || !adapter.parseEvent || !adapter.formatReply) {\r\n return ALLOW;\r\n }\r\n\r\n const raw = parseStdin(stdin);\r\n const evt = adapter.parseEvent(event, raw);\r\n // The hook command carries the authoritative connector id; stamp it on the\r\n // event so handlers see the connector they were dispatched for.\r\n evt.connectorId = connectorId;\r\n // Unify the hook event with HostCtx: the adapter's parseEvent has no\r\n // capabilities/scope/telemetry at parse time (they are runtime-only), so the\r\n // runtime ALWAYS backfills them before the handler runs. capabilities comes\r\n // from the adapter; scope is recovered from the registered metadata (sync);\r\n // telemetry is the lazy per-connector usage accessor. The SAME three\r\n // assignments run on the error path (failOpenOrPreserveDeny) for symmetry.\r\n evt.capabilities = adapter.capabilities;\r\n evt.scope = readRegisteredMeta(connectorId)?.scope;\r\n evt.telemetry = buildTelemetryAccessor(connectorId);\r\n\r\n const definition = connector.hooks[event];\r\n if (!definition || typeof definition.handler !== \"function\") {\r\n // The connector declares no handler for this event — allow.\r\n return ALLOW;\r\n }\r\n\r\n // Per-host handler override: when dispatching for host X, `hosts[X].handler`\r\n // wins over the top-level handler; a host not listed (or a per-host entry\r\n // that is somehow not a function) falls back to the top-level handler.\r\n // Selection NEVER throws — fail-open is preserved.\r\n // NOTE: keyed on the RAW install-baked platformId (the host this entrypoint\r\n // was registered for), NOT resolveHookHostPlatform's AGENT_CONNECTOR_HOST\r\n // override — that override only re-labels the telemetry row. The error path\r\n // (failOpenOrPreserveDeny) resolves the IDENTICAL handler the same way, so a\r\n // per-host deny is preserved consistently.\r\n const perHost = definition.hosts?.[platformId as PlatformId]?.handler;\r\n const handler = typeof perHost === \"function\" ? perHost : definition.handler;\r\n\r\n // Matcher: if set and the event's subject (tool name, or agent type for\r\n // Subagent* events) does not match, allow (the hook is simply not\r\n // interested in this tool/agent). The matcher is the definition's — a\r\n // per-host handler shares the top-level matcher.\r\n const subject = eventMatcherSubject(evt);\r\n if (subject !== undefined) {\r\n const re = compileMatcher(definition.matcher);\r\n if (re && !re.test(subject)) return ALLOW;\r\n }\r\n\r\n // Run the resolved handler. Its return is normalized (void → allow) and\r\n // handed to the adapter to render the host-native reply.\r\n const handlerResult = await handler(\r\n // The adapter parsed `evt` as exactly this event's payload type; the\r\n // HooksConfig handler for the same event expects that payload.\r\n evt as never,\r\n );\r\n const response = normalizeResponse(handlerResult);\r\n\r\n // Developer-axis telemetry for the RUNTIME `hook` surface. Fail-open: never\r\n // lets a measurement error break the hook (handled inside the helper).\r\n recordHookTelemetry(opts, connector, evt, response);\r\n\r\n const reply = adapter.formatReply(event, response);\r\n return {\r\n exitCode: reply.exitCode,\r\n ...(reply.stdout !== undefined ? { stdout: reply.stdout } : {}),\r\n ...(reply.stderr !== undefined ? { stderr: reply.stderr } : {}),\r\n };\r\n } catch (err) {\r\n // Fail-open: a framework/handler error must not wedge the host's tool call.\r\n // The ONE exception is a PreToolUse / PermissionRequest deny — a deliberate\r\n // block we honor even if a later step threw. We re-derive that decision\r\n // defensively from the adapter so a throw AFTER the deny still surfaces it.\r\n const message = err instanceof Error ? err.message : String(err);\r\n log.error(`hook ${platformId}/${event} (${connectorId}) failed:`, message);\r\n return failOpenOrPreserveDeny(opts, message);\r\n }\r\n}\r\n\r\n/**\r\n * Events whose explicit deny survives the fail-open error path. Both are\r\n * permission gates — a deny there is a deliberate security decision that must\r\n * never be silently downgraded to allow. (Stop-class denies are persistence\r\n * conveniences, not security boundaries, so they stay fail-open.)\r\n */\r\nconst DENY_PRESERVE_EVENTS: ReadonlySet<HookEventName> = new Set([\r\n \"PreToolUse\",\r\n \"PermissionRequest\",\r\n]);\r\n\r\n/**\r\n * Error path. Returns fail-open allow, UNLESS this is a deny-preserving event\r\n * (PreToolUse / PermissionRequest) whose handler explicitly denied — in which\r\n * case the deny is reconstructed and preserved (a security decision is never\r\n * silently downgraded to allow).\r\n *\r\n * Best-effort: if anything in the deny reconstruction itself throws, we fall\r\n * back to allow (we never escalate an error into a spurious block).\r\n */\r\nasync function failOpenOrPreserveDeny(\r\n opts: RunHookOptions,\r\n errorMessage: string,\r\n): Promise<RunHookResult> {\r\n if (!DENY_PRESERVE_EVENTS.has(opts.event)) return ALLOW;\r\n try {\r\n const connector = await loadRegisteredConnector(opts.connectorId);\r\n const adapter = await loadAdapter(opts.platformId);\r\n if (!adapter?.parseEvent || !adapter.formatReply) return ALLOW;\r\n\r\n const definition = connector.hooks[opts.event];\r\n if (!definition || typeof definition.handler !== \"function\") return ALLOW;\r\n\r\n // Reconstruct the deny from the SAME handler the main path would have run —\r\n // the per-host override if one is declared for this host, else the top-level\r\n // handler. Otherwise a per-host deny could be silently downgraded on the\r\n // error path.\r\n const perHost = definition.hosts?.[opts.platformId as PlatformId]?.handler;\r\n const handler = typeof perHost === \"function\" ? perHost : definition.handler;\r\n\r\n const raw = parseStdin(opts.stdin);\r\n const evt = adapter.parseEvent(opts.event, raw);\r\n evt.connectorId = opts.connectorId;\r\n // Backfill the HostCtx fields here too (symmetry with the main path), so a\r\n // per-host deny reconstructed on the error path still carries capabilities/\r\n // scope/telemetry — a handler that branches on them denies identically.\r\n evt.capabilities = adapter.capabilities;\r\n evt.scope = readRegisteredMeta(opts.connectorId)?.scope;\r\n evt.telemetry = buildTelemetryAccessor(opts.connectorId);\r\n\r\n const subject = eventMatcherSubject(evt);\r\n if (subject !== undefined) {\r\n const re = compileMatcher(definition.matcher);\r\n if (re && !re.test(subject)) return ALLOW;\r\n }\r\n\r\n const response = normalizeResponse(await handler(evt as never));\r\n if (response.decision !== \"deny\") return ALLOW;\r\n\r\n const reply = adapter.formatReply(opts.event, response);\r\n return {\r\n exitCode: reply.exitCode,\r\n ...(reply.stdout !== undefined ? { stdout: reply.stdout } : {}),\r\n ...(reply.stderr !== undefined\r\n ? { stderr: reply.stderr }\r\n : { stderr: errorMessage }),\r\n };\r\n } catch {\r\n // Reconstruction failed — never escalate an internal error into a block.\r\n return ALLOW;\r\n }\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Native (passthrough) hooks — host events OUTSIDE the normalized union\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** Flags + stdin for {@link runNativeHook}. `event` is HOST-NATIVE (non-union). */\r\nexport interface RunNativeHookOptions {\r\n /** Host platform id from the hook command (`hook <platformId> ...`). */\r\n platformId: string;\r\n /** Host-native event name, verbatim (e.g. \"TaskCreated\"). */\r\n event: string;\r\n /** Connector id from `--connector <id>`. */\r\n connectorId: string;\r\n /** Raw stdin payload (host-native JSON). Empty string is tolerated → `{}`. */\r\n stdin: string;\r\n}\r\n\r\n/** The connector's native hook definition for `platformId`/`event`, if any. */\r\nfunction nativeHookDef(\r\n connector: ResolvedConnector,\r\n platformId: string,\r\n event: string,\r\n): NativeHookDef | undefined {\r\n return connector.platforms[platformId as PlatformId]?.nativeHooks?.[event];\r\n}\r\n\r\n/**\r\n * True when the registered connector declares `event` under\r\n * `platforms[platformId].nativeHooks` with a live handler. Used by the hook CLI\r\n * to decide whether a non-union event name is accepted (declared → dispatch via\r\n * {@link runNativeHook}) or rejected with the strict unknown-event error.\r\n * Fail-safe: any load error reads as \"not declared\".\r\n */\r\nexport async function isNativeHookDeclared(\r\n platformId: string,\r\n event: string,\r\n connectorId: string,\r\n): Promise<boolean> {\r\n try {\r\n const connector = await loadRegisteredConnector(connectorId);\r\n return typeof nativeHookDef(connector, platformId, event)?.handler === \"function\";\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Dispatch one NATIVE (passthrough) hook invocation. No normalized parse, no\r\n * HookResponse mapping, no runtime matcher evaluation (the host's own matcher\r\n * already filtered — the def's matcher string was written verbatim into the\r\n * host config at install):\r\n *\r\n * stdin JSON → NativeHookEvent{raw} → handler → VERBATIM JSON stdout (exit 0).\r\n *\r\n * void/undefined return → exit 0 with no output. Fail-open: ANY throw degrades\r\n * to exit 0 with no output — exit-2 blocking semantics are not modeled in v1.\r\n * Telemetry: records the same scope:\"hook\" developer-axis row as normalized\r\n * hooks, with the NATIVE event name as the per-item name.\r\n */\r\nexport async function runNativeHook(\r\n opts: RunNativeHookOptions,\r\n): Promise<RunHookResult> {\r\n const { platformId, event, connectorId, stdin } = opts;\r\n try {\r\n const connector = await loadRegisteredConnector(connectorId);\r\n const def = nativeHookDef(connector, platformId, event);\r\n if (!def || typeof def.handler !== \"function\") {\r\n // Not declared for this platform — nothing to do, allow.\r\n return ALLOW;\r\n }\r\n\r\n const raw = parseStdin(stdin);\r\n // Best-effort session/project extraction from the json-stdio common shape\r\n // (Claude Code: session_id / cwd). Everything else stays in `raw` untouched.\r\n const sessionId =\r\n typeof (raw as { session_id?: unknown })?.session_id === \"string\"\r\n ? (raw as { session_id: string }).session_id\r\n : \"\";\r\n const cwd = (raw as { cwd?: unknown })?.cwd;\r\n const evt: NativeHookEvent = {\r\n event,\r\n hostPlatform: REGISTERED_PLATFORM_IDS.has(platformId as PlatformId)\r\n ? (platformId as PlatformId)\r\n : \"unknown\",\r\n sessionId,\r\n ...(typeof cwd === \"string\" && cwd !== \"\" ? { projectDir: cwd } : {}),\r\n raw,\r\n };\r\n\r\n const result = await def.handler(evt);\r\n\r\n // Same scope:\"hook\" telemetry row as normalized hooks (fail-open inside).\r\n recordHookTelemetry(opts, connector, evt, result === undefined ? {} : result);\r\n\r\n if (result === undefined) return ALLOW; // void → exit 0, no output\r\n const json = JSON.stringify(result);\r\n // Non-JSON-serializable returns (functions/symbols) degrade to silence.\r\n if (json === undefined) return ALLOW;\r\n return { exitCode: 0, stdout: json };\r\n } catch (err) {\r\n // Fail-open, unconditionally: native hooks have no deny-preserve carve-out\r\n // (there is no normalized decision to reconstruct).\r\n const message = err instanceof Error ? err.message : String(err);\r\n log.error(\r\n `native hook ${platformId}/${event} (${connectorId}) failed:`,\r\n message,\r\n );\r\n return ALLOW;\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/detect.ts"],"sourcesContent":["/**\r\n * adapters/detect — two complementary detection answers.\r\n *\r\n * 1. detectInstalledPlatforms(projectDir): which hosts are INSTALLED on this\r\n * machine (probe each adapter's config dirs / marker files). Drives the\r\n * install planner (\"auto\" target expansion, `doctor`).\r\n *\r\n * 2. detectRuntimeHost(env): which host is EXECUTING right now (read env\r\n * markers the host runtime injects into its child processes). Drives the\r\n * universal hook entrypoint — it must know which adapter's wire format the\r\n * stdin payload is in before it can parse it.\r\n *\r\n * Both derive their platform set from `ADAPTER_REGISTRY` (the single source of\r\n * truth in registry.ts), generalizing context-mode's detection cascade\r\n * (env-var tier → config-dir tier → low-confidence fallback) to our\r\n * Adapter/AdapterFactory contract. The config-dir tier is delegated to each\r\n * adapter's `detectInstalled()` rather than duplicated here, so adding a\r\n * platform never requires editing this file.\r\n */\r\n\r\nimport type { DetectedPlatform, DetectionSignal, PlatformId } from \"../core/types.js\";\r\nimport { ADAPTER_REGISTRY, loadAdapter } from \"./registry.js\";\r\n\r\n/**\r\n * Detect every platform installed on this machine.\r\n *\r\n * Loads each registered adapter and calls its `detectInstalled(projectDir)`\r\n * (which probes the platform's native config dirs + marker files), returning\r\n * only those reported `installed === true`, in registry order. Adapters never\r\n * throw from detection, but we guard each call so one misbehaving adapter\r\n * cannot abort the whole probe.\r\n *\r\n * @param projectDir Resolved project root — lets project-scoped adapters look\r\n * for `<projectDir>/.mcp.json`, `<projectDir>/.cursor`, etc.\r\n */\r\nexport async function detectInstalledPlatforms(\r\n projectDir: string,\r\n): Promise<DetectedPlatform[]> {\r\n const adapters = await Promise.all(\r\n ADAPTER_REGISTRY.map(async (factory) => {\r\n try {\r\n return await factory.load();\r\n } catch {\r\n return undefined;\r\n }\r\n }),\r\n );\r\n\r\n const detected: DetectedPlatform[] = [];\r\n for (const adapter of adapters) {\r\n if (!adapter) continue;\r\n try {\r\n const result = adapter.detectInstalled(projectDir);\r\n if (result.installed) detected.push(result);\r\n } catch {\r\n // A single adapter's detection failure must not sink the whole probe.\r\n }\r\n }\r\n return detected;\r\n}\r\n\r\n/**\r\n * One env-marker rule for runtime-host detection. `vars` is checked in order;\r\n * the FIRST rule with any non-empty matching env var wins, so the table order\r\n * is the detection precedence (forks before parents). Each var is documented at\r\n * the rule so the source of the marker is auditable.\r\n */\r\ninterface RuntimeSignalRule {\r\n readonly platform: PlatformId;\r\n /** Env var names that prove \"this host is executing me\". */\r\n readonly vars: readonly string[];\r\n /** Human-readable note (printed in the DetectionSignal.reason). */\r\n readonly note: string;\r\n}\r\n\r\n/**\r\n * Ordered env-marker table. Forks BEFORE parents (cursor — a VS Code fork —\r\n * precedes any future vscode-copilot entry so a Cursor session inside a\r\n * VS Code-derived shell is not misclassified). Markers are verified against\r\n * each host's source:\r\n *\r\n * - codebuddy: CODEBUDDY_SESSION_ID / CODEBUDDY_PROJECT_DIR / CODEBUDDY_PLUGIN_ROOT\r\n * are CodeBuddy-exclusive (verified in @tencent-ai/codebuddy-code@2.109.0).\r\n * CodeBuddy is a Claude Code fork that ALSO exports CLAUDE_SESSION_ID\r\n * (its bundle sets both), so it MUST precede claude-code or a CodeBuddy\r\n * session would be misclassified as Claude Code (fork-before-parent).\r\n * - claude-code: CLAUDE_CODE_ENTRYPOINT is set on every Claude Code session;\r\n * CLAUDE_PLUGIN_ROOT is set when a plugin is loaded. Both are CC-exclusive\r\n * and serve as the disambiguators when CC runs inside another IDE's shell.\r\n * - codex: CODEX_THREAD_ID is set per exec (codex-rs/core exec_env);\r\n * CODEX_CI is set in CI mode (unified_exec process_manager).\r\n * - cursor: CURSOR_TRACE_ID is the widely-observed Cursor marker (Cursor\r\n * CLI / agent); CURSOR_CLI marks the Cursor terminal; CURSOR_CWD is the\r\n * documented workspace var.\r\n */\r\nconst RUNTIME_SIGNALS: readonly RuntimeSignalRule[] = [\r\n {\r\n platform: \"codebuddy\",\r\n vars: [\"CODEBUDDY_SESSION_ID\", \"CODEBUDDY_PROJECT_DIR\", \"CODEBUDDY_PLUGIN_ROOT\"],\r\n note: \"CodeBuddy env marker set\",\r\n },\r\n {\r\n platform: \"claude-code\",\r\n vars: [\"CLAUDE_CODE_ENTRYPOINT\", \"CLAUDE_PLUGIN_ROOT\", \"CLAUDE_PROJECT_DIR\", \"CLAUDE_SESSION_ID\"],\r\n note: \"Claude Code env marker set\",\r\n },\r\n {\r\n platform: \"cursor\",\r\n vars: [\"CURSOR_TRACE_ID\", \"CURSOR_CLI\", \"CURSOR_CWD\"],\r\n note: \"Cursor env marker set\",\r\n },\r\n {\r\n platform: \"codex\",\r\n vars: [\"CODEX_THREAD_ID\", \"CODEX_CI\"],\r\n note: \"Codex env marker set\",\r\n },\r\n];\r\n\r\n/**\r\n * Identify which host runtime is executing the current process from environment\r\n * markers. High confidence on an env-marker hit; otherwise an explicit\r\n * `AGENT_CONNECTOR_PLATFORM` override (high confidence, validated against the\r\n * registry) is honored; failing both, returns `unknown` at low confidence so\r\n * the caller can decide on a safe default.\r\n *\r\n * Pure with respect to its `env` argument (defaults to `process.env`) so it is\r\n * trivially testable — pass a synthetic env to assert each rule.\r\n */\r\nexport function detectRuntimeHost(env: NodeJS.ProcessEnv = process.env): DetectionSignal {\r\n // ── Explicit override (highest priority, validated against the registry) ──\r\n const override = env.AGENT_CONNECTOR_PLATFORM;\r\n if (override && override.trim() !== \"\") {\r\n if (REGISTERED_RUNTIME_IDS.has(override as PlatformId)) {\r\n return {\r\n platform: override as PlatformId,\r\n confidence: \"high\",\r\n reason: `AGENT_CONNECTOR_PLATFORM=${override} override`,\r\n };\r\n }\r\n }\r\n\r\n // ── Env-marker tier (forks before parents; first hit wins) ──────────────\r\n for (const rule of RUNTIME_SIGNALS) {\r\n const hit = rule.vars.find((name) => {\r\n const value = env[name];\r\n return typeof value === \"string\" && value !== \"\";\r\n });\r\n if (hit) {\r\n return {\r\n platform: rule.platform,\r\n confidence: \"high\",\r\n reason: `${rule.note} (${hit})`,\r\n };\r\n }\r\n }\r\n\r\n // ── Fallback: no host marker present ────────────────────────────────────\r\n return {\r\n platform: \"unknown\",\r\n confidence: \"low\",\r\n reason: \"no host runtime env marker detected\",\r\n };\r\n}\r\n\r\n/** Platform ids the runtime-signal table knows how to detect. */\r\nconst REGISTERED_RUNTIME_IDS: ReadonlySet<PlatformId> = new Set(\r\n RUNTIME_SIGNALS.map((rule) => rule.platform),\r\n);\r\n\r\n// Re-export for callers that resolve an adapter straight from a detection\r\n// result without reaching into registry.ts.\r\nexport { loadAdapter };\r\n"],"mappings":";;;;;;AAmCA,eAAsB,yBACpB,YAC6B;AAC7B,QAAM,WAAW,MAAM,QAAQ;AAAA,IAC7B,iBAAiB,IAAI,OAAO,YAAY;AACtC,UAAI;AACF,eAAO,MAAM,QAAQ,KAAK;AAAA,MAC5B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,WAA+B,CAAC;AACtC,aAAW,WAAW,UAAU;AAC9B,QAAI,CAAC,QAAS;AACd,QAAI;AACF,YAAM,SAAS,QAAQ,gBAAgB,UAAU;AACjD,UAAI,OAAO,UAAW,UAAS,KAAK,MAAM;AAAA,IAC5C,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAoCA,IAAM,kBAAgD;AAAA,EACpD;AAAA,IACE,UAAU;AAAA,IACV,MAAM,CAAC,wBAAwB,yBAAyB,uBAAuB;AAAA,IAC/E,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,MAAM,CAAC,0BAA0B,sBAAsB,sBAAsB,mBAAmB;AAAA,IAChG,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,MAAM,CAAC,mBAAmB,cAAc,YAAY;AAAA,IACpD,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,MAAM,CAAC,mBAAmB,UAAU;AAAA,IACpC,MAAM;AAAA,EACR;AACF;AAYO,SAAS,kBAAkB,MAAyB,QAAQ,KAAsB;AAEvF,QAAM,WAAW,IAAI;AACrB,MAAI,YAAY,SAAS,KAAK,MAAM,IAAI;AACtC,QAAI,uBAAuB,IAAI,QAAsB,GAAG;AACtD,aAAO;AAAA,QACL,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,QAAQ,4BAA4B,QAAQ;AAAA,MAC9C;AAAA,IACF;AAAA,EACF;AAGA,aAAW,QAAQ,iBAAiB;AAClC,UAAM,MAAM,KAAK,KAAK,KAAK,CAAC,SAAS;AACnC,YAAM,QAAQ,IAAI,IAAI;AACtB,aAAO,OAAO,UAAU,YAAY,UAAU;AAAA,IAChD,CAAC;AACD,QAAI,KAAK;AACP,aAAO;AAAA,QACL,UAAU,KAAK;AAAA,QACf,YAAY;AAAA,QACZ,QAAQ,GAAG,KAAK,IAAI,KAAK,GAAG;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,QAAQ;AAAA,EACV;AACF;AAGA,IAAM,yBAAkD,IAAI;AAAA,EAC1D,gBAAgB,IAAI,CAAC,SAAS,KAAK,QAAQ;AAC7C;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/managed-block.ts"],"sourcesContent":["/**\r\n * core/managed-block — the marker-fenced MANAGED BLOCK engine behind the\r\n * `memory` content surface.\r\n *\r\n * Memory/rules files (AGENTS.md, CLAUDE.md, GEMINI.md, …) are SHARED,\r\n * user-authored files — unlike commands/skills/subagents, agent-connector does\r\n * not own them. Every write therefore goes through this one module, which\r\n * implements idempotent, hash-stamped, uninstall-reversible blocks:\r\n *\r\n * <!-- agent-connector:begin <connectorId>/<name> hash=<sha256-12> -->\r\n * <!-- Managed by agent-connector for \"<connectorId>\". Do not edit … -->\r\n * …guidance…\r\n * <!-- agent-connector:end <connectorId>/<name> -->\r\n *\r\n * Design rules (each grounded in surveyed prior art — ansible blockinfile,\r\n * conda init, OMC mergeClaudeMd, Ruler, doctoc):\r\n * • UNIQUE marker per block: the blockId (`<connectorId>/<entryName>`) lives\r\n * ON the marker line, so multiple connectors coexist in one file and each\r\n * update path only ever touches its own pair (ansible's documented\r\n * multi-block rule). The shared `agent-connector` namespace token lets\r\n * doctor/uninstall enumerate every block with one scan.\r\n * • `hash=` is the first 12 hex chars of sha256 over the NORMALIZED inner\r\n * region (CRLF→LF + trim — stable under prettier/EOL converters): O(1)\r\n * idempotence (unchanged → skip, no mtime/git churn) and tamper detection\r\n * (actual inner hash ≠ recorded ⇒ the user edited inside the block ⇒ warn,\r\n * never clobber without an explicit force).\r\n * • IN-PLACE replacement (conda semantics): zero bytes outside the marker\r\n * pair ever change — no move-to-top, no user blank-line reflow.\r\n * • Line-anchored matching that explicitly tolerates `\\r` line ends and a\r\n * FENCE-AWARE scanner (markers quoted inside ``` / ~~~ code fences never\r\n * match) — fixes the CRLF duplicate-block and quoted-marker classes.\r\n * • First-begin / FIRST-end-after pairing (never last-end): a missing end\r\n * marker must not swallow a sibling connector's block.\r\n * • HTML-comment markers are CORRECT for CLAUDE.md: Claude Code strips HTML\r\n * comments from CLAUDE.md before context injection (verified against docs\r\n * and the 2.1.172 binary), so the markers and the do-not-edit notice are\r\n * INVISIBLE to the model there while remaining fully parseable by us for\r\n * sync/doctor/uninstall. On AGENTS.md hosts (which inline the whole file\r\n * into the prompt) the short notice doubles as an in-prompt \"do not edit\"\r\n * instruction to the host's own agent. A `commentStyle: \"hash\"` variant\r\n * exists for future non-markdown targets (`#`-prefixed marker lines).\r\n *\r\n * The module also owns the per-connector MEMORY OWNERSHIP LEDGER\r\n * (`connectorDir(id)/memory-state.json`): the markers in the file remain the\r\n * source of truth for uninstall (a teammate's machine with no ledger still\r\n * uninstalls via the prefix scan); the ledger only adds the created-vs-modified\r\n * distinction (file deletion rights) and doctor diagnostics.\r\n */\r\n\r\nimport { createHash } from \"node:crypto\";\r\nimport {\r\n chmodSync,\r\n copyFileSync,\r\n existsSync,\r\n readFileSync,\r\n renameSync,\r\n rmSync,\r\n statSync,\r\n writeFileSync,\r\n} from \"node:fs\";\r\nimport { basename, dirname, join } from \"node:path\";\r\n\r\nimport { assertNoSymlinkInPath, backupsDir, connectorDir, ensureDir } from \"./paths.js\";\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Grammar constants (shared with defineConnector's content validation)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** Literal begin token. Connector memory content MUST NOT contain it. */\r\nexport const MANAGED_BLOCK_BEGIN_TOKEN = \"agent-connector:begin\";\r\n/** Literal end token. Connector memory content MUST NOT contain it. */\r\nexport const MANAGED_BLOCK_END_TOKEN = \"agent-connector:end\";\r\n\r\n/** Hard cap on one memory entry's content (ConnectorConfigError above this). */\r\nexport const MEMORY_CONTENT_HARD_CAP_BYTES = 16 * 1024;\r\n/**\r\n * Soft per-entry budget — exceeded → install-time `warn` ChangeRecord (memory\r\n * files are injected into EVERY prompt of EVERY session on the host; codex\r\n * additionally caps combined project docs at 32 KiB).\r\n */\r\nexport const MEMORY_CONTENT_SOFT_BUDGET_BYTES = 4 * 1024;\r\n\r\n/** Marker comment style: HTML comments (default, all v1 markdown targets) or `#` lines. */\r\nexport type BlockCommentStyle = \"html\" | \"hash\";\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Hashing / rendering\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** CRLF/CR → LF + trim: the normalization both hashing sides use. */\r\nfunction normalizeInner(s: string): string {\r\n return s.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").trim();\r\n}\r\n\r\n/** First 12 hex chars of sha256 over the NORMALIZED inner region. */\r\nexport function hashBlockContent(inner: string): string {\r\n return createHash(\"sha256\").update(normalizeInner(inner), \"utf8\").digest(\"hex\").slice(0, 12);\r\n}\r\n\r\n/** The conda-style in-block warning, adapted for the LLM era (see header). */\r\nfunction defaultNotice(connectorId: string): string {\r\n return (\r\n `Managed by agent-connector for \"${connectorId}\". Do not edit between these markers: ` +\r\n `run your connector package's upgrade/sync command to rewrite this block; ` +\r\n `run its uninstall command to remove it ` +\r\n `(framework fallback: \\`agent-connector uninstall ${connectorId}\\`).`\r\n );\r\n}\r\n\r\nfunction beginLine(blockId: string, hash: string, style: BlockCommentStyle): string {\r\n return style === \"hash\"\r\n ? `# ${MANAGED_BLOCK_BEGIN_TOKEN} ${blockId} hash=${hash}`\r\n : `<!-- ${MANAGED_BLOCK_BEGIN_TOKEN} ${blockId} hash=${hash} -->`;\r\n}\r\n\r\n/** END line is a fixed exact string (no hash) so a content change never strands it. */\r\nfunction endLine(blockId: string, style: BlockCommentStyle): string {\r\n return style === \"hash\"\r\n ? `# ${MANAGED_BLOCK_END_TOKEN} ${blockId}`\r\n : `<!-- ${MANAGED_BLOCK_END_TOKEN} ${blockId} -->`;\r\n}\r\n\r\nfunction noticeLine(notice: string, style: BlockCommentStyle): string {\r\n return style === \"hash\" ? `# ${notice}` : `<!-- ${notice} -->`;\r\n}\r\n\r\nexport interface RenderBlockOptions {\r\n /** `<connectorId>/<entryName>` (or the reserved `_shared/<name>` bridge prefix). */\r\n blockId: string;\r\n /** Connector id named in the default do-not-edit notice line. */\r\n connectorId: string;\r\n /** The guidance payload (plain markdown; normalized + trimmed on emit). */\r\n content: string;\r\n /** Marker comment style; default \"html\". */\r\n commentStyle?: BlockCommentStyle;\r\n /** Override the default notice line TEXT (without comment delimiters). */\r\n notice?: string;\r\n}\r\n\r\n/**\r\n * Render the block as LOGICAL lines (no EOL chars). The recorded `hash=`\r\n * covers the full inner region (notice line + content) so install and scan\r\n * compare like for like.\r\n */\r\nexport function renderBlockLines(opts: RenderBlockOptions): string[] {\r\n const style = opts.commentStyle ?? \"html\";\r\n const inner = [\r\n noticeLine(opts.notice ?? defaultNotice(opts.connectorId), style),\r\n ...normalizeInner(opts.content).split(\"\\n\"),\r\n ];\r\n const hash = hashBlockContent(inner.join(\"\\n\"));\r\n return [beginLine(opts.blockId, hash, style), ...inner, endLine(opts.blockId, style)];\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Scanning (fence-aware, line-anchored, CRLF-tolerant)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n// Both comment styles are RECOGNIZED in one pass (a file may legitimately mix\r\n// styles only across rewrites; emission always uses the requested style).\r\nconst BEGIN_RE =\r\n /^(?:<!--\\s+agent-connector:begin\\s+(\\S+)\\s+hash=([0-9a-f]{12})\\s+-->|#\\s+agent-connector:begin\\s+(\\S+)\\s+hash=([0-9a-f]{12}))\\s*$/;\r\nconst END_RE = /^(?:<!--\\s+agent-connector:end\\s+(\\S+)\\s+-->|#\\s+agent-connector:end\\s+(\\S+))\\s*$/;\r\n\r\ninterface MarkerEvent {\r\n kind: \"begin\" | \"end\";\r\n blockId: string;\r\n /** Recorded hash (begin markers only). */\r\n hash?: string;\r\n /** Line index into the logical-lines array. */\r\n index: number;\r\n}\r\n\r\n/** Strip ONE trailing `\\r` — `raw.split(\"\\n\")` leaves it on CRLF files. */\r\nfunction stripCr(line: string): string {\r\n return line.endsWith(\"\\r\") ? line.slice(0, -1) : line;\r\n}\r\n\r\n/**\r\n * Scan logical lines for marker events, tracking ``` / ~~~ fence state so a\r\n * marker QUOTED inside a fenced code block never matches (the OMC class of\r\n * false positives, eliminated rather than just reduced).\r\n */\r\nfunction scanMarkers(logical: string[]): MarkerEvent[] {\r\n const events: MarkerEvent[] = [];\r\n let fence: { char: string; len: number } | null = null;\r\n for (let i = 0; i < logical.length; i++) {\r\n const line = logical[i]!;\r\n if (fence) {\r\n const close = line.match(/^ {0,3}(`{3,}|~{3,})\\s*$/);\r\n if (close && close[1]![0] === fence.char && close[1]!.length >= fence.len) fence = null;\r\n continue;\r\n }\r\n const open = line.match(/^ {0,3}(`{3,}|~{3,})/);\r\n if (open) {\r\n fence = { char: open[1]![0]!, len: open[1]!.length };\r\n continue;\r\n }\r\n const b = line.match(BEGIN_RE);\r\n if (b) {\r\n events.push({ kind: \"begin\", blockId: (b[1] ?? b[3])!, hash: (b[2] ?? b[4])!, index: i });\r\n continue;\r\n }\r\n const e = line.match(END_RE);\r\n if (e) events.push({ kind: \"end\", blockId: (e[1] ?? e[2])!, index: i });\r\n }\r\n return events;\r\n}\r\n\r\n/**\r\n * Logical lines of `raw` that sit OUTSIDE ``` / ~~~ code fences — for host\r\n * probes that must ignore quoted examples (e.g. the claude-code `@AGENTS.md`\r\n * import detection). Fence lines themselves are excluded.\r\n */\r\nexport function linesOutsideFences(raw: string): string[] {\r\n const body = raw.startsWith(\"\\uFEFF\") ? raw.slice(1) : raw;\r\n const logical = body.split(\"\\n\").map(stripCr);\r\n const out: string[] = [];\r\n let fence: { char: string; len: number } | null = null;\r\n for (const line of logical) {\r\n if (fence) {\r\n const close = line.match(/^ {0,3}(`{3,}|~{3,})\\s*$/);\r\n if (close && close[1]![0] === fence.char && close[1]!.length >= fence.len) fence = null;\r\n continue;\r\n }\r\n const open = line.match(/^ {0,3}(`{3,}|~{3,})/);\r\n if (open) {\r\n fence = { char: open[1]![0]!, len: open[1]!.length };\r\n continue;\r\n }\r\n out.push(line);\r\n }\r\n return out;\r\n}\r\n\r\ninterface BlockPair {\r\n blockId: string;\r\n begin: MarkerEvent;\r\n end: MarkerEvent;\r\n}\r\n\r\n/**\r\n * Pair markers per blockId: first begin, then the FIRST end at-or-after it.\r\n * A nested begin for the SAME id (inside an open pair) is treated as inner\r\n * content; unpaired markers are returned as strays. Per-id state means a\r\n * missing end for connector A can never swallow connector B's block.\r\n */\r\nfunction pairMarkers(events: MarkerEvent[]): { pairs: BlockPair[]; strays: MarkerEvent[] } {\r\n const open = new Map<string, MarkerEvent>();\r\n const pairs: BlockPair[] = [];\r\n const strays: MarkerEvent[] = [];\r\n for (const ev of events) {\r\n if (ev.kind === \"begin\") {\r\n if (!open.has(ev.blockId)) open.set(ev.blockId, ev);\r\n // else: a begin inside an open pair — inner content / abuse; the hash\r\n // check will flag the pair as drifted rather than guessing boundaries.\r\n } else {\r\n const b = open.get(ev.blockId);\r\n if (b) {\r\n pairs.push({ blockId: ev.blockId, begin: b, end: ev });\r\n open.delete(ev.blockId);\r\n } else {\r\n strays.push(ev);\r\n }\r\n }\r\n }\r\n for (const b of open.values()) strays.push(b);\r\n return { pairs, strays };\r\n}\r\n\r\n/** One discovered block (for doctor / refcount / uninstall enumeration). */\r\nexport interface ManagedBlockInfo {\r\n blockId: string;\r\n /** The hash recorded on the begin marker line. */\r\n recordedHash: string;\r\n /** Hash of the actual normalized inner region as found on disk. */\r\n actualHash: string;\r\n /** True when recordedHash ≠ actualHash — the user edited inside the block. */\r\n drifted: boolean;\r\n beginLine: number;\r\n endLine: number;\r\n}\r\n\r\n/** Enumerate every complete agent-connector block in `raw` (namespace scan). */\r\nexport function listManagedBlocks(raw: string): ManagedBlockInfo[] {\r\n const body = raw.startsWith(\"\\uFEFF\") ? raw.slice(1) : raw;\r\n const logical = body.split(\"\\n\").map(stripCr);\r\n const { pairs } = pairMarkers(scanMarkers(logical));\r\n return pairs.map((p) => {\r\n const inner = logical.slice(p.begin.index + 1, p.end.index).join(\"\\n\");\r\n const actualHash = hashBlockContent(inner);\r\n const recordedHash = p.begin.hash ?? \"\";\r\n return {\r\n blockId: p.blockId,\r\n recordedHash,\r\n actualHash,\r\n drifted: recordedHash !== actualHash,\r\n beginLine: p.begin.index,\r\n endLine: p.end.index,\r\n };\r\n });\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Pure text upsert / removal\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\nexport interface UpsertBlockOptions extends RenderBlockOptions {\r\n /** Overwrite a USER-EDITED (drifted) block. Default false → warn + leave intact. */\r\n force?: boolean;\r\n}\r\n\r\nexport interface BlockTextResult {\r\n /** Resulting full text (=== input when nothing changed). */\r\n text: string;\r\n action: \"create\" | \"update\" | \"skip\" | \"warn\";\r\n detail: string;\r\n changed: boolean;\r\n /**\r\n * True when the rewrite was DESTRUCTIVE-ADJACENT (force-overwrote a drifted\r\n * block, collapsed duplicate pairs, or stripped stray markers) — the file\r\n * wrapper writes a timestamped backup before persisting such a change.\r\n */\r\n recovered: boolean;\r\n /** Hash of the (new) rendered inner region — recorded in the ledger. */\r\n hash: string;\r\n}\r\n\r\n/** Detect the file's dominant EOL; new/replaced lines are emitted with it. */\r\nfunction detectEol(body: string): \"\\n\" | \"\\r\\n\" {\r\n const crlf = (body.match(/\\r\\n/g) ?? []).length;\r\n const total = (body.match(/\\n/g) ?? []).length;\r\n return crlf > 0 && crlf >= total - crlf ? \"\\r\\n\" : \"\\n\";\r\n}\r\n\r\n/** Add `\\r` to logical lines when the file is CRLF (joined with \"\\n\" later). */\r\nfunction withEol(lines: string[], eol: \"\\n\" | \"\\r\\n\"): string[] {\r\n return eol === \"\\r\\n\" ? lines.map((l) => `${l}\\r`) : lines;\r\n}\r\n\r\n/**\r\n * Strip trailing newline(s) + whitespace-only line tails WITHOUT touching\r\n * trailing spaces on the last content line (bytes outside our block).\r\n */\r\nfunction trimTrailingBlank(body: string): string {\r\n let out = body;\r\n for (;;) {\r\n const m = out.match(/\\r?\\n[ \\t]*$/);\r\n if (!m) return out;\r\n out = out.slice(0, out.length - m[0].length);\r\n }\r\n}\r\n\r\n/** Append the rendered block at EOF with exactly ONE blank separator line. */\r\nfunction appendBlock(body: string, blockLines: string[], eol: \"\\n\" | \"\\r\\n\"): string {\r\n const blockText = blockLines.join(eol) + eol;\r\n if (body.trim() === \"\") return blockText;\r\n return trimTrailingBlank(body) + eol + eol + blockText;\r\n}\r\n\r\n/**\r\n * Idempotent upsert of one managed block into `raw` (pure; no I/O).\r\n * See the module header for the full algorithm contract.\r\n */\r\nexport function upsertBlockInText(raw: string, opts: UpsertBlockOptions): BlockTextResult {\r\n const bom = raw.startsWith(\"\\uFEFF\");\r\n const body = bom ? raw.slice(1) : raw;\r\n const eol = detectEol(body);\r\n const lines = body.split(\"\\n\"); // elements keep a trailing \"\\r\" on CRLF files\r\n const logical = lines.map(stripCr);\r\n\r\n const events = scanMarkers(logical).filter((e) => e.blockId === opts.blockId);\r\n const { pairs, strays } = pairMarkers(events);\r\n\r\n const newLines = renderBlockLines(opts);\r\n const newHash = hashBlockContent(newLines.slice(1, -1).join(\"\\n\"));\r\n const finish = (text: string, r: Omit<BlockTextResult, \"text\" | \"hash\">): BlockTextResult => ({\r\n text: bom ? `\\uFEFF${text}` : text,\r\n hash: newHash,\r\n ...r,\r\n });\r\n\r\n if (pairs.length > 0) {\r\n const { begin, end } = pairs[0]!;\r\n const recorded = begin.hash ?? \"\";\r\n const actualHash = hashBlockContent(logical.slice(begin.index + 1, end.index).join(\"\\n\"));\r\n const drifted = actualHash !== recorded;\r\n\r\n if (drifted && !opts.force) {\r\n return finish(body, {\r\n action: \"warn\",\r\n detail:\r\n `block ${opts.blockId} was edited inside the markers ` +\r\n `(recorded hash ${recorded}, found ${actualHash}); left untouched — ` +\r\n `re-run with --force to overwrite the in-block edits`,\r\n changed: false,\r\n recovered: false,\r\n });\r\n }\r\n\r\n const duplicates = pairs.slice(1);\r\n const needsWrite =\r\n drifted || recorded !== newHash || duplicates.length > 0 || strays.length > 0;\r\n if (!needsWrite) {\r\n return finish(body, {\r\n action: \"skip\",\r\n detail: `block ${opts.blockId} already up to date (hash ${newHash})`,\r\n changed: false,\r\n recovered: false,\r\n });\r\n }\r\n\r\n // Build the deletion set: the canonical pair region (replaced in place),\r\n // duplicate pairs (+ ONE adjacent blank line each), and stray marker LINES.\r\n const del = new Set<number>();\r\n for (let i = begin.index; i <= end.index; i++) del.add(i);\r\n for (const dup of duplicates) {\r\n for (let i = dup.begin.index; i <= dup.end.index; i++) del.add(i);\r\n reclaimBlankLine(logical, dup.begin.index, dup.end.index, del, lines.length);\r\n }\r\n for (const s of strays) del.add(s.index);\r\n\r\n const out: string[] = [];\r\n for (let i = 0; i < lines.length; i++) {\r\n if (i === begin.index) out.push(...withEol(newLines, eol));\r\n if (!del.has(i)) out.push(lines[i]!);\r\n }\r\n const notes: string[] = [];\r\n if (drifted) notes.push(\"in-block edits overwritten (--force; backup taken)\");\r\n if (duplicates.length > 0) notes.push(`${duplicates.length} duplicate pair(s) collapsed`);\r\n if (strays.length > 0) notes.push(`${strays.length} stray marker line(s) removed`);\r\n return finish(out.join(\"\\n\"), {\r\n action: \"update\",\r\n detail:\r\n `block ${opts.blockId} updated (hash ${recorded || \"?\"} → ${newHash})` +\r\n (notes.length > 0 ? `; ${notes.join(\"; \")}` : \"\"),\r\n changed: true,\r\n recovered: drifted || duplicates.length > 0 || strays.length > 0,\r\n });\r\n }\r\n\r\n if (strays.length > 0) {\r\n // Corruption recovery: strip ONLY the stray marker line(s) (never the text\r\n // between guessed boundaries), then proceed as not-found.\r\n const del = new Set(strays.map((s) => s.index));\r\n const cleaned = lines.filter((_, i) => !del.has(i)).join(\"\\n\");\r\n return finish(appendBlock(cleaned, newLines, eol), {\r\n action: \"create\",\r\n detail:\r\n `block ${opts.blockId} appended after recovery: ${strays.length} stray ` +\r\n `marker line(s) removed (backup taken)`,\r\n changed: true,\r\n recovered: true,\r\n });\r\n }\r\n\r\n // Not found → append at EOF (one blank separator; whole file when empty).\r\n return finish(appendBlock(body, newLines, eol), {\r\n action: \"create\",\r\n detail: `block ${opts.blockId} appended (hash ${newHash})`,\r\n changed: true,\r\n recovered: false,\r\n });\r\n}\r\n\r\n/**\r\n * Mark ONE blank line adjacent to a removed begin..end region for deletion\r\n * (prefer the line before; never the post-final-newline \"\" sentinel).\r\n */\r\nfunction reclaimBlankLine(\r\n logical: string[],\r\n beginIdx: number,\r\n endIdx: number,\r\n del: Set<number>,\r\n lineCount: number,\r\n): void {\r\n const before = beginIdx - 1;\r\n if (before >= 0 && !del.has(before) && logical[before]!.trim() === \"\") {\r\n del.add(before);\r\n return;\r\n }\r\n const after = endIdx + 1;\r\n // The final \"\" element of split(\"\\n\") on a newline-terminated file is the\r\n // trailing-newline sentinel, not a blank line — leave it alone.\r\n const isSentinel = after === lineCount - 1 && logical[after] === \"\";\r\n if (after < lineCount && !isSentinel && !del.has(after) && logical[after]!.trim() === \"\") {\r\n del.add(after);\r\n }\r\n}\r\n\r\nexport interface RemoveBlocksOptions {\r\n /** Remove the block with EXACTLY this id… */\r\n blockId?: string;\r\n /** …or every block whose id starts with this prefix (e.g. `<connectorId>/`). */\r\n blockIdPrefix?: string;\r\n}\r\n\r\nexport interface RemoveRecord {\r\n blockId: string;\r\n action: \"remove\" | \"warn\";\r\n detail: string;\r\n}\r\n\r\nexport interface RemoveBlocksResult {\r\n text: string;\r\n changed: boolean;\r\n records: RemoveRecord[];\r\n /** True when the remaining text is whitespace-only (file-deletion candidate). */\r\n fileNowEmpty: boolean;\r\n /** True when a drifted block was removed or stray markers were stripped. */\r\n recovered: boolean;\r\n}\r\n\r\n/**\r\n * Remove every matching managed block (begin..end inclusive + at most ONE\r\n * adjacent blank separator line — no blank-line accumulation across cycles).\r\n * Drifted blocks are removed WITH a warn (uninstall intent is explicit; the\r\n * file wrapper backs the file up first). Stray matching markers are stripped\r\n * line-only with a warn. Pure; no I/O.\r\n */\r\nexport function removeBlocksFromText(raw: string, match: RemoveBlocksOptions): RemoveBlocksResult {\r\n const bom = raw.startsWith(\"\\uFEFF\");\r\n const body = bom ? raw.slice(1) : raw;\r\n const lines = body.split(\"\\n\");\r\n const logical = lines.map(stripCr);\r\n\r\n const matches = (id: string): boolean =>\r\n (match.blockId !== undefined && id === match.blockId) ||\r\n (match.blockIdPrefix !== undefined && id.startsWith(match.blockIdPrefix));\r\n\r\n const { pairs, strays } = pairMarkers(scanMarkers(logical));\r\n const targetPairs = pairs.filter((p) => matches(p.blockId));\r\n const targetStrays = strays.filter((s) => matches(s.blockId));\r\n\r\n if (targetPairs.length === 0 && targetStrays.length === 0) {\r\n return { text: raw, changed: false, records: [], fileNowEmpty: false, recovered: false };\r\n }\r\n\r\n const del = new Set<number>();\r\n const records: RemoveRecord[] = [];\r\n let recovered = false;\r\n\r\n for (const p of targetPairs) {\r\n const recorded = p.begin.hash ?? \"\";\r\n const actualHash = hashBlockContent(\r\n logical.slice(p.begin.index + 1, p.end.index).join(\"\\n\"),\r\n );\r\n for (let i = p.begin.index; i <= p.end.index; i++) del.add(i);\r\n reclaimBlankLine(logical, p.begin.index, p.end.index, del, lines.length);\r\n if (actualHash !== recorded) {\r\n recovered = true;\r\n records.push({\r\n blockId: p.blockId,\r\n action: \"warn\",\r\n detail:\r\n `block ${p.blockId} had in-block edits (recorded hash ${recorded}, found ` +\r\n `${actualHash}); removed anyway (backup taken)`,\r\n });\r\n }\r\n records.push({ blockId: p.blockId, action: \"remove\", detail: `block ${p.blockId} removed` });\r\n }\r\n for (const s of targetStrays) {\r\n del.add(s.index);\r\n recovered = true;\r\n records.push({\r\n blockId: s.blockId,\r\n action: \"warn\",\r\n detail: `stray ${s.kind} marker for ${s.blockId} removed (marker line only)`,\r\n });\r\n }\r\n\r\n const text = lines.filter((_, i) => !del.has(i)).join(\"\\n\");\r\n return {\r\n text: bom ? `\\uFEFF${text}` : text,\r\n changed: true,\r\n records,\r\n fileNowEmpty: text.trim() === \"\",\r\n recovered,\r\n };\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// File-level wrappers (atomic write, backups, dry-run)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\nexport interface BlockFileChange {\r\n action: \"create\" | \"update\" | \"skip\" | \"warn\" | \"remove\";\r\n path: string;\r\n detail: string;\r\n /** True when the FILE did not exist and this upsert created it (ledger fact). */\r\n createdFile: boolean;\r\n /** Set when a pre-mutation backup was written (destructive/recovery paths only). */\r\n backupPath?: string;\r\n /** Hash of the rendered inner region (ledger / doctor). */\r\n hash: string;\r\n}\r\n\r\n/** Timestamped copy into backupsDir(); best-effort (null on failure). */\r\nfunction backupFile(path: string): string | null {\r\n try {\r\n ensureDir(backupsDir());\r\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\r\n const dest = join(backupsDir(), `memory-${stamp}-${basename(path)}`);\r\n copyFileSync(path, dest);\r\n return dest;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Atomic write: temp file in the SAME directory + rename, preserving the\r\n * destination's mode. Shrinks (does not eliminate) the read-modify-write race\r\n * with hosts that append to memory files mid-session (Claude `#` quick-memory).\r\n */\r\nfunction atomicWriteFile(path: string, text: string): void {\r\n const dir = dirname(path);\r\n ensureDir(dir);\r\n const tmp = join(dir, `.${basename(path)}.ac-tmp-${process.pid}-${Date.now()}`);\r\n try {\r\n writeFileSync(tmp, text, \"utf8\");\r\n try {\r\n chmodSync(tmp, statSync(path).mode);\r\n } catch {\r\n /* destination absent — default mode is fine */\r\n }\r\n renameSync(tmp, path);\r\n } catch (err) {\r\n rmSync(tmp, { force: true });\r\n throw err;\r\n }\r\n}\r\n\r\n/**\r\n * Upsert one managed block into the file at `path` (created when absent).\r\n * Re-reads immediately before computing+writing; honors dryRun (the action is\r\n * computed and reported, nothing is written).\r\n */\r\nexport function upsertManagedBlockFile(\r\n path: string,\r\n opts: UpsertBlockOptions & { dryRun: boolean },\r\n): BlockFileChange {\r\n let raw: string | null = null;\r\n if (existsSync(path)) {\r\n try {\r\n raw = readFileSync(path, \"utf8\");\r\n } catch {\r\n return {\r\n action: \"warn\",\r\n path,\r\n detail: `cannot read ${path}; memory block ${opts.blockId} not written`,\r\n createdFile: false,\r\n hash: \"\",\r\n };\r\n }\r\n }\r\n\r\n if (raw === null) {\r\n const lines = renderBlockLines(opts);\r\n const hash = hashBlockContent(lines.slice(1, -1).join(\"\\n\"));\r\n if (!opts.dryRun) atomicWriteFile(path, `${lines.join(\"\\n\")}\\n`);\r\n return {\r\n action: \"create\",\r\n path,\r\n detail: `created ${basename(path)} with block ${opts.blockId} (hash ${hash})`,\r\n createdFile: true,\r\n hash,\r\n };\r\n }\r\n\r\n const res = upsertBlockInText(raw, opts);\r\n const change: BlockFileChange = {\r\n action: res.action,\r\n path,\r\n detail: res.detail,\r\n createdFile: false,\r\n hash: res.hash,\r\n };\r\n if (res.changed && !opts.dryRun) {\r\n if (res.recovered) {\r\n const backup = backupFile(path);\r\n if (backup) change.backupPath = backup;\r\n }\r\n atomicWriteFile(path, res.text);\r\n }\r\n return change;\r\n}\r\n\r\n/**\r\n * Remove every matching managed block from the file at `path`. Missing file or\r\n * no matching markers → a single idempotent \"skip\". When the remaining content\r\n * is whitespace-only AND `deleteFileIfCreated` is true (the ledger recorded\r\n * that agent-connector created this file), the file itself is deleted (Ruler\r\n * revert semantics: delete only what we created).\r\n */\r\nexport function removeManagedBlocksFile(\r\n path: string,\r\n match: RemoveBlocksOptions,\r\n opts: { dryRun: boolean; deleteFileIfCreated?: boolean },\r\n): BlockFileChange[] {\r\n if (!existsSync(path)) {\r\n return [\r\n {\r\n action: \"skip\",\r\n path,\r\n detail: `${basename(path)} absent; nothing to remove`,\r\n createdFile: false,\r\n hash: \"\",\r\n },\r\n ];\r\n }\r\n let raw: string;\r\n try {\r\n raw = readFileSync(path, \"utf8\");\r\n } catch {\r\n return [\r\n {\r\n action: \"warn\",\r\n path,\r\n detail: `cannot read ${path}; managed blocks left in place`,\r\n createdFile: false,\r\n hash: \"\",\r\n },\r\n ];\r\n }\r\n\r\n const res = removeBlocksFromText(raw, match);\r\n if (!res.changed) {\r\n return [\r\n {\r\n action: \"skip\",\r\n path,\r\n detail: `no agent-connector blocks matching ${match.blockId ?? match.blockIdPrefix ?? \"?\"} in ${basename(path)}`,\r\n createdFile: false,\r\n hash: \"\",\r\n },\r\n ];\r\n }\r\n\r\n const changes: BlockFileChange[] = [];\r\n let backupPath: string | undefined;\r\n if (!opts.dryRun) {\r\n if (res.recovered) backupPath = backupFile(path) ?? undefined;\r\n if (res.fileNowEmpty && opts.deleteFileIfCreated) {\r\n rmSync(path, { force: true });\r\n } else {\r\n atomicWriteFile(path, res.text);\r\n }\r\n }\r\n for (const r of res.records) {\r\n changes.push({\r\n action: r.action,\r\n path,\r\n detail: r.detail,\r\n createdFile: false,\r\n hash: \"\",\r\n ...(backupPath && r.action === \"warn\" ? { backupPath } : {}),\r\n });\r\n }\r\n if (res.fileNowEmpty && opts.deleteFileIfCreated) {\r\n changes.push({\r\n action: \"remove\",\r\n path,\r\n detail: `${basename(path)} deleted (agent-connector created it; only whitespace remained)`,\r\n createdFile: false,\r\n hash: \"\",\r\n });\r\n }\r\n return changes;\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Memory ownership ledger — connectorDir(id)/memory-state.json\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\nexport interface MemoryLedgerTarget {\r\n platform: string;\r\n scope: string;\r\n path: string;\r\n blockId: string;\r\n /** True when agent-connector CREATED the file (grants deletion rights). */\r\n createdFile: boolean;\r\n /** Inner-region hash we last wrote (doctor drift detection). */\r\n hash: string;\r\n}\r\n\r\nexport interface MemoryLedger {\r\n version: 1;\r\n targets: MemoryLedgerTarget[];\r\n}\r\n\r\nexport function memoryLedgerPath(connectorId: string): string {\r\n return join(connectorDir(connectorId), \"memory-state.json\");\r\n}\r\n\r\nexport function loadMemoryLedger(connectorId: string): MemoryLedger {\r\n const p = memoryLedgerPath(connectorId);\r\n if (!existsSync(p)) return { version: 1, targets: [] };\r\n try {\r\n const parsed = JSON.parse(readFileSync(p, \"utf8\")) as MemoryLedger;\r\n if (parsed && Array.isArray(parsed.targets)) return { version: 1, targets: parsed.targets };\r\n } catch {\r\n /* corrupt → fresh ledger; the markers remain the source of truth */\r\n }\r\n return { version: 1, targets: [] };\r\n}\r\n\r\n/** Persist the ledger; an EMPTY ledger deletes the file (no orphan state). */\r\nexport function saveMemoryLedger(connectorId: string, ledger: MemoryLedger): void {\r\n const p = memoryLedgerPath(connectorId);\r\n assertNoSymlinkInPath(p);\r\n if (ledger.targets.length === 0) {\r\n rmSync(p, { force: true });\r\n return;\r\n }\r\n ensureDir(dirname(p));\r\n writeFileSync(p, `${JSON.stringify(ledger, null, 2)}\\n`, \"utf8\");\r\n}\r\n\r\n/** True when the connector has any recorded memory targets (uninstall guard). */\r\nexport function hasMemoryLedger(connectorId: string): boolean {\r\n return loadMemoryLedger(connectorId).targets.length > 0;\r\n}\r\n\r\n/**\r\n * Upsert one ledger row (keyed by platform+scope+path+blockId). The\r\n * `createdFile` fact is STICKY: once we created the file, deletion rights\r\n * survive later syncs that found it present.\r\n */\r\nexport function recordMemoryTarget(ledger: MemoryLedger, t: MemoryLedgerTarget): void {\r\n const i = ledger.targets.findIndex(\r\n (x) =>\r\n x.platform === t.platform &&\r\n x.scope === t.scope &&\r\n x.path === t.path &&\r\n x.blockId === t.blockId,\r\n );\r\n if (i >= 0) {\r\n ledger.targets[i] = { ...t, createdFile: ledger.targets[i]!.createdFile || t.createdFile };\r\n } else {\r\n ledger.targets.push(t);\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/telemetry/store.ts","../src/telemetry/types.ts","../src/telemetry/tokenizer.ts"],"sourcesContent":["/**\r\n * telemetry/store — the default NDJSON {@link TelemetryStore} implementation.\r\n *\r\n * Local-first, dependency-free persistence. One JSON object per line; appends\r\n * are atomic enough for concurrent multi-host writers (a single `appendFileSync`\r\n * of a `<line>\\n` chunk is written in one `write(2)` for small payloads, so\r\n * interleaving across processes does not corrupt individual records). A\r\n * SQLite/WAL backend is a drop-in upgrade behind the same interface.\r\n *\r\n * Telemetry stores AGGREGATE COUNTS ONLY — raw tool arguments/results are never\r\n * persisted (enforced by the {@link ToolEventRecord} shape, which carries no\r\n * content fields).\r\n *\r\n * Global kill switch: AGENT_CONNECTOR_TELEMETRY=0 makes `append` a no-op so a\r\n * disabled telemetry layer can never touch disk or break a host's tool call.\r\n */\r\n\r\nimport { appendFileSync, existsSync, readFileSync } from \"node:fs\";\r\nimport { dirname } from \"node:path\";\r\n\r\nimport { ensureDir, telemetryPath } from \"../core/paths.js\";\r\nimport { worstConfidence } from \"./types.js\";\r\nimport type {\r\n QueryFilter,\r\n RollupRow,\r\n TelemetryStore,\r\n ToolEventRecord,\r\n} from \"./types.js\";\r\n\r\n/** Options for {@link openStore}. */\r\nexport interface OpenStoreOptions {\r\n /** Override the NDJSON file path. Defaults to telemetryPath(\"ndjson\"). */\r\n path?: string;\r\n}\r\n\r\n// Confidence ranking + worst-of comparison live in ./types (the single source\r\n// of truth) so a new ConfidenceSource value orders correctly everywhere.\r\n\r\n/** Is `process.env.AGENT_CONNECTOR_TELEMETRY` an explicit off switch? */\r\nfunction telemetryDisabled(): boolean {\r\n return process.env.AGENT_CONNECTOR_TELEMETRY === \"0\";\r\n}\r\n\r\n/**\r\n * A unique-enough record id. Combines the wall clock with a caller-supplied\r\n * sequence so two records written in the same millisecond still differ.\r\n */\r\nexport function newRecordId(seq: number): string {\r\n return `${Date.now()}-${seq}`;\r\n}\r\n\r\nclass NdjsonStore implements TelemetryStore {\r\n private readonly path: string;\r\n\r\n constructor(path: string) {\r\n this.path = path;\r\n }\r\n\r\n append(record: ToolEventRecord): void {\r\n if (telemetryDisabled()) return;\r\n ensureDir(dirname(this.path));\r\n appendFileSync(this.path, `${JSON.stringify(record)}\\n`, \"utf8\");\r\n }\r\n\r\n query(filter: QueryFilter): ToolEventRecord[] {\r\n if (!existsSync(this.path)) return [];\r\n\r\n let text: string;\r\n try {\r\n text = readFileSync(this.path, \"utf8\");\r\n } catch {\r\n return [];\r\n }\r\n\r\n const out: ToolEventRecord[] = [];\r\n for (const line of text.split(\"\\n\")) {\r\n const trimmed = line.trim();\r\n if (trimmed === \"\") continue;\r\n\r\n let record: ToolEventRecord;\r\n try {\r\n record = JSON.parse(trimmed) as ToolEventRecord;\r\n } catch {\r\n continue; // skip malformed lines\r\n }\r\n\r\n if (matches(record, filter)) out.push(record);\r\n }\r\n return out;\r\n }\r\n\r\n rollup(by: \"tool\" | \"session\" | \"project\", filter: QueryFilter): RollupRow[] {\r\n const groups = new Map<string, RollupRow>();\r\n\r\n for (const record of this.query(filter)) {\r\n const key = groupKey(record, by);\r\n const existing = groups.get(key);\r\n if (existing === undefined) {\r\n groups.set(key, {\r\n key,\r\n calls: 1,\r\n inputTokens: record.inputTokens,\r\n outputTokens: record.outputTokens,\r\n totalTokens: record.inputTokens + record.outputTokens,\r\n confidence: record.confidenceSource,\r\n lastTs: record.ts,\r\n });\r\n } else {\r\n existing.calls += 1;\r\n existing.inputTokens += record.inputTokens;\r\n existing.outputTokens += record.outputTokens;\r\n existing.totalTokens += record.inputTokens + record.outputTokens;\r\n existing.confidence = worstConfidence(\r\n existing.confidence,\r\n record.confidenceSource,\r\n );\r\n if (record.ts > existing.lastTs) existing.lastTs = record.ts;\r\n }\r\n }\r\n\r\n return [...groups.values()];\r\n }\r\n\r\n close(): void {\r\n /* no-op for the NDJSON backend */\r\n }\r\n}\r\n\r\n/** Does a record satisfy every set field of the filter? */\r\nfunction matches(record: ToolEventRecord, filter: QueryFilter): boolean {\r\n if (\r\n filter.connectorId !== undefined &&\r\n record.connectorId !== filter.connectorId\r\n ) {\r\n return false;\r\n }\r\n if (\r\n filter.projectKey !== undefined &&\r\n record.projectKey !== filter.projectKey\r\n ) {\r\n return false;\r\n }\r\n if (filter.sessionId !== undefined && record.sessionId !== filter.sessionId) {\r\n return false;\r\n }\r\n if (filter.toolName !== undefined && record.toolName !== filter.toolName) {\r\n return false;\r\n }\r\n if (filter.sinceMs !== undefined && record.ts < filter.sinceMs) {\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n/** Resolve the grouping key value for a record under the requested dimension. */\r\nfunction groupKey(\r\n record: ToolEventRecord,\r\n by: \"tool\" | \"session\" | \"project\",\r\n): string {\r\n switch (by) {\r\n case \"tool\":\r\n return record.toolName;\r\n case \"session\":\r\n return record.sessionId;\r\n case \"project\":\r\n return record.projectDir;\r\n }\r\n}\r\n\r\n/**\r\n * Open the default local telemetry store. NDJSON backend; no native deps. The\r\n * returned store is safe to use even when telemetry is disabled — `append`\r\n * becomes a no-op and reads of a missing file return empty results.\r\n */\r\nexport function openStore(opts?: OpenStoreOptions): TelemetryStore {\r\n const path = opts?.path ?? telemetryPath(\"ndjson\");\r\n return new NdjsonStore(path);\r\n}\r\n","/**\r\n * telemetry/types — runtime telemetry contracts.\r\n *\r\n * The framework measures the MCP server's OWN bytes (tool args in, tool result\r\n * out, tool-definition schemas) and tokenizes them locally — the only signal\r\n * identical across every host (no host reports per-tool usage to a server).\r\n * Records are aggregate counts only; raw arguments/results are NEVER stored.\r\n */\r\n\r\nimport type { PlatformId } from \"../core/types.js\";\r\n\r\n/** Provenance of a token count — surfaced so \"estimate\" is never read as \"exact\". */\r\nexport type ConfidenceSource =\r\n | \"tokenizer-exact\" // real BPE for a matching model family\r\n | \"tokenizer-calibrated\" // approx adjusted by a sampled Anthropic count_tokens factor\r\n | \"tokenizer-approx\" // real BPE used as a documented approximation (e.g. Anthropic)\r\n | \"heuristic\" // chars/4 fallback\r\n | \"host-native\"; // a host actually reported usage (e.g. Gemini AfterModel)\r\n\r\n/**\r\n * Confidence ranking, least-trustworthy (0) → most-trustworthy. The single\r\n * source of truth for ordering {@link ConfidenceSource} values; every\r\n * worst-of comparison across the telemetry subsystem (store rollup, both\r\n * leaderboards, report, measure) reads through {@link rankOf} /\r\n * {@link worstConfidence} so a new value orders correctly everywhere.\r\n *\r\n * heuristic < tokenizer-approx < tokenizer-calibrated < tokenizer-exact < host-native\r\n *\r\n * `tokenizer-calibrated` sits between the raw approximation and an exact\r\n * family-matched BPE count: it is an approximation nudged toward truth by a\r\n * sampled real count_tokens factor, so it is more trustworthy than bare approx\r\n * but still not the exact encoding for the target family.\r\n */\r\nexport const CONFIDENCE_RANK: Record<ConfidenceSource, number> = {\r\n heuristic: 0,\r\n \"tokenizer-approx\": 1,\r\n \"tokenizer-calibrated\": 2,\r\n \"tokenizer-exact\": 3,\r\n \"host-native\": 4,\r\n};\r\n\r\n/** Trust rank of a single {@link ConfidenceSource} (higher = more trustworthy). */\r\nexport function rankOf(c: ConfidenceSource): number {\r\n return CONFIDENCE_RANK[c];\r\n}\r\n\r\n/** Return whichever source is the worse (least-confident) of the two. */\r\nexport function worstConfidence(\r\n a: ConfidenceSource,\r\n b: ConfidenceSource,\r\n): ConfidenceSource {\r\n return CONFIDENCE_RANK[b] < CONFIDENCE_RANK[a] ? b : a;\r\n}\r\n\r\nexport type ModelFamily = \"openai\" | \"anthropic\" | \"generic\";\r\n\r\nexport interface TokenCount {\r\n tokens: number;\r\n source: ConfidenceSource;\r\n}\r\n\r\n/** Pluggable tokenizer. Default impl uses gpt-tokenizer; fallback is chars/4. */\r\nexport interface Tokenizer {\r\n /** Count tokens for a string under the given model family. */\r\n count(text: string, family: ModelFamily): TokenCount;\r\n /** Count tokens for an arbitrary JSON value (serialized canonically first). */\r\n countValue(value: unknown, family: ModelFamily): TokenCount;\r\n}\r\n\r\n/**\r\n * What a record measures. DISTINCT origins that must never be summed:\r\n * • `call` — one per-MCP `tools/call` round-trip (serve-proxy bytes).\r\n * • `tool_defs` — the one-time `tools/list` schema overhead (serve-proxy).\r\n * • `model_turn` — a WHOLE-CONVERSATION host-native turn reported by the host\r\n * (e.g. Gemini/Antigravity AfterModel `usageMetadata`). This is NOT per-MCP:\r\n * it covers the entire model turn, so the MCP leaderboard EXCLUDES it (just\r\n * as it treats `tool_defs` specially) and it is surfaced as its own labeled\r\n * section rather than added to the per-MCP `call` totals or to the\r\n * usage-reader host-scan numbers.\r\n * • `hook` — one RUNTIME hook dispatch through the home-bin `hook`\r\n * entrypoint (src/runtime/hook-entrypoint). The framework tokenizes the\r\n * inbound normalized event payload the handler reads (input) and what the\r\n * handler returns that becomes context/decision (output). The per-item\r\n * `toolName` is the hook EVENT name (e.g. \"SessionStart\"). This is the\r\n * developer-axis \"hook\" surface — measured live, like `call`.\r\n */\r\nexport type EventScope = \"call\" | \"tool_defs\" | \"model_turn\" | \"hook\";\r\n\r\n/**\r\n * Which of the FIVE developer-axis surfaces a record (or footprint) belongs to.\r\n * `server`/`hook` are RUNTIME-measured surfaces that produce {@link ToolEventRecord}\r\n * store rows; `command`/`skill`/`subagent` are STATIC footprints computed from the\r\n * registered connector (the host loads them as context — we never intercept them,\r\n * so they are NEVER written as usage rows). OPTIONAL on the record so rows written\r\n * before this field existed (every legacy serve-proxy row) read as `server`.\r\n */\r\nexport type SurfaceKind = \"server\" | \"hook\" | \"command\" | \"skill\" | \"subagent\";\r\n\r\n/**\r\n * The install scope a wrapped server was deployed under, narrowed to the two\r\n * dimensions that matter for slicing telemetry: a `user`-global install vs a\r\n * `project`-local one. The framework's broader {@link InstallScope}\r\n * (`system|user|project|profile|managed`) is mapped down to this at wrap time —\r\n * everything that is not project-local reads as `user`. Optional on the record\r\n * so rows written before this field existed are treated as \"unknown\".\r\n */\r\nexport type TelemetryInstallScope = \"user\" | \"project\";\r\n\r\n/**\r\n * How the real MCP server underneath the proxy was launched. `npx`/`bunx`/`uvx`\r\n * are ephemeral package runners; `node`/`bun`/`deno` are interpreters running a\r\n * local script; `binary` is a resolved executable on PATH; `http` marks a remote\r\n * server reached over the network (no local launch); `unknown` is the honest\r\n * fallback. Optional on the record so older rows read as \"unknown\".\r\n */\r\nexport type LaunchMethod =\r\n | \"npx\"\r\n | \"bunx\"\r\n | \"uvx\"\r\n | \"node\"\r\n | \"binary\"\r\n | \"http\"\r\n | \"unknown\";\r\n\r\n/**\r\n * One telemetry row. Aggregate counts only — no content. `projectKey` is the\r\n * hashed stable project identity (git remote || normalized abs path); `projectDir`\r\n * is kept human-readable for reports but is the same partition.\r\n */\r\nexport interface ToolEventRecord {\r\n /** Monotonic-ish unique id (e.g. `${ts}-${seq}`). */\r\n id: string;\r\n /** Epoch milliseconds. */\r\n ts: number;\r\n connectorId: string;\r\n toolName: string;\r\n scope: EventScope;\r\n hostPlatform: PlatformId;\r\n sessionId: string;\r\n projectKey: string;\r\n projectDir: string;\r\n inputTokens: number;\r\n outputTokens: number;\r\n confidenceSource: ConfidenceSource;\r\n isError: boolean;\r\n /**\r\n * The (narrowed) install scope the wrapped server was deployed under — a\r\n * slicing dimension for global(user) vs project usage. OPTIONAL: rows written\r\n * before this field existed lack it and must be read as \"unknown\".\r\n */\r\n installScope?: TelemetryInstallScope;\r\n /**\r\n * How the real server was launched (npx/bunx/uvx/node/binary/http) — the\r\n * \"launch-method\" slicing dimension. OPTIONAL: older rows lack it → \"unknown\".\r\n */\r\n launchMethod?: LaunchMethod;\r\n /**\r\n * Which developer-axis surface produced this row. OPTIONAL and\r\n * backward-compatible: rows written before this field existed (every legacy\r\n * serve-proxy `call`/`tool_defs` row) lack it and MUST be read as `server`.\r\n * Runtime rows stamp it explicitly (`server` for the proxy, `hook` for the\r\n * hook runtime); the static command/skill/subagent surfaces never produce\r\n * store rows (they are reported as footprints — see surface-footprint.ts).\r\n */\r\n surfaceKind?: SurfaceKind;\r\n}\r\n\r\nexport interface QueryFilter {\r\n connectorId?: string;\r\n projectKey?: string;\r\n sessionId?: string;\r\n toolName?: string;\r\n /** Lower bound epoch ms (inclusive). */\r\n sinceMs?: number;\r\n}\r\n\r\n/** A grouped rollup row for reports. */\r\nexport interface RollupRow {\r\n /** The grouping key value (tool name, session id, or project dir). */\r\n key: string;\r\n calls: number;\r\n inputTokens: number;\r\n outputTokens: number;\r\n totalTokens: number;\r\n /** Worst (least-confident) source seen in the group, for honest labeling. */\r\n confidence: ConfidenceSource;\r\n lastTs: number;\r\n}\r\n\r\n/**\r\n * Local-first telemetry store. The default NDJSON implementation appends one\r\n * line per record (append is atomic enough for concurrent multi-host writers);\r\n * a SQLite/WAL implementation is a drop-in upgrade behind this same interface.\r\n */\r\nexport interface TelemetryStore {\r\n append(record: ToolEventRecord): void;\r\n query(filter: QueryFilter): ToolEventRecord[];\r\n /** Group + aggregate by tool | session | project. */\r\n rollup(by: \"tool\" | \"session\" | \"project\", filter: QueryFilter): RollupRow[];\r\n close(): void;\r\n}\r\n","/**\r\n * telemetry/tokenizer — the default {@link Tokenizer} implementation.\r\n *\r\n * Token counts are the only signal that is identical across every host: no host\r\n * reports a server's per-tool usage back to the server, so the framework\r\n * tokenizes its OWN bytes locally. We use gpt-tokenizer's `o200k_base` encoding\r\n * (the GPT-4o / o1 BPE) for all families:\r\n * • openai → exact BPE for the target family → \"tokenizer-exact\"\r\n * • anthropic → o200k_base used as a documented stand-in → \"tokenizer-approx\"\r\n * • generic → o200k_base used as a generic approximation → \"tokenizer-approx\"\r\n *\r\n * gpt-tokenizer is loaded LAZILY on first use, inside a try/catch. If the module\r\n * (or its BPE data) fails to load we degrade to a chars/4 heuristic that never\r\n * throws, so telemetry can never break a host's tool call.\r\n *\r\n * Pure + side-effect-free apart from the one-time lazy encoder load. No IO.\r\n */\r\n\r\nimport { createRequire } from \"node:module\";\r\n\r\nimport type {\r\n ConfidenceSource,\r\n ModelFamily,\r\n TokenCount,\r\n Tokenizer,\r\n} from \"./types.js\";\r\n\r\n/**\r\n * A CommonJS `require` bound to this module's URL. gpt-tokenizer ships its\r\n * encoding entry points as CJS (the package `exports` `require` condition maps\r\n * `./encoding/*` to `./cjs/encoding/*.js`), so we resolve them synchronously via\r\n * createRequire — this keeps the {@link Tokenizer} surface synchronous and works\r\n * under ESM / NodeNext without baking an absolute path.\r\n */\r\nconst requireCjs = createRequire(import.meta.url);\r\n\r\n/** Minimal shape we use from a gpt-tokenizer encoding module. */\r\ninterface EncodingModule {\r\n encode(text: string): number[];\r\n}\r\n\r\n/**\r\n * Lazy-load state for the shared o200k_base encoder.\r\n * - undefined → not attempted yet\r\n * - null → load attempted and failed (stick to the heuristic)\r\n * - object → loaded encoder\r\n */\r\nlet encoderState: EncodingModule | null | undefined;\r\n\r\n/** Average characters-per-token for the chars/4 fallback heuristic. */\r\nconst HEURISTIC_CHARS_PER_TOKEN = 4;\r\n\r\n/**\r\n * Lazily import the o200k_base encoding. Returns the encoder, or null if the\r\n * module/data could not be loaded (in which case callers use the heuristic).\r\n *\r\n * The dynamic import resolves to `gpt-tokenizer/esm/encoding/o200k_base.js` via\r\n * the package `exports` map and exposes a named `encode(text) => number[]`.\r\n */\r\nfunction loadEncoder(): EncodingModule | null {\r\n if (encoderState !== undefined) return encoderState;\r\n try {\r\n // Synchronous require keeps count() a sync function (per the Tokenizer\r\n // interface). The package's `require` export condition resolves this to\r\n // gpt-tokenizer/cjs/encoding/o200k_base.js, which exports a named `encode`.\r\n const mod = requireCjs(\"gpt-tokenizer/encoding/o200k_base\") as {\r\n encode?: (text: string) => number[];\r\n };\r\n if (mod && typeof mod.encode === \"function\") {\r\n encoderState = { encode: mod.encode.bind(mod) };\r\n } else {\r\n encoderState = null;\r\n }\r\n } catch {\r\n encoderState = null;\r\n }\r\n return encoderState;\r\n}\r\n\r\n/** Heuristic chars/4 count — used when no encoder is available. Never throws. */\r\nfunction heuristicCount(text: string): TokenCount {\r\n return {\r\n tokens: Math.ceil(text.length / HEURISTIC_CHARS_PER_TOKEN),\r\n source: \"heuristic\",\r\n };\r\n}\r\n\r\n/**\r\n * Confidence label for a successful BPE count: exact only for the OpenAI family\r\n * (the encoding actually matches), approximate for everything else.\r\n */\r\nfunction bpeSource(family: ModelFamily): ConfidenceSource {\r\n return family === \"openai\" ? \"tokenizer-exact\" : \"tokenizer-approx\";\r\n}\r\n\r\n/**\r\n * The default tokenizer. A single shared instance is fine: the encoder is\r\n * stateless across calls and lazy-loaded once.\r\n */\r\nclass DefaultTokenizer implements Tokenizer {\r\n count(text: string, family: ModelFamily): TokenCount {\r\n const encoder = loadEncoder();\r\n if (!encoder) return heuristicCount(text);\r\n try {\r\n return { tokens: encoder.encode(text).length, source: bpeSource(family) };\r\n } catch {\r\n // A per-call encode failure (e.g. a pathological input) still degrades\r\n // gracefully rather than breaking the host's tool call.\r\n return heuristicCount(text);\r\n }\r\n }\r\n\r\n countValue(value: unknown, family: ModelFamily): TokenCount {\r\n return this.count(canonicalString(value), family);\r\n }\r\n}\r\n\r\n/**\r\n * Canonical string form of an arbitrary JSON value for counting.\r\n * `undefined` (and anything that JSON.stringify drops to undefined, e.g. a bare\r\n * function or symbol) serializes to the empty string.\r\n */\r\nfunction canonicalString(value: unknown): string {\r\n if (typeof value === \"string\") return value;\r\n const json = JSON.stringify(value);\r\n return json ?? \"\";\r\n}\r\n\r\nconst SHARED_TOKENIZER: Tokenizer = new DefaultTokenizer();\r\n\r\n/** Return the default {@link Tokenizer} (shared singleton). */\r\nexport function getTokenizer(): Tokenizer {\r\n return SHARED_TOKENIZER;\r\n}\r\n\r\n/**\r\n * Infer the {@link ModelFamily} for token counting.\r\n *\r\n * An explicit hint other than \"auto\" wins outright. Otherwise we sniff the\r\n * client/host name: Anthropic/Claude → anthropic, OpenAI/GPT/Codex → openai,\r\n * Gemini and everything unrecognized → generic.\r\n */\r\nexport function inferModelFamily(\r\n clientName: string,\r\n hint: \"auto\" | ModelFamily,\r\n): ModelFamily {\r\n if (hint !== \"auto\") return hint;\r\n const name = clientName.toLowerCase();\r\n if (name.includes(\"claude\") || name.includes(\"anthropic\")) return \"anthropic\";\r\n if (\r\n name.includes(\"codex\") ||\r\n name.includes(\"gpt\") ||\r\n name.includes(\"openai\")\r\n ) {\r\n return \"openai\";\r\n }\r\n // gemini and all others fall through to the generic approximation.\r\n return \"generic\";\r\n}\r\n"],"mappings":";;;;;;;AAiBA,SAAS,gBAAgB,YAAY,oBAAoB;AACzD,SAAS,eAAe;;;ACejB,IAAM,kBAAoD;AAAA,EAC/D,WAAW;AAAA,EACX,oBAAoB;AAAA,EACpB,wBAAwB;AAAA,EACxB,mBAAmB;AAAA,EACnB,eAAe;AACjB;AAQO,SAAS,gBACd,GACA,GACkB;AAClB,SAAO,gBAAgB,CAAC,IAAI,gBAAgB,CAAC,IAAI,IAAI;AACvD;;;ADbA,SAAS,oBAA6B;AACpC,SAAO,QAAQ,IAAI,8BAA8B;AACnD;AAMO,SAAS,YAAY,KAAqB;AAC/C,SAAO,GAAG,KAAK,IAAI,CAAC,IAAI,GAAG;AAC7B;AAEA,IAAM,cAAN,MAA4C;AAAA,EACzB;AAAA,EAEjB,YAAY,MAAc;AACxB,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,OAAO,QAA+B;AACpC,QAAI,kBAAkB,EAAG;AACzB,cAAU,QAAQ,KAAK,IAAI,CAAC;AAC5B,mBAAe,KAAK,MAAM,GAAG,KAAK,UAAU,MAAM,CAAC;AAAA,GAAM,MAAM;AAAA,EACjE;AAAA,EAEA,MAAM,QAAwC;AAC5C,QAAI,CAAC,WAAW,KAAK,IAAI,EAAG,QAAO,CAAC;AAEpC,QAAI;AACJ,QAAI;AACF,aAAO,aAAa,KAAK,MAAM,MAAM;AAAA,IACvC,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,MAAyB,CAAC;AAChC,eAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,YAAY,GAAI;AAEpB,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,OAAO;AAAA,MAC7B,QAAQ;AACN;AAAA,MACF;AAEA,UAAI,QAAQ,QAAQ,MAAM,EAAG,KAAI,KAAK,MAAM;AAAA,IAC9C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,IAAoC,QAAkC;AAC3E,UAAM,SAAS,oBAAI,IAAuB;AAE1C,eAAW,UAAU,KAAK,MAAM,MAAM,GAAG;AACvC,YAAM,MAAM,SAAS,QAAQ,EAAE;AAC/B,YAAM,WAAW,OAAO,IAAI,GAAG;AAC/B,UAAI,aAAa,QAAW;AAC1B,eAAO,IAAI,KAAK;AAAA,UACd;AAAA,UACA,OAAO;AAAA,UACP,aAAa,OAAO;AAAA,UACpB,cAAc,OAAO;AAAA,UACrB,aAAa,OAAO,cAAc,OAAO;AAAA,UACzC,YAAY,OAAO;AAAA,UACnB,QAAQ,OAAO;AAAA,QACjB,CAAC;AAAA,MACH,OAAO;AACL,iBAAS,SAAS;AAClB,iBAAS,eAAe,OAAO;AAC/B,iBAAS,gBAAgB,OAAO;AAChC,iBAAS,eAAe,OAAO,cAAc,OAAO;AACpD,iBAAS,aAAa;AAAA,UACpB,SAAS;AAAA,UACT,OAAO;AAAA,QACT;AACA,YAAI,OAAO,KAAK,SAAS,OAAQ,UAAS,SAAS,OAAO;AAAA,MAC5D;AAAA,IACF;AAEA,WAAO,CAAC,GAAG,OAAO,OAAO,CAAC;AAAA,EAC5B;AAAA,EAEA,QAAc;AAAA,EAEd;AACF;AAGA,SAAS,QAAQ,QAAyB,QAA8B;AACtE,MACE,OAAO,gBAAgB,UACvB,OAAO,gBAAgB,OAAO,aAC9B;AACA,WAAO;AAAA,EACT;AACA,MACE,OAAO,eAAe,UACtB,OAAO,eAAe,OAAO,YAC7B;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,cAAc,UAAa,OAAO,cAAc,OAAO,WAAW;AAC3E,WAAO;AAAA,EACT;AACA,MAAI,OAAO,aAAa,UAAa,OAAO,aAAa,OAAO,UAAU;AACxE,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,UAAa,OAAO,KAAK,OAAO,SAAS;AAC9D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,SAAS,SACP,QACA,IACQ;AACR,UAAQ,IAAI;AAAA,IACV,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,EAClB;AACF;AAOO,SAAS,UAAU,MAAyC;AACjE,QAAM,OAAO,MAAM,QAAQ,cAAc,QAAQ;AACjD,SAAO,IAAI,YAAY,IAAI;AAC7B;;;AE/JA,SAAS,qBAAqB;AAgB9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAahD,IAAI;AAGJ,IAAM,4BAA4B;AASlC,SAAS,cAAqC;AAC5C,MAAI,iBAAiB,OAAW,QAAO;AACvC,MAAI;AAIF,UAAM,MAAM,WAAW,mCAAmC;AAG1D,QAAI,OAAO,OAAO,IAAI,WAAW,YAAY;AAC3C,qBAAe,EAAE,QAAQ,IAAI,OAAO,KAAK,GAAG,EAAE;AAAA,IAChD,OAAO;AACL,qBAAe;AAAA,IACjB;AAAA,EACF,QAAQ;AACN,mBAAe;AAAA,EACjB;AACA,SAAO;AACT;AAGA,SAAS,eAAe,MAA0B;AAChD,SAAO;AAAA,IACL,QAAQ,KAAK,KAAK,KAAK,SAAS,yBAAyB;AAAA,IACzD,QAAQ;AAAA,EACV;AACF;AAMA,SAAS,UAAU,QAAuC;AACxD,SAAO,WAAW,WAAW,oBAAoB;AACnD;AAMA,IAAM,mBAAN,MAA4C;AAAA,EAC1C,MAAM,MAAc,QAAiC;AACnD,UAAM,UAAU,YAAY;AAC5B,QAAI,CAAC,QAAS,QAAO,eAAe,IAAI;AACxC,QAAI;AACF,aAAO,EAAE,QAAQ,QAAQ,OAAO,IAAI,EAAE,QAAQ,QAAQ,UAAU,MAAM,EAAE;AAAA,IAC1E,QAAQ;AAGN,aAAO,eAAe,IAAI;AAAA,IAC5B;AAAA,EACF;AAAA,EAEA,WAAW,OAAgB,QAAiC;AAC1D,WAAO,KAAK,MAAM,gBAAgB,KAAK,GAAG,MAAM;AAAA,EAClD;AACF;AAOA,SAAS,gBAAgB,OAAwB;AAC/C,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,OAAO,KAAK,UAAU,KAAK;AACjC,SAAO,QAAQ;AACjB;AAEA,IAAM,mBAA8B,IAAI,iBAAiB;AAGlD,SAAS,eAA0B;AACxC,SAAO;AACT;AASO,SAAS,iBACd,YACA,MACa;AACb,MAAI,SAAS,OAAQ,QAAO;AAC5B,QAAM,OAAO,WAAW,YAAY;AACpC,MAAI,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,WAAW,EAAG,QAAO;AAClE,MACE,KAAK,SAAS,OAAO,KACrB,KAAK,SAAS,KAAK,KACnB,KAAK,SAAS,QAAQ,GACtB;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/config-patch-ledger.ts"],"sourcesContent":["/**\r\n * core/config-patch-ledger — the persisted, refcounted ownership ledger behind\r\n * the declarative `configPatch` surface, plus the shared configPatch helpers\r\n * (leaf-path grammar, JSON-value checks, the agent-connector namespace guard,\r\n * canonical hashing, and the manual-edit hint every skip-warn prints).\r\n *\r\n * WHY A LEDGER (and not the hooks-style recompute ownership): hook entries\r\n * embed the connector id in their command string, so uninstall can identify\r\n * \"mine\" by inspection. configPatch writes bare host keys (`true` is `true`) —\r\n * not attributable by inspection — and the same key may be needed by SEVERAL\r\n * connectors (a shared host feature flag). Without a persisted refcount,\r\n * connector A uninstalling would delete a key connector B still relies on.\r\n *\r\n * Contract (docs/ARCHITECTURE.md §4 \"configPatch\"):\r\n * • One global file at `<dataRoot>/state/config-patches.json`, written\r\n * ATOMICALLY (temp + rename) — shared across connectors because\r\n * refcounting requires a global view; survives a connector's record purge.\r\n * • Primary key per entry: (platform, file, key). `prior` is ALWAYS\r\n * `{ present: false }` — ownership only ever attaches to keys\r\n * agent-connector itself created (set-if-absent), so the blind\r\n * \"restore prior value\" footgun is structurally impossible.\r\n * • Uninstall removes a key ONLY when the releasing connector is the LAST\r\n * owner AND the current value still deep-equals `writtenValue` AND prior\r\n * was absent; any mismatch → skip-warn and leave the key in place.\r\n * • The ledger is advisory FOR DELETION ONLY: the worst desync outcome is a\r\n * skip-warn and a leftover key, never data loss.\r\n */\r\n\r\nimport { createHash, randomBytes } from \"node:crypto\";\r\nimport {\r\n existsSync,\r\n readFileSync,\r\n renameSync,\r\n rmSync,\r\n writeFileSync,\r\n} from \"node:fs\";\r\nimport { dirname, join } from \"node:path\";\r\n\r\nimport type { ConfigPatchDef, JsonValue, PlatformId } from \"./types.js\";\r\nimport { assertNoSymlinkInPath, ensureDir } from \"./paths.js\";\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Shared configPatch helpers (grammar / values / namespace guard / hints)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** Grammar for ONE dotted-path segment: no dots-in-key, no array indices. */\r\nexport const CONFIG_PATCH_SEGMENT_RE = /^[A-Za-z0-9_-]+$/;\r\n\r\n/**\r\n * True when `key` is a valid dotted LEAF path: 1+ segments joined by \".\",\r\n * every segment matching {@link CONFIG_PATCH_SEGMENT_RE}.\r\n */\r\nexport function isValidConfigPatchKey(key: unknown): key is string {\r\n if (typeof key !== \"string\" || key.length === 0) return false;\r\n return key.split(\".\").every((seg) => CONFIG_PATCH_SEGMENT_RE.test(seg));\r\n}\r\n\r\n/**\r\n * True when `value` is JSON-serializable data (string / finite number /\r\n * boolean / null / arrays / plain objects thereof). Rejects undefined,\r\n * functions, symbols, bigints, NaN/Infinity (JSON.stringify silently corrupts\r\n * those), and class instances other than plain objects/arrays.\r\n */\r\nexport function isJsonValue(value: unknown): value is JsonValue {\r\n if (value === null) return true;\r\n switch (typeof value) {\r\n case \"string\":\r\n case \"boolean\":\r\n return true;\r\n case \"number\":\r\n return Number.isFinite(value);\r\n case \"object\":\r\n break;\r\n default:\r\n return false;\r\n }\r\n if (Array.isArray(value)) return value.every((v) => isJsonValue(v));\r\n const proto = Object.getPrototypeOf(value);\r\n if (proto !== Object.prototype && proto !== null) return false;\r\n return Object.values(value as Record<string, unknown>).every((v) => isJsonValue(v));\r\n}\r\n\r\n/**\r\n * GUARDED NAMESPACE: keys agent-connector already models are NOT patchable —\r\n * the proper surface must be used instead (otherwise a connector could smuggle\r\n * hooks past the nativeHooks modeling/reporting, or fight the server install).\r\n * Returns a human-readable violation pointing at the proper surface, or null.\r\n * Checked at defineConnector (statically knowable) AND re-enforced in the\r\n * adapter (meta-loaded connectors must not bypass it).\r\n */\r\nexport function configPatchNamespaceViolation(key: string): string | null {\r\n const head = key.split(\".\")[0] ?? \"\";\r\n if (head === \"hooks\") {\r\n return `\"${key}\" targets the hooks config agent-connector already models; declare it via \\`hooks\\` (normalized) or \\`platforms.<id>.nativeHooks\\` instead`;\r\n }\r\n if (\r\n head === \"mcpServers\" ||\r\n head === \"enableAllProjectMcpServers\" ||\r\n head === \"enabledMcpjsonServers\" ||\r\n head === \"disabledMcpjsonServers\"\r\n ) {\r\n return `\"${key}\" targets MCP server registration agent-connector already models; declare it via \\`server\\` / \\`platforms.<id>.server\\` / \\`extra\\` instead`;\r\n }\r\n if (head === \"statusLine\") {\r\n return `\"${key}\" is modeled by the statusline surface; declare it via \\`statusline: { render }\\` instead`;\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * The exact manual edit a user can perform when a patch is skipped (conflict,\r\n * drift, unsupported host, denylist…). Printed in every skip-warn so a single\r\n * declaration doubles as its own documented manual step.\r\n */\r\nexport function configPatchManualEdit(patch: ConfigPatchDef): string {\r\n const docs = patch.docsUrl ? ` — see ${patch.docsUrl}` : \"\";\r\n return (\r\n `manual edit if wanted: set ${patch.key} = ${describeJsonValue(patch.value)} ` +\r\n `(${patch.reason})${docs}`\r\n );\r\n}\r\n\r\n/** Compact single-line JSON rendering for diffs/warnings (\"<absent>\" for undefined). */\r\nexport function describeJsonValue(value: JsonValue | undefined): string {\r\n if (value === undefined) return \"<absent>\";\r\n try {\r\n return JSON.stringify(value);\r\n } catch {\r\n return String(value);\r\n }\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Canonical JSON / deep equality\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** Canonical (sorted-keys) JSON of a value — the hash/equality basis. */\r\nexport function canonicalJson(value: JsonValue | undefined): string {\r\n if (value === undefined) return \"undefined\";\r\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\r\n if (Array.isArray(value)) {\r\n return `[${value.map((v) => canonicalJson(v)).join(\",\")}]`;\r\n }\r\n const keys = Object.keys(value).sort();\r\n const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`);\r\n return `{${parts.join(\",\")}}`;\r\n}\r\n\r\n/** sha256 of the canonical JSON — the fast path; deep-equal is the authority. */\r\nexport function hashJsonValue(value: JsonValue): string {\r\n return createHash(\"sha256\").update(canonicalJson(value)).digest(\"hex\");\r\n}\r\n\r\n/** Order-insensitive (object keys) deep equality over JSON data. */\r\nexport function jsonDeepEquals(\r\n a: JsonValue | undefined,\r\n b: JsonValue | undefined,\r\n): boolean {\r\n return canonicalJson(a) === canonicalJson(b);\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Ledger shape + persistence\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** One connector's registration on a ledger entry. */\r\nexport interface ConfigPatchOwner {\r\n connectorId: string;\r\n connectorVersion: string;\r\n installedAt: string;\r\n}\r\n\r\n/** Refcounted ownership record for one (platform, file, key). */\r\nexport interface ConfigPatchLedgerEntry {\r\n platform: PlatformId;\r\n /** Absolute path of the file actually written (scope-resolved at install time). */\r\n file: string;\r\n /** Dotted leaf path — (platform, file, key) is the primary key. */\r\n key: string;\r\n writtenValue: JsonValue;\r\n /** sha256 of canonical (sorted-keys) JSON of writtenValue (fast path only). */\r\n writtenValueHash: string;\r\n /** Always { present: false } — ownership ONLY attaches to keys we created. */\r\n prior: { present: false };\r\n owners: ConfigPatchOwner[];\r\n}\r\n\r\nexport interface ConfigPatchLedger {\r\n version: 1;\r\n entries: ConfigPatchLedgerEntry[];\r\n}\r\n\r\n/** Ledger file path under the framework data-root. */\r\nexport function configPatchLedgerPath(dataRoot: string): string {\r\n return join(dataRoot, \"state\", \"config-patches.json\");\r\n}\r\n\r\n/**\r\n * Load the ledger; a missing or corrupt file degrades to an empty ledger\r\n * (advisory-for-deletion-only: a lost ledger can never cause data loss, only\r\n * retained keys + skip-warns).\r\n */\r\nexport function loadConfigPatchLedger(dataRoot: string): ConfigPatchLedger {\r\n const path = configPatchLedgerPath(dataRoot);\r\n if (!existsSync(path)) return { version: 1, entries: [] };\r\n try {\r\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as ConfigPatchLedger;\r\n if (!parsed || !Array.isArray(parsed.entries)) return { version: 1, entries: [] };\r\n return { version: 1, entries: parsed.entries.filter(isPlausibleEntry) };\r\n } catch {\r\n return { version: 1, entries: [] };\r\n }\r\n}\r\n\r\n/** Minimal shape sanity for a persisted entry (corrupt rows are dropped). */\r\nfunction isPlausibleEntry(e: unknown): e is ConfigPatchLedgerEntry {\r\n if (e == null || typeof e !== \"object\") return false;\r\n const entry = e as Partial<ConfigPatchLedgerEntry>;\r\n return (\r\n typeof entry.platform === \"string\" &&\r\n typeof entry.file === \"string\" &&\r\n typeof entry.key === \"string\" &&\r\n Array.isArray(entry.owners)\r\n );\r\n}\r\n\r\n/**\r\n * Persist the ledger ATOMICALLY: write to a temp file in the same directory,\r\n * then rename over the target (rename is atomic on the same filesystem).\r\n */\r\nexport function saveConfigPatchLedger(dataRoot: string, ledger: ConfigPatchLedger): void {\r\n const path = configPatchLedgerPath(dataRoot);\r\n assertNoSymlinkInPath(path);\r\n ensureDir(dirname(path));\r\n const tmpFile = `${path}.${process.pid}-${randomBytes(4).toString(\"hex\")}.tmp`;\r\n try {\r\n writeFileSync(tmpFile, `${JSON.stringify(ledger, null, 2)}\\n`, \"utf8\");\r\n renameSync(tmpFile, path);\r\n } catch (err) {\r\n rmSync(tmpFile, { force: true });\r\n throw err;\r\n }\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Ledger operations (pure in-memory; callers load → mutate → save)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** Find the entry for (platform, file, key), or undefined. */\r\nexport function findLedgerEntry(\r\n ledger: ConfigPatchLedger,\r\n platform: PlatformId,\r\n file: string,\r\n key: string,\r\n): ConfigPatchLedgerEntry | undefined {\r\n return ledger.entries.find(\r\n (e) => e.platform === platform && e.file === file && e.key === key,\r\n );\r\n}\r\n\r\n/** Every entry on `platform` owned (at least in part) by `connectorId`. */\r\nexport function ledgerEntriesOwnedBy(\r\n ledger: ConfigPatchLedger,\r\n platform: PlatformId,\r\n connectorId: string,\r\n): ConfigPatchLedgerEntry[] {\r\n return ledger.entries.filter(\r\n (e) => e.platform === platform && e.owners.some((o) => o.connectorId === connectorId),\r\n );\r\n}\r\n\r\n/** Create a fresh entry (prior is absent BY CONSTRUCTION) and append it. */\r\nexport function createLedgerEntry(\r\n ledger: ConfigPatchLedger,\r\n init: {\r\n platform: PlatformId;\r\n file: string;\r\n key: string;\r\n value: JsonValue;\r\n connectorId: string;\r\n connectorVersion: string;\r\n },\r\n): ConfigPatchLedgerEntry {\r\n const entry: ConfigPatchLedgerEntry = {\r\n platform: init.platform,\r\n file: init.file,\r\n key: init.key,\r\n writtenValue: init.value,\r\n writtenValueHash: hashJsonValue(init.value),\r\n prior: { present: false },\r\n owners: [\r\n {\r\n connectorId: init.connectorId,\r\n connectorVersion: init.connectorVersion,\r\n installedAt: new Date().toISOString(),\r\n },\r\n ],\r\n };\r\n ledger.entries.push(entry);\r\n return entry;\r\n}\r\n\r\n/**\r\n * Register `connectorId` as a (co-)owner. Returns true when added, false when\r\n * it was already an owner (idempotent re-install → no ledger mutation).\r\n */\r\nexport function addLedgerOwner(\r\n entry: ConfigPatchLedgerEntry,\r\n connectorId: string,\r\n connectorVersion: string,\r\n): boolean {\r\n if (entry.owners.some((o) => o.connectorId === connectorId)) return false;\r\n entry.owners.push({\r\n connectorId,\r\n connectorVersion,\r\n installedAt: new Date().toISOString(),\r\n });\r\n return true;\r\n}\r\n\r\n/**\r\n * Remove `connectorId` from the owners. Returns whether an owner was removed\r\n * and whether the entry is now ownerless (last owner out).\r\n */\r\nexport function removeLedgerOwner(\r\n entry: ConfigPatchLedgerEntry,\r\n connectorId: string,\r\n): { removed: boolean; lastOwner: boolean } {\r\n const before = entry.owners.length;\r\n entry.owners = entry.owners.filter((o) => o.connectorId !== connectorId);\r\n return { removed: entry.owners.length < before, lastOwner: entry.owners.length === 0 };\r\n}\r\n\r\n/** Drop an entry from the ledger (identity match). */\r\nexport function dropLedgerEntry(\r\n ledger: ConfigPatchLedger,\r\n entry: ConfigPatchLedgerEntry,\r\n): void {\r\n ledger.entries = ledger.entries.filter((e) => e !== entry);\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/usage/paths.ts"],"sourcesContent":["/**\r\n * usage/paths — per-platform HOST storage-root resolution + small fs walkers.\r\n *\r\n * This is the read-only complement to core/paths.ts: where that module resolves\r\n * the FRAMEWORK data-root (~/.agent-connector), this one resolves each agent\r\n * CLI's OWN native storage roots (~/.qwen, ~/.claude, …) so the usage readers\r\n * can parse them. It NEVER writes; it only enumerates files.\r\n *\r\n * Ported from tokscale paths.rs (the tokscale cache dir) and from each reader's\r\n * storage-path spec (docs/research/usage-readers.json). Resolution order is\r\n * uniform across platforms:\r\n * 1. an explicit AGENT_CONNECTOR_<PLATFORM>_DIR env override (verbatim, when\r\n * non-empty — an empty string is treated as unset so we never resolve to \"\");\r\n * 2. the platform-specific default for the current OS.\r\n *\r\n * The \"tokscale cache dir\" (~/.config/tokscale, or AGENT_CONNECTOR_TOKSCALE_DIR)\r\n * is where synced readers (cursor/antigravity/antigravity-cli/trae/warp) look for\r\n * a local cache that a separate tokscale run may have produced — we read it if\r\n * present, but we never create or sync it. (Antigravity's own native store is\r\n * protobuf .pb with no public schema, so it too is synced-cache-or-skip.)\r\n */\r\n\r\nimport {\r\n existsSync,\r\n readdirSync,\r\n statSync,\r\n} from \"node:fs\";\r\nimport { homedir } from \"node:os\";\r\nimport { isAbsolute, join, resolve } from \"node:path\";\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Home / env helpers\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** Expand a leading \"~\" (and \"~/\") to the user's home directory. */\r\nexport function expandHome(p: string): string {\r\n if (p === \"~\") return homedir();\r\n if (p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\r\n return join(homedir(), p.slice(2));\r\n }\r\n return p;\r\n}\r\n\r\n/**\r\n * Read an env override, treating empty/blank as unset (mirrors tokscale's\r\n * is_config_dir_overridden contract: an empty override must NOT resolve to \"\").\r\n * Relative paths are resolved against the process CWD; \"~\" is expanded.\r\n */\r\nfunction envOverride(name: string): string | undefined {\r\n const raw = process.env[name];\r\n if (raw == null || raw.trim() === \"\") return undefined;\r\n const expanded = expandHome(raw.trim());\r\n return isAbsolute(expanded) ? expanded : resolve(expanded);\r\n}\r\n\r\n/** $XDG_DATA_HOME (when set & non-empty) else ~/.local/share. */\r\nfunction xdgDataHome(): string {\r\n const xdg = process.env.XDG_DATA_HOME;\r\n if (xdg && xdg.trim() !== \"\") return resolve(expandHome(xdg.trim()));\r\n return join(homedir(), \".local\", \"share\");\r\n}\r\n\r\n/** $XDG_CONFIG_HOME (when set & non-empty) else ~/.config. */\r\nfunction xdgConfigHome(): string {\r\n const xdg = process.env.XDG_CONFIG_HOME;\r\n if (xdg && xdg.trim() !== \"\") return resolve(expandHome(xdg.trim()));\r\n return join(homedir(), \".config\");\r\n}\r\n\r\n/** Windows %LOCALAPPDATA% else ~/AppData/Local. */\r\nfunction localAppData(): string {\r\n const v = process.env.LOCALAPPDATA;\r\n if (v && v.trim() !== \"\") return resolve(v.trim());\r\n return join(homedir(), \"AppData\", \"Local\");\r\n}\r\n\r\nconst isMac = process.platform === \"darwin\";\r\nconst isWin = process.platform === \"win32\";\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// tokscale cache dir (for synced readers)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Resolve the tokscale config dir (where synced caches live). Ported from\r\n * tokscale paths.rs get_config_dir():\r\n * 1. AGENT_CONNECTOR_TOKSCALE_DIR override (verbatim, non-empty);\r\n * 2. macOS: $HOME/.config/tokscale (NOT ~/Library/Application Support);\r\n * 3. Linux: $XDG_CONFIG_HOME/tokscale or ~/.config/tokscale;\r\n * 4. Windows: %APPDATA%/tokscale (config_dir equivalent) else ~/.config/tokscale.\r\n *\r\n * We use AGENT_CONNECTOR_TOKSCALE_DIR (not TOKSCALE_CONFIG_DIR) so the framework\r\n * never accidentally couples to a tokscale install's hermetic test env.\r\n */\r\nexport function tokscaleConfigDir(): string {\r\n const override = envOverride(\"AGENT_CONNECTOR_TOKSCALE_DIR\");\r\n if (override) return override;\r\n if (isMac) return join(homedir(), \".config\", \"tokscale\");\r\n if (isWin) {\r\n const appData = process.env.APPDATA;\r\n if (appData && appData.trim() !== \"\") return join(resolve(appData.trim()), \"tokscale\");\r\n return join(homedir(), \".config\", \"tokscale\");\r\n }\r\n return join(xdgConfigHome(), \"tokscale\");\r\n}\r\n\r\n/** The directory a synced platform's local cache lives under, e.g. `<tokscale>/cursor-cache`. */\r\nexport function tokscaleCacheDir(name: string): string {\r\n return join(tokscaleConfigDir(), name);\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Antigravity native store roots (DETECTION ONLY — not parsed for usage)\r\n//\r\n// CONFIRMED-BY-INSTALL (2026-06-03, docs/research/antigravity-paths-confirmed.md):\r\n// the native Antigravity store is `~/.gemini/antigravity/conversations/<uuid>.pb`\r\n// — PROTOBUF with no public schema — and `brain/<uuid>/` holds only media +\r\n// `*.metadata.json`. There are NO `transcript*.jsonl` files. The `agy` CLI shares\r\n// this SAME `~/.gemini/antigravity/` dir (no separate config/storage dir). So the\r\n// native dir is used only for platform DETECTION (in the adapters); the usage\r\n// readers do NOT parse it (protobuf, unreadable) — they are SYNCED readers that\r\n// read the tokscale synced-cache instead (see hostRoots → antigravity-cache).\r\n//\r\n// These helpers are retained for any detection caller and honor the standard\r\n// AGENT_CONNECTOR_<PLATFORM>_DIR override (prepended, preferred when set).\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * The native Antigravity store root for DETECTION: `~/.gemini/antigravity/`\r\n * (CONFIRMED). Its `conversations/<uuid>.pb` payloads are protobuf with no public\r\n * schema, so this dir is NOT parsed for usage — only its presence is a signal.\r\n *\r\n * Honors AGENT_CONNECTOR_ANTIGRAVITY_DIR (verbatim, when set) as the override.\r\n */\r\nexport function antigravityNativeRoots(): string[] {\r\n const out: string[] = [];\r\n const override = envOverride(\"AGENT_CONNECTOR_ANTIGRAVITY_DIR\");\r\n if (override) out.push(override);\r\n out.push(join(homedir(), \".gemini\", \"antigravity\"));\r\n return out;\r\n}\r\n\r\n/**\r\n * The native store root for the Antigravity CLI (`agy`) for DETECTION. CONFIRMED:\r\n * `agy` has NO separate dir — it SHARES the IDE's `~/.gemini/antigravity/` (also\r\n * protobuf, not parsed for usage).\r\n *\r\n * Honors AGENT_CONNECTOR_ANTIGRAVITY_CLI_DIR (verbatim, when set) as the override.\r\n */\r\nexport function antigravityCliNativeRoots(): string[] {\r\n const out: string[] = [];\r\n const override = envOverride(\"AGENT_CONNECTOR_ANTIGRAVITY_CLI_DIR\");\r\n if (override) out.push(override);\r\n out.push(join(homedir(), \".gemini\", \"antigravity\"));\r\n return out;\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Per-platform host roots\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Candidate host storage roots for a platform, most-preferred first. Readers\r\n * iterate the returned list and use the first that exists (each variant is the\r\n * OS-correct default; the env override, when present, is prepended).\r\n *\r\n * Only the platforms this subsystem currently ships a reader for are listed in\r\n * detail; the table is structured so a new platform is one new case. Returning\r\n * `[]` (no candidate root) makes a reader fail-open to zero records.\r\n */\r\nexport function hostRoots(platformId: string): string[] {\r\n const out: string[] = [];\r\n const override = envOverride(`AGENT_CONNECTOR_${platformId.toUpperCase().replace(/-/g, \"_\")}_DIR`);\r\n if (override) out.push(override);\r\n\r\n switch (platformId) {\r\n case \"qwen-code\":\r\n // ~/.qwen/projects/*/chats/*.jsonl\r\n out.push(join(homedir(), \".qwen\", \"projects\"));\r\n break;\r\n case \"claude-code\":\r\n out.push(join(homedir(), \".claude\", \"projects\"));\r\n break;\r\n case \"codex\":\r\n out.push(join(homedir(), \".codex\", \"sessions\"));\r\n break;\r\n case \"gemini-cli\":\r\n out.push(join(homedir(), \".gemini\", \"tmp\"));\r\n break;\r\n case \"pi\":\r\n out.push(join(homedir(), \".pi\", \"agent\", \"sessions\"));\r\n break;\r\n case \"kimi\":\r\n // TWO Moonshot products share the \"kimi\" id with DIFFERENT session trees:\r\n // - Kimi Code (newer, @moonshot-ai/kimi-code): ~/.kimi-code/sessions/...\r\n // (deeper tree, see readers/kimi.ts) — the product our adapter targets.\r\n // - Kimi CLI (older): ~/.kimi/sessions/<GROUP>/<UUID>/wire.jsonl\r\n // Both are scanned (Kimi Code preferred first). $KIMI_CODE_HOME relocates\r\n // the Kimi Code home and is honored ahead of these in readers/kimi.ts.\r\n out.push(join(homedir(), \".kimi-code\", \"sessions\"));\r\n out.push(join(homedir(), \".kimi\", \"sessions\"));\r\n break;\r\n case \"copilot-cli\":\r\n out.push(join(xdgDataHome(), \"Copilot\", \"telemetry\"));\r\n out.push(join(xdgConfigHome(), \"copilot\", \"telemetry\"));\r\n break;\r\n case \"amp\":\r\n out.push(join(xdgDataHome(), \"amp\", \"threads\"));\r\n break;\r\n case \"droid\":\r\n out.push(join(homedir(), \".factory\", \"sessions\"));\r\n break;\r\n case \"mux\":\r\n out.push(join(homedir(), \".mux\", \"sessions\"));\r\n break;\r\n case \"kiro\":\r\n out.push(join(homedir(), \".kiro\", \"sessions\", \"cli\"));\r\n break;\r\n case \"opencode\":\r\n out.push(join(xdgDataHome(), \"opencode\", \"opencode.db\"));\r\n break;\r\n case \"goose\":\r\n if (isMac) out.push(join(homedir(), \"Library\", \"Application Support\", \"goose\", \"sessions\", \"sessions.db\"));\r\n out.push(join(xdgDataHome(), \"goose\", \"sessions\", \"sessions.db\"));\r\n out.push(join(xdgDataHome(), \"Block\", \"goose\", \"sessions\", \"sessions.db\"));\r\n break;\r\n case \"hermes\":\r\n out.push(join(homedir(), \".hermes\", \"state.db\"));\r\n break;\r\n case \"kilo-cli\":\r\n out.push(join(xdgDataHome(), \"kilo\", \"kilo.db\"));\r\n break;\r\n case \"crush\":\r\n out.push(join(homedir(), \".cache\", \"crush\", \"crush.db\"));\r\n break;\r\n case \"zed\":\r\n if (isMac) out.push(join(homedir(), \"Library\", \"Application Support\", \"Zed\", \"threads\", \"threads.db\"));\r\n else if (isWin) out.push(join(localAppData(), \"Zed\", \"threads\", \"threads.db\"));\r\n else out.push(join(xdgDataHome(), \"zed\", \"threads\", \"threads.db\"));\r\n break;\r\n // Synced platforms: local cache under the tokscale config dir.\r\n case \"cursor\":\r\n out.push(tokscaleCacheDir(\"cursor-cache\"));\r\n break;\r\n case \"antigravity\":\r\n // SYNCED: the native store is protobuf (.pb, no public schema) and NOT\r\n // parseable, so the reader reads only the tokscale synced-cache mirror if a\r\n // separate tokscale run produced one (else [] → \"requires sync\"). The\r\n // native `~/.gemini/antigravity/` dir is used only for detection.\r\n out.push(tokscaleCacheDir(\"antigravity-cache\"));\r\n break;\r\n case \"antigravity-cli\":\r\n // SYNCED: `agy` shares the IDE's protobuf (.pb) store (no schema), so the\r\n // reader reads only the tokscale synced-cache mirror if present (else [] →\r\n // \"requires sync\"). Shares the IDE's antigravity-cache name (same data).\r\n out.push(tokscaleCacheDir(\"antigravity-cache\"));\r\n break;\r\n case \"trae\":\r\n out.push(tokscaleCacheDir(\"trae-cache\"));\r\n break;\r\n case \"warp\":\r\n out.push(tokscaleCacheDir(\"warp-cache\"));\r\n break;\r\n default:\r\n break;\r\n }\r\n return out;\r\n}\r\n\r\n/** First existing host root for a platform, or undefined when none is present. */\r\nexport function firstExistingRoot(platformId: string): string | undefined {\r\n for (const root of hostRoots(platformId)) {\r\n if (existsSync(root)) return root;\r\n }\r\n return undefined;\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// Directory / glob walkers (tolerant — never throw)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** Is the path an existing directory? Tolerant (false on any stat error). */\r\nexport function isDir(p: string): boolean {\r\n try {\r\n return statSync(p).isDirectory();\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/** Is the path an existing regular file? Tolerant (false on any stat error). */\r\nexport function isFile(p: string): boolean {\r\n try {\r\n return statSync(p).isFile();\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Recursively list every file under `root` whose name matches `predicate`.\r\n * Tolerant: an unreadable directory is skipped, never thrown. Returns absolute\r\n * paths in directory-walk order. A non-existent root yields [].\r\n */\r\nexport function walkFiles(\r\n root: string,\r\n predicate: (name: string, absPath: string) => boolean,\r\n maxDepth = 12,\r\n): string[] {\r\n const out: string[] = [];\r\n if (!isDir(root)) return out;\r\n\r\n const stack: Array<{ dir: string; depth: number }> = [{ dir: root, depth: 0 }];\r\n while (stack.length > 0) {\r\n const { dir, depth } = stack.pop() as { dir: string; depth: number };\r\n let entries: import(\"node:fs\").Dirent[];\r\n try {\r\n entries = readdirSync(dir, { withFileTypes: true });\r\n } catch {\r\n continue; // unreadable dir → skip\r\n }\r\n for (const entry of entries) {\r\n const abs = join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n if (depth < maxDepth) stack.push({ dir: abs, depth: depth + 1 });\r\n } else if (entry.isFile() && predicate(entry.name, abs)) {\r\n out.push(abs);\r\n }\r\n }\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * List immediate subdirectory names of `dir` (not recursive). Tolerant: returns\r\n * [] for a missing/unreadable directory.\r\n */\r\nexport function listSubdirs(dir: string): string[] {\r\n if (!isDir(dir)) return [];\r\n try {\r\n return readdirSync(dir, { withFileTypes: true })\r\n .filter((e) => e.isDirectory())\r\n .map((e) => join(dir, e.name));\r\n } catch {\r\n return [];\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sdk/test-harness.ts"],"sourcesContent":["/**\r\n * sdk/test-harness — the SHIPPED, OFFLINE behavioral harness.\r\n *\r\n * Two read-only tools a connector author runs WITHOUT installing anything or\r\n * touching the host's filesystem:\r\n *\r\n * • `explain(connector)` — the static per-host × per-DECLARED-surface matrix:\r\n * for every registered host and every surface the connector actually ships,\r\n * is it `native` (the host can honor it), `disabled` (the connector opted\r\n * out for that host via `platforms[host].<surface> === false`), or\r\n * `skip-warn` (the host cannot honor it → install would skip-warn)?\r\n *\r\n * • `simulate(connector, opts)` — runs the REAL adapter parse → handler →\r\n * format chain INLINE against a host-shaped raw payload and reports whether\r\n * the host actually HONORS the connector's response (the trustworthy part:\r\n * it surfaces the documented degradations, e.g. Codex silently dropping a\r\n * UserPromptSubmit \"context\" injection that has no stdout path).\r\n *\r\n * It deliberately does NOT import the runtime entrypoints (runHook /\r\n * runStatusline) — those load the REGISTERED connector from disk and record\r\n * telemetry. The harness instead re-implements the tiny parse→handler→format\r\n * chain against the in-memory connector + the adapter, mirroring the runtime's\r\n * input tolerance (empty/malformed payload → `{}`) and its fail-safe envelope.\r\n */\r\n\r\nimport type {\r\n HookEventName,\r\n HookResponse,\r\n PlatformCapabilities,\r\n PlatformId,\r\n PlatformOverride,\r\n ResolvedConnector,\r\n} from \"../core/types.js\";\r\nimport { allAdapters, loadAdapter } from \"../adapters/registry.js\";\r\nimport type { NormalizedEvent } from \"../adapters/spi.js\";\r\nimport {\r\n compileMatcher,\r\n eventMatcherSubject,\r\n} from \"../runtime/hook-entrypoint.js\";\r\nimport type { SurfaceName } from \"./introspect.js\";\r\nimport { hostCanFireEvent, SURFACE_PREDICATES } from \"./introspect.js\";\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// explain — the static per-host × per-declared-surface matrix\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** One cell of the {@link explain} matrix: a host × a declared surface. */\r\nexport interface ExplainRow {\r\n host: PlatformId;\r\n surface: SurfaceName;\r\n /**\r\n * - \"native\" — the host's capabilities pass the surface predicate.\r\n * - \"disabled\" — the connector opted out for this host\r\n * (`platforms[host].<surface> === false`).\r\n * - \"skip-warn\" — the host cannot honor it; install would skip-warn.\r\n */\r\n support: \"native\" | \"skip-warn\" | \"disabled\";\r\n reason: string;\r\n}\r\n\r\n/**\r\n * The GLOBAL surfaces a connector declares (these apply to every host — only\r\n * declared surfaces get a matrix row). The two platform-scoped surfaces\r\n * (`configPatch` / `nativeHooks`) are deliberately EXCLUDED here: they are\r\n * declared under one specific `platforms[host]`, so each is added PER HOST in\r\n * {@link explain} (see {@link platformScopedSurfacesFor}) — adding them globally\r\n * would falsely promise a configPatch row on hosts that never declared it.\r\n */\r\nfunction declaredGlobalSurfaces(connector: ResolvedConnector): SurfaceName[] {\r\n const out: SurfaceName[] = [];\r\n if (connector.server) out.push(\"server\");\r\n if (Object.keys(connector.hooks).length > 0) out.push(\"hooks\");\r\n if (connector.commands.length > 0) out.push(\"commands\");\r\n if (connector.skills.length > 0) out.push(\"skills\");\r\n if (connector.subagents.length > 0) out.push(\"subagents\");\r\n if (connector.memory.length > 0) out.push(\"memory\");\r\n if (connector.statusline) out.push(\"statusline\");\r\n if (connector.actions.length > 0) out.push(\"actions\");\r\n return out;\r\n}\r\n\r\n/**\r\n * The platform-scoped surfaces (`configPatch` / `nativeHooks`) THIS host\r\n * actually declares — keyed off the host's own `platforms[host]` override, so a\r\n * configPatch declared under claude-code never shows up as a row for codex.\r\n */\r\nfunction platformScopedSurfacesFor(\r\n override: PlatformOverride | undefined,\r\n): SurfaceName[] {\r\n const out: SurfaceName[] = [];\r\n if (!override) return out;\r\n if ((override.configPatch?.length ?? 0) > 0) out.push(\"configPatch\");\r\n if (override.nativeHooks != null && Object.keys(override.nativeHooks).length > 0) {\r\n out.push(\"nativeHooks\");\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Is the surface explicitly DISABLED for this host? Mirrors the per-host opt-out\r\n * the installer honors: a boolean-or-object surface override is \"disabled\" only\r\n * when set to the literal `false` (the object form is a tuning, not an opt-out).\r\n * `configPatch` / `nativeHooks` have no boolean opt-out form — declaring them is\r\n * the opt-in, so they are never \"disabled\".\r\n */\r\nfunction isDisabledForHost(\r\n connector: ResolvedConnector,\r\n host: PlatformId,\r\n surface: SurfaceName,\r\n): boolean {\r\n const override = connector.platforms[host];\r\n if (!override) return false;\r\n switch (surface) {\r\n case \"server\":\r\n return override.server === false;\r\n case \"hooks\":\r\n return override.hooks === false;\r\n case \"commands\":\r\n return override.commands === false;\r\n case \"skills\":\r\n return override.skills === false;\r\n case \"subagents\":\r\n return override.subagents === false;\r\n case \"memory\":\r\n return override.memory === false;\r\n case \"statusline\":\r\n return override.statusline === false;\r\n case \"actions\":\r\n case \"configPatch\":\r\n case \"nativeHooks\":\r\n // No per-host boolean opt-out form (declaring them is the opt-in).\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * The static support matrix: every registered host × every surface the\r\n * connector declares. Rows are sorted by host then surface for stable output.\r\n * Pure read of the adapter capabilities — installs nothing, writes nothing.\r\n */\r\nexport async function explain(connector: ResolvedConnector): Promise<ExplainRow[]> {\r\n const globalSurfaces = declaredGlobalSurfaces(connector);\r\n const adapters = await allAdapters();\r\n const rows: ExplainRow[] = [];\r\n\r\n for (const adapter of adapters) {\r\n // Per-host surface set = the global surfaces every host gets a row for, plus\r\n // the platform-scoped surfaces THIS host actually declares (configPatch /\r\n // nativeHooks live under platforms[host], so they are host-local).\r\n const override = connector.platforms[adapter.id];\r\n const surfaces = [\r\n ...globalSurfaces,\r\n ...platformScopedSurfacesFor(override),\r\n ];\r\n for (const surface of surfaces) {\r\n if (isDisabledForHost(connector, adapter.id, surface)) {\r\n rows.push({\r\n host: adapter.id,\r\n surface,\r\n support: \"disabled\",\r\n reason: `connector opted out for ${adapter.id} (platforms.${adapter.id}.${surface} = false)`,\r\n });\r\n continue;\r\n }\r\n // The hooks surface is judged against the connector's SPECIFIC declared\r\n // events, not the coarse \"host has a hook runtime\" predicate — otherwise a\r\n // Stop-only connector falsely shows `native` on a PreToolUse-only host\r\n // (the host CAN fire a hook, just not the one this connector declared).\r\n if (surface === \"hooks\") {\r\n rows.push(hooksRow(connector, adapter.id, adapter.capabilities));\r\n continue;\r\n }\r\n const native = SURFACE_PREDICATES[surface](adapter.capabilities);\r\n rows.push({\r\n host: adapter.id,\r\n surface,\r\n support: native ? \"native\" : \"skip-warn\",\r\n reason: explainReason(adapter.id, surface, native),\r\n });\r\n }\r\n }\r\n\r\n rows.sort((a, b) =>\r\n a.host === b.host ? a.surface.localeCompare(b.surface) : a.host.localeCompare(b.host),\r\n );\r\n return rows;\r\n}\r\n\r\n/**\r\n * The per-DECLARED-event hooks verdict for one host. A connector's hooks surface\r\n * is `native` only when EVERY event it declares can fire on the host; if any\r\n * declared event has no native equivalent the row is `skip-warn` and the reason\r\n * names the specific dead events (the same events install skip-warns). This is\r\n * the fix for the OR-across-all-events false green: a Stop-only connector on a\r\n * host that cannot fire Stop now reports skip-warn, not native.\r\n *\r\n * Reuses {@link hostCanFireEvent} — the single per-event source of truth shared\r\n * with {@link SURFACE_PREDICATES} and simulate's runtime verdict — so explain\r\n * never re-derives the per-event rules.\r\n */\r\nfunction hooksRow(\r\n connector: ResolvedConnector,\r\n host: PlatformId,\r\n capabilities: PlatformCapabilities,\r\n): ExplainRow {\r\n const declared = connector.hookEvents;\r\n const dead = declared.filter((e) => !hostCanFireEvent(capabilities, e));\r\n if (dead.length === 0) {\r\n return {\r\n host,\r\n surface: \"hooks\",\r\n support: \"native\",\r\n reason: `${host} can fire every declared hook event (${declared.join(\", \")})`,\r\n };\r\n }\r\n const fireable = declared.filter((e) => hostCanFireEvent(capabilities, e));\r\n const reason =\r\n fireable.length === 0\r\n ? `${host} cannot fire any declared hook event (${dead.join(\", \")}); install skip-warns each`\r\n : `${host} cannot fire ${dead.join(\", \")} (install skip-warns these); honors ${fireable.join(\", \")}`;\r\n return { host, surface: \"hooks\", support: \"skip-warn\", reason };\r\n}\r\n\r\n/**\r\n * The human-readable reason for a native/skip-warn cell. `server` is a special\r\n * case: the predicate is CAPABILITY-based (the host advertises ≥1 MCP\r\n * transport), but advertising a transport does NOT guarantee a writable\r\n * registration — e.g. jetbrains-copilot advertises stdio yet its installServer\r\n * skip-warns because MCP is managed in IDE settings. So the server reason states\r\n * the verdict is capability-based and registration may be host-managed, rather\r\n * than promising a write. (A precise per-adapter installServer dry-run is a\r\n * Phase-2 enhancement — until then this avoids over-promising.)\r\n */\r\nfunction explainReason(host: PlatformId, surface: SurfaceName, native: boolean): string {\r\n if (surface === \"server\") {\r\n return native\r\n ? `${host} advertises an MCP transport (capability-based; registration may be host-managed)`\r\n : `${host} advertises no MCP transport; install skip-warns`;\r\n }\r\n return native\r\n ? `${host} natively supports ${surface}`\r\n : `${host} cannot honor ${surface}; install skip-warns`;\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// simulate — the real adapter parse → handler → format chain, inline\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/** What to drive through the harness: one host, one surface, one raw payload. */\r\nexport interface SimulateOptions {\r\n surface: \"hooks\" | \"statusline\";\r\n host: PlatformId | string;\r\n /** REQUIRED when `surface === \"hooks\"`: which normalized lifecycle event. */\r\n event?: HookEventName;\r\n /** The host-shaped RAW payload (what the host would pipe in on stdin). */\r\n input: unknown;\r\n}\r\n\r\n/** The harness verdict: did the host actually honor the connector's response? */\r\nexport interface SimulateResult {\r\n /** True when the host's native reply carries the connector's intent. */\r\n honored: boolean;\r\n /** The host-native stdout the adapter would emit (when there is one). */\r\n hostReply?: string;\r\n /** Human-readable why — names the documented degradation when honored:false. */\r\n reason: string;\r\n}\r\n\r\n/**\r\n * Tolerantly coerce the raw payload, mirroring the runtime's parseStdin: a\r\n * string is JSON-parsed (empty/malformed → `{}`); a nullish value → `{}`; any\r\n * other value (already-parsed object) is passed through untouched.\r\n */\r\nfunction coerceInput(input: unknown): unknown {\r\n if (input == null) return {};\r\n if (typeof input === \"string\") {\r\n const trimmed = input.trim();\r\n if (trimmed === \"\") return {};\r\n try {\r\n return JSON.parse(trimmed);\r\n } catch {\r\n return {};\r\n }\r\n }\r\n return input;\r\n}\r\n\r\n/** Normalize a handler's return into a concrete response (void/non-object → `{}`). */\r\nfunction normalizeResponse(value: HookResponse | void): HookResponse {\r\n if (value == null || typeof value !== \"object\") return {};\r\n return value;\r\n}\r\n\r\n/**\r\n * The decoded shape of a host's native hook reply on stdout. Every json-stdio\r\n * adapter writes a SUBSET of these fields (claude-code / codex share the\r\n * `hookSpecificOutput` envelope; Stop-class continuation uses the TOP-LEVEL\r\n * `decision:\"block\"`). We read the DECODED fields — never substring-match the\r\n * serialized JSON, which a path/newline/quote in the payload would break.\r\n */\r\ninterface ParsedReply {\r\n /** Top-level Stop-class continuation: {\"decision\":\"block\",\"reason\"}. */\r\n decision?: unknown;\r\n reason?: unknown;\r\n /** Top-level Claude context channel (no adapter emits it yet; future-proofing). */\r\n systemMessage?: unknown;\r\n hookSpecificOutput?: {\r\n hookEventName?: unknown;\r\n additionalContext?: unknown;\r\n permissionDecision?: unknown;\r\n permissionDecisionReason?: unknown;\r\n updatedInput?: unknown;\r\n /** PermissionRequest nested envelope: decision:{behavior:\"allow\"|\"deny\", updatedInput?}. */\r\n decision?: { behavior?: unknown; updatedInput?: unknown };\r\n };\r\n}\r\n\r\n/**\r\n * Tolerantly decode a reply's stdout into {@link ParsedReply}. Empty / absent /\r\n * unparseable stdout is a legitimate \"no payload\" signal (the adapter wrote a\r\n * bare exit-0) → `undefined`, the honest \"this decision had no stdout path\"\r\n * verdict input.\r\n */\r\nfunction parseReplyStdout(stdout: string | undefined): ParsedReply | undefined {\r\n if (stdout == null || stdout.trim() === \"\") return undefined;\r\n try {\r\n const value = JSON.parse(stdout);\r\n return value != null && typeof value === \"object\"\r\n ? (value as ParsedReply)\r\n : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Structural deep-equal for plain JSON values (objects/arrays/primitives). */\r\nfunction deepEqual(a: unknown, b: unknown): boolean {\r\n if (a === b) return true;\r\n if (typeof a !== typeof b || a == null || b == null) return false;\r\n if (typeof a !== \"object\") return false;\r\n const aArr = Array.isArray(a);\r\n if (aArr !== Array.isArray(b)) return false;\r\n if (aArr) {\r\n const x = a as unknown[];\r\n const y = b as unknown[];\r\n return x.length === y.length && x.every((v, i) => deepEqual(v, y[i]));\r\n }\r\n const x = a as Record<string, unknown>;\r\n const y = b as Record<string, unknown>;\r\n const xk = Object.keys(x);\r\n const yk = Object.keys(y);\r\n return xk.length === yk.length && xk.every((k) => deepEqual(x[k], y[k]));\r\n}\r\n\r\n/**\r\n * Decide whether the host HONORED a decision-bearing response, judging the\r\n * PARSED reply against each adapter's ACTUAL (event, decision) contract — never\r\n * by substring-matching the serialized JSON. `allow`/no-decision is always\r\n * pass-through honored; every other decision is honored only when the decoded\r\n * reply carries that exact intent, and the verdict names the documented\r\n * degradation when it does not.\r\n */\r\nfunction judgeHookHonor(\r\n host: string,\r\n event: HookEventName,\r\n response: HookResponse,\r\n stdout: string | undefined,\r\n): { honored: boolean; reason: string } {\r\n const decision = response.decision;\r\n if (decision === undefined || decision === \"allow\") {\r\n return { honored: true, reason: \"pass-through allow\" };\r\n }\r\n\r\n const reply = parseReplyStdout(stdout);\r\n const hso = reply?.hookSpecificOutput;\r\n\r\n if (decision === \"context\") {\r\n // Honored only when the DECODED additionalContext equals what the handler\r\n // asked to inject (claude-code's hookSpecificOutput.additionalContext, or a\r\n // future top-level systemMessage). codex only emits this on\r\n // SessionStart/PostToolUse — every other event silently drops it.\r\n const want = response.additionalContext;\r\n const got = hso?.additionalContext ?? reply?.systemMessage;\r\n if (reply && got === want) {\r\n return { honored: true, reason: `${host} injects context on ${event}` };\r\n }\r\n return {\r\n honored: false,\r\n reason: `${host} drops context on ${event} (no stdout path)`,\r\n };\r\n }\r\n\r\n if (decision === \"deny\") {\r\n if (!reply) {\r\n return {\r\n honored: false,\r\n reason: `${host} drops deny on ${event} (deny not honored; fails open to allow)`,\r\n };\r\n }\r\n // A real block: PreToolUse's permissionDecision:\"deny\", or the\r\n // PermissionRequest nested decision.behavior:\"deny\".\r\n if (hso?.permissionDecision === \"deny\" || hso?.decision?.behavior === \"deny\") {\r\n return { honored: true, reason: `${host} blocks ${event}` };\r\n }\r\n // Stop-class continuation: top-level decision:\"block\" KEEPS the session /\r\n // subagent running (persistence) — the SEMANTIC OPPOSITE of a block. Honored\r\n // (the reply carried the intent), but the reason must not say \"blocks\".\r\n if (reply.decision === \"block\") {\r\n return {\r\n honored: true,\r\n reason: `${host} continues the session/subagent on ${event} (persistence) — NOT a block`,\r\n };\r\n }\r\n // SubagentStart / PostToolUseFailure DEGRADE a deny to a context note — the\r\n // spawn/failure is not actually blocked.\r\n if (hso?.additionalContext != null) {\r\n return {\r\n honored: false,\r\n reason: `${host} degrades deny to a context note on ${event}; spawn/failure not blocked`,\r\n };\r\n }\r\n return {\r\n honored: false,\r\n reason: `${host} drops deny on ${event} (deny not honored; fails open to allow)`,\r\n };\r\n }\r\n\r\n if (decision === \"ask\") {\r\n if (hso?.permissionDecision === \"ask\") {\r\n return { honored: true, reason: `${host} asks on ${event}` };\r\n }\r\n // On PermissionRequest, ask/void falls through to the host's NATIVE dialog\r\n // (the dialog IS the ask), which the adapter expresses as a bare exit-0.\r\n if (event === \"PermissionRequest\" && !reply) {\r\n return {\r\n honored: true,\r\n reason: `${host} shows the native confirmation dialog on ${event} (the ask)`,\r\n };\r\n }\r\n return {\r\n honored: false,\r\n reason: `${host} drops ask on ${event} (no ask path)`,\r\n };\r\n }\r\n\r\n // decision === \"modify\": honored only when the decoded updatedInput deep-equals\r\n // what the handler asked to write (hookSpecificOutput.updatedInput, or the\r\n // PermissionRequest nested decision.updatedInput).\r\n const wrote = hso?.updatedInput ?? hso?.decision?.updatedInput;\r\n if (reply && wrote !== undefined && deepEqual(wrote, response.updatedInput)) {\r\n return { honored: true, reason: `${host} rewrites tool input on ${event}` };\r\n }\r\n return {\r\n honored: false,\r\n reason: `${host} drops modify on ${event} (modify not honored)`,\r\n };\r\n}\r\n\r\n/**\r\n * Run the connector against ONE host-shaped payload through the REAL adapter\r\n * path and report whether the host honors the response. Self-contained (does not\r\n * touch the registered-connector loader or telemetry) and fail-safe: any throw\r\n * in the chain degrades to `honored:false` with the error message, never rejects.\r\n */\r\nexport async function simulate(\r\n connector: ResolvedConnector,\r\n opts: SimulateOptions,\r\n): Promise<SimulateResult> {\r\n try {\r\n const adapter = await loadAdapter(opts.host);\r\n if (!adapter) {\r\n return { honored: false, reason: `unknown host \"${opts.host}\"` };\r\n }\r\n\r\n if (opts.surface === \"statusline\") {\r\n if (\r\n !(adapter.capabilities.supportsStatusline ?? false) ||\r\n !adapter.parseStatusInput ||\r\n !adapter.formatStatusOutput\r\n ) {\r\n return { honored: false, reason: `${opts.host} has no statusline surface` };\r\n }\r\n // Guarding on the TOP-LEVEL render is sufficient: a `hosts`-only statusline\r\n // (a hosts: map with no top-level render) is rejected at defineConnector, so\r\n // a connector that targets `opts.host` only via hosts[] still has a top-level\r\n // render here. (If that author-time invariant ever relaxes, revisit this.)\r\n if (!connector.statusline || typeof connector.statusline.render !== \"function\") {\r\n return { honored: false, reason: \"connector declares no statusline\" };\r\n }\r\n const ctx = adapter.parseStatusInput(coerceInput(opts.input));\r\n ctx.connectorId = connector.id;\r\n // Defensive: parseStatusInput now sets ctx.capabilities (it is required on\r\n // StatuslineContext), but a future adapter could omit it — backfill from\r\n // the adapter so render() can always read ctx.capabilities.\r\n if (ctx.capabilities == null) ctx.capabilities = adapter.capabilities;\r\n // Per-host render override, mirroring the runtime: hosts[host].render wins\r\n // over the top-level render; an unlisted host falls back to top-level.\r\n const perHost = connector.statusline.hosts?.[opts.host as PlatformId]?.render;\r\n const render =\r\n typeof perHost === \"function\" ? perHost : connector.statusline.render;\r\n const rendered = await render(ctx);\r\n const line = rendered == null ? \"\" : String(rendered);\r\n const reply = adapter.formatStatusOutput(line);\r\n return {\r\n honored: true,\r\n ...(reply.stdout !== undefined ? { hostReply: reply.stdout } : {}),\r\n reason: \"rendered\",\r\n };\r\n }\r\n\r\n // surface === \"hooks\"\r\n const event = opts.event;\r\n if (!event) {\r\n return { honored: false, reason: \"event is required when surface === \\\"hooks\\\"\" };\r\n }\r\n if (!adapter.parseEvent || !adapter.formatReply) {\r\n return { honored: false, reason: `${opts.host} has no hook runtime (mcp-only)` };\r\n }\r\n\r\n const evt = adapter.parseEvent(event, coerceInput(opts.input));\r\n evt.connectorId = connector.id;\r\n // Mirror the runtime: runHook backfills evt.capabilities from the adapter so\r\n // a handler reading evt.capabilities behaves the same here. (scope/telemetry\r\n // are runtime-only — recovered from the registered meta / store, which the\r\n // offline harness deliberately never touches — so they stay undefined.)\r\n evt.capabilities = adapter.capabilities;\r\n\r\n // Guarding on the TOP-LEVEL handler is sufficient: a `hosts`-only hook (a\r\n // hosts: map with no top-level handler) is rejected at defineConnector, so a\r\n // host targeted only via hosts[] still has a top-level handler here. (If that\r\n // author-time invariant ever relaxes, revisit this.)\r\n const definition = connector.hooks[event];\r\n if (!definition || typeof definition.handler !== \"function\") {\r\n return { honored: false, reason: `connector declares no ${event} handler` };\r\n }\r\n\r\n // Matcher filtering, in lockstep with runHook: when the connector's matcher\r\n // is set and the event's subject (tool name, or agent type for Subagent*\r\n // events) does not match, the host never runs the handler — so the harness\r\n // must NOT run it either, and reports the (honored) exclusion. Reuses the\r\n // runtime helpers directly so the two stay byte-for-byte aligned.\r\n const subject = eventMatcherSubject(evt as NormalizedEvent);\r\n if (subject !== undefined) {\r\n const re = compileMatcher(definition.matcher);\r\n if (re && !re.test(subject)) {\r\n return {\r\n honored: true,\r\n reason: `matcher excludes ${subject} (handler not run)`,\r\n };\r\n }\r\n }\r\n\r\n // Per-host handler override, mirroring runHook: hosts[host].handler wins\r\n // over the top-level handler; an unlisted host falls back to top-level.\r\n const perHost = definition.hosts?.[opts.host as PlatformId]?.handler;\r\n const handler = typeof perHost === \"function\" ? perHost : definition.handler;\r\n const response = normalizeResponse(\r\n await handler(evt as never),\r\n );\r\n const reply = adapter.formatReply(event, response);\r\n const verdict = judgeHookHonor(String(opts.host), event, response, reply.stdout);\r\n return {\r\n honored: verdict.honored,\r\n ...(reply.stdout !== undefined ? { hostReply: reply.stdout } : {}),\r\n reason: verdict.reason,\r\n };\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n const verb = opts.surface === \"statusline\" ? \"render\" : \"handler\";\r\n return { honored: false, reason: `${verb} threw: ${message}` };\r\n }\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────\r\n// explainHooks — the per-(host, event) honor verdict (powers doctor --explain)\r\n// ─────────────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * One per-(host, event) cell of {@link explainHooks}.\r\n *\r\n * - \"honored\" — the host fires the event AND carries the connector's response\r\n * intent (simulate's runtime verdict, e.g. a real deny/block).\r\n * - \"degraded\" — the host fires the event but silently DROPS the response's\r\n * decision (e.g. codex fires UserPromptSubmit but has no stdout\r\n * path for a context injection, so the injection vanishes).\r\n * - \"dropped\" — the host cannot fire the event at all (no native equivalent /\r\n * mcp-only host); install skip-warns it and it never runs.\r\n */\r\nexport interface HookEventVerdict {\r\n host: PlatformId;\r\n event: HookEventName;\r\n verdict: \"honored\" | \"degraded\" | \"dropped\";\r\n /** The simulate()-grade reason string (names the documented degradation). */\r\n reason: string;\r\n}\r\n\r\n/**\r\n * A subject (tool name / agent type) that SATISFIES the connector's matcher for\r\n * this event, so a matcher-gated hook actually runs and the verdict reflects the\r\n * real honor — not a spurious \"matcher excludes my arbitrary synthetic tool\".\r\n * A plain-literal matcher (no regex metacharacters — the common case, e.g.\r\n * `acme_query`) IS the subject. A missing/empty matcher matches everything, and\r\n * a complex regex falls back to the generic subject (then an honest \"matcher\r\n * excludes\" verdict surfaces — the dev's matcher genuinely would not fire here).\r\n */\r\nfunction matchingSubject(matcher: string | undefined, fallback: string): string {\r\n if (!matcher || matcher === \"\") return fallback;\r\n return /^[\\w.-]+$/.test(matcher) ? matcher : fallback;\r\n}\r\n\r\n/**\r\n * A minimal host-shaped synthetic payload so the real parse→handler→format\r\n * chain has something to run on. The harness coerces a `{}` to a valid event,\r\n * but tool/subagent events read a subject (tool name / agent type) the connector\r\n * matcher may filter on — so we plant a subject that SATISFIES the connector's\r\n * declared matcher ({@link matchingSubject}) to exercise the honor path.\r\n */\r\nfunction syntheticPayloadFor(\r\n event: HookEventName,\r\n matcher: string | undefined,\r\n): Record<string, unknown> {\r\n switch (event) {\r\n case \"PreToolUse\":\r\n case \"PostToolUse\":\r\n case \"PostToolUseFailure\":\r\n case \"PermissionRequest\": {\r\n const tool = matchingSubject(matcher, \"Bash\");\r\n return { tool_name: tool, tool_input: { command: \"echo hi\" } };\r\n }\r\n case \"SubagentStart\":\r\n case \"SubagentStop\": {\r\n const agent = matchingSubject(matcher, \"general\");\r\n return { agent_type: agent, subagent_type: agent };\r\n }\r\n case \"UserPromptSubmit\":\r\n return { prompt: \"hello\" };\r\n default:\r\n return {};\r\n }\r\n}\r\n\r\n/**\r\n * The per-(host, event) hook honor matrix for the connector's DECLARED events\r\n * across `hosts` — the trustworthy detail behind `doctor --explain`.\r\n *\r\n * For each (host, declared event): when the host cannot fire the event\r\n * ({@link hostCanFireEvent} is false, OR the host has no hook runtime at all)\r\n * the cell is `dropped`; otherwise it RUNS the real chain via {@link simulate}\r\n * and reports `honored` (simulate honored the response) or `degraded` (the host\r\n * fired but dropped the decision). Reuses the per-event signal + simulate's\r\n * verdict — no per-event honor logic is re-derived here.\r\n */\r\nexport async function explainHooks(\r\n connector: ResolvedConnector,\r\n hosts: (PlatformId | string)[],\r\n): Promise<HookEventVerdict[]> {\r\n const events = connector.hookEvents;\r\n const rows: HookEventVerdict[] = [];\r\n\r\n for (const host of hosts) {\r\n const adapter = await loadAdapter(host);\r\n if (!adapter) {\r\n for (const event of events) {\r\n rows.push({\r\n host: host as PlatformId,\r\n event,\r\n verdict: \"dropped\",\r\n reason: `unknown host \"${host}\"`,\r\n });\r\n }\r\n continue;\r\n }\r\n\r\n // An mcp-only host (no hook runtime) drops every declared event.\r\n const hasHookRuntime = !!adapter.parseEvent && !!adapter.formatReply;\r\n\r\n for (const event of events) {\r\n if (!hasHookRuntime) {\r\n rows.push({\r\n host: adapter.id,\r\n event,\r\n verdict: \"dropped\",\r\n reason: `${adapter.id} has no hook runtime (mcp-only) — ${event} never fires`,\r\n });\r\n continue;\r\n }\r\n if (!hostCanFireEvent(adapter.capabilities, event)) {\r\n rows.push({\r\n host: adapter.id,\r\n event,\r\n verdict: \"dropped\",\r\n reason: `${adapter.id} has no ${event} equivalent — install skip-warns it; never fires`,\r\n });\r\n continue;\r\n }\r\n const sim = await simulate(connector, {\r\n surface: \"hooks\",\r\n host: adapter.id,\r\n event,\r\n input: syntheticPayloadFor(event, connector.hooks[event]?.matcher),\r\n });\r\n rows.push({\r\n host: adapter.id,\r\n event,\r\n verdict: sim.honored ? \"honored\" : \"degraded\",\r\n reason: sim.reason,\r\n });\r\n }\r\n }\r\n\r\n rows.sort((a, b) =>\r\n a.host === b.host ? a.event.localeCompare(b.event) : a.host.localeCompare(b.host),\r\n );\r\n return rows;\r\n}\r\n"],"mappings":"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","names":["x","y","perHost","reply"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/telemetry/jsonrpc.ts"],"sourcesContent":["/**\r\n * telemetry/jsonrpc — shared MCP stdio wire primitives.\r\n *\r\n * The MCP stdio transport is NEWLINE-DELIMITED JSON-RPC: one single-line JSON\r\n * object per message, terminated by `\\n` (no Content-Length framing). These\r\n * primitives — the message shape, the id key, the line buffer — are used by BOTH\r\n * the telemetry proxy (which tees + measures) and `doctor --probe` (which sends\r\n * a real initialize/ping/tools-list handshake), so framing + id-correlation are\r\n * byte-identical across the two paths. Extracted here rather than duplicated.\r\n */\r\n\r\nimport { StringDecoder } from \"node:string_decoder\";\r\n\r\n/** Any newline-delimited JSON-RPC message — request, response, or notification. */\r\nexport interface JsonRpcMessage {\r\n jsonrpc?: unknown;\r\n id?: unknown;\r\n method?: unknown;\r\n params?: unknown;\r\n result?: unknown;\r\n error?: unknown;\r\n}\r\n\r\nexport function isObject(v: unknown): v is Record<string, unknown> {\r\n return typeof v === \"object\" && v !== null;\r\n}\r\n\r\n/** JSON-RPC ids are number | string; normalize to a Map key. `null` → no key. */\r\nexport function idKey(id: unknown): string | null {\r\n if (typeof id === \"string\") return `s:${id}`;\r\n if (typeof id === \"number\") return `n:${id}`;\r\n return null;\r\n}\r\n\r\n/**\r\n * Buffer a byte stream into complete `\\n`-terminated lines and invoke `onLine`\r\n * for each. Carriage returns are tolerated (some hosts emit CRLF). This is a\r\n * pure side-channel: it never owns or mutates the bytes being forwarded — the\r\n * caller has already written the original chunk downstream verbatim.\r\n */\r\nexport class LineBuffer {\r\n private buf = \"\";\r\n private readonly onLine: (line: string) => void;\r\n // Incremental UTF-8 decoder: `data` events split on arbitrary byte\r\n // boundaries, so a multi-byte sequence (emoji/CJK in a tool arg or result)\r\n // can straddle two chunks. StringDecoder holds back a trailing partial\r\n // sequence until it completes, keeping the measured copy byte-accurate.\r\n // (The forwarded stream is untouched — this is only the tee copy.)\r\n private readonly decoder = new StringDecoder(\"utf8\");\r\n\r\n constructor(onLine: (line: string) => void) {\r\n this.onLine = onLine;\r\n }\r\n\r\n push(chunk: Buffer): void {\r\n this.buf += this.decoder.write(chunk);\r\n let nl = this.buf.indexOf(\"\\n\");\r\n while (nl !== -1) {\r\n // Slice off one line (without the newline), tolerate a trailing CR.\r\n let line = this.buf.slice(0, nl);\r\n if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\r\n if (line.length > 0) this.onLine(line);\r\n this.buf = this.buf.slice(nl + 1);\r\n nl = this.buf.indexOf(\"\\n\");\r\n }\r\n }\r\n}\r\n"],"mappings":";;;AAWA,SAAS,qBAAqB;AAYvB,SAAS,SAAS,GAA0C;AACjE,SAAO,OAAO,MAAM,YAAY,MAAM;AACxC;AAGO,SAAS,MAAM,IAA4B;AAChD,MAAI,OAAO,OAAO,SAAU,QAAO,KAAK,EAAE;AAC1C,MAAI,OAAO,OAAO,SAAU,QAAO,KAAK,EAAE;AAC1C,SAAO;AACT;AAQO,IAAM,aAAN,MAAiB;AAAA,EACd,MAAM;AAAA,EACG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,IAAI,cAAc,MAAM;AAAA,EAEnD,YAAY,QAAgC;AAC1C,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,KAAK,OAAqB;AACxB,SAAK,OAAO,KAAK,QAAQ,MAAM,KAAK;AACpC,QAAI,KAAK,KAAK,IAAI,QAAQ,IAAI;AAC9B,WAAO,OAAO,IAAI;AAEhB,UAAI,OAAO,KAAK,IAAI,MAAM,GAAG,EAAE;AAC/B,UAAI,KAAK,SAAS,IAAI,EAAG,QAAO,KAAK,MAAM,GAAG,EAAE;AAChD,UAAI,KAAK,SAAS,EAAG,MAAK,OAAO,IAAI;AACrC,WAAK,MAAM,KAAK,IAAI,MAAM,KAAK,CAAC;AAChC,WAAK,KAAK,IAAI,QAAQ,IAAI;AAAA,IAC5B;AAAA,EACF;AACF;","names":[]}
|