squidclaw 3.0.3 → 3.0.4
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/dist/{audio-preflight-BTYxAJjy.js → audio-preflight-BKgdc7dS.js} +4 -4
- package/dist/{audio-preflight-Dkl6Z32z.js → audio-preflight-DpCWFB4z.js} +4 -4
- package/dist/{audio-transcription-runner-Gi_h5HEE.js → audio-transcription-runner-B2BdTEps.js} +1 -1
- package/dist/{audio-transcription-runner-DBkDgluo.js → audio-transcription-runner-BnbdYMDl.js} +1 -1
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +6 -6
- package/dist/bundled/session-memory/handler.js +6 -6
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-CAd6FQEn.js → chrome-BQDCalPp.js} +8 -8
- package/dist/{chrome-pBkBuWci.js → chrome-CjxCwFA9.js} +8 -8
- package/dist/{command-registry-CvgCFxfY.js → command-registry-CxiSVXru.js} +6 -6
- package/dist/{completion-cli-BGM1V6EN.js → completion-cli-s0fxD0OE.js} +2 -2
- package/dist/{completion-cli-OrgUDc2S.js → completion-cli-slzdOlRV.js} +1 -1
- package/dist/{config-cli-DbBvjvpS.js → config-cli-C0mk9eRl.js} +1 -1
- package/dist/{config-cli-uIP4r17f.js → config-cli-DhHEA_Nc.js} +1 -1
- package/dist/{configure-BGvoAfbs.js → configure-CeLdVVyh.js} +8 -1
- package/dist/{configure-BOsTXjBw.js → configure-FRd92XZ8.js} +8 -1
- package/dist/{deliver-DBXe-ZmL.js → deliver-D-f6Wa3i.js} +1 -1
- package/dist/{deliver-BJuiq0GS.js → deliver-aL8yOYS1.js} +1 -1
- package/dist/{deliver-runtime-DHKcNQzq.js → deliver-runtime-ChVR6sR3.js} +3 -3
- package/dist/{deliver-runtime-GlnBJNCj.js → deliver-runtime-_egya0QZ.js} +3 -3
- package/dist/{deps-send-whatsapp.runtime-DdxKewuy.js → deps-send-whatsapp.runtime-CZj97m5A.js} +7 -7
- package/dist/{deps-send-whatsapp.runtime-CslTuV47.js → deps-send-whatsapp.runtime-CiG6xd2e.js} +7 -7
- package/dist/{doctor-completion-42wcUATu.js → doctor-completion-I-WDZUs1.js} +1 -1
- package/dist/{doctor-completion-D6RGDBD5.js → doctor-completion-mnJOkoQ_.js} +1 -1
- package/dist/entry.js +2 -2
- package/dist/extensionAPI.js +6 -6
- package/dist/{gateway-cli-CG3mshpO.js → gateway-cli-B5rTNvd7.js} +1 -1
- package/dist/{gateway-cli-BbPfLLT6.js → gateway-cli-BHfkbI0u.js} +1 -1
- package/dist/{image-j_UomzVG.js → image-C-C7hQ26.js} +1 -1
- package/dist/{image-sRW3RpTY.js → image-DKZCmkET.js} +1 -1
- package/dist/{image-runtime-BNh3IfMj.js → image-runtime-D7n4dID4.js} +3 -3
- package/dist/{image-runtime-SUtf9jqh.js → image-runtime-poRypm-b.js} +3 -3
- package/dist/index.js +1 -1
- package/dist/llm-slug-generator.js +6 -6
- package/dist/{onboard-ChxvwUze.js → onboard-BP2Cr_Xy.js} +1 -1
- package/dist/{onboard-1KfKwvMR.js → onboard-CCJCvPgf.js} +1 -1
- package/dist/{onboarding-D6kMb3yv.js → onboarding-D5G87dvM.js} +1 -1
- package/dist/{onboarding-DuUMPrqA.js → onboarding-D7CIbxzd.js} +1 -1
- package/dist/{onboarding.finalize-KTOhO1-l.js → onboarding.finalize-B1MmYTFV.js} +4 -4
- package/dist/{onboarding.finalize-bphDUwZy.js → onboarding.finalize-Cxzl-fYU.js} +3 -3
- package/dist/{pi-embedded-BP2UlUm_.js → pi-embedded-CHzwPt6X.js} +24 -24
- package/dist/{pi-embedded-BGz_qdCc.js → pi-embedded-MktS4l8v.js} +24 -24
- package/dist/{pi-embedded-helpers-CmLnmKlb.js → pi-embedded-helpers-Bse_QhEf.js} +3 -3
- package/dist/{pi-embedded-helpers-BruaFB5l.js → pi-embedded-helpers-DYWYzEOC.js} +3 -3
- package/dist/plugin-sdk/accounts-BNuRM3rG.js +288 -0
- package/dist/plugin-sdk/accounts-CGTYP7Rh.js +46 -0
- package/dist/plugin-sdk/accounts-CcS9IAhD.js +35 -0
- package/dist/plugin-sdk/{accounts-YTdQYQFr.js → accounts-CxUSDHsT.js} +3 -3
- package/dist/plugin-sdk/{accounts-h__dTrLK.js → accounts-PSzw-z3S.js} +2 -2
- package/dist/plugin-sdk/{accounts-DghIDNk2.js → accounts-kr-Gz1hk.js} +2 -2
- package/dist/plugin-sdk/{active-listener-_PRYjtJv.js → active-listener-BQNrTcR3.js} +2 -2
- package/dist/plugin-sdk/active-listener-CTsLn1AX.js +50 -0
- package/dist/plugin-sdk/{api-key-rotation-mVDSAkKQ.js → api-key-rotation-Bhck7wki.js} +2 -2
- package/dist/plugin-sdk/api-key-rotation-DE4gr5YM.js +181 -0
- package/dist/plugin-sdk/audio-preflight-CRGLqp-g.js +69 -0
- package/dist/plugin-sdk/{audio-preflight-BZlQM-qX.js → audio-preflight-_xgGaeho.js} +26 -26
- package/dist/plugin-sdk/{audio-transcription-runner-CrYTX8py.js → audio-transcription-runner-Dwc0Eh-B.js} +11 -11
- package/dist/plugin-sdk/audio-transcription-runner-RXsskMMk.js +2176 -0
- package/dist/plugin-sdk/audit-membership-runtime-B9b-zRwg.js +58 -0
- package/dist/plugin-sdk/{audit-membership-runtime-Xl20kCBe.js → audit-membership-runtime-DHQDvH4u.js} +2 -2
- package/dist/plugin-sdk/{channel-activity-gwxRn4wF.js → channel-activity-XajEg_DL.js} +3 -3
- package/dist/plugin-sdk/channel-activity-gPvD1D7S.js +94 -0
- package/dist/plugin-sdk/{channel-web-1WF-Nabe.js → channel-web-KtqCp4mz.js} +18 -18
- package/dist/plugin-sdk/channel-web-LGl1zPJt.js +2256 -0
- package/dist/plugin-sdk/chrome-9Y_LcUg1.js +2415 -0
- package/dist/plugin-sdk/{chrome-BXbYwXRH.js → chrome-diV5m81I.js} +6 -6
- package/dist/plugin-sdk/commands-registry-CcdEPxVg.js +1125 -0
- package/dist/plugin-sdk/{commands-registry-0w-aZenK.js → commands-registry-DwZAJuut.js} +4 -4
- package/dist/plugin-sdk/{common-DBOCt6Yv.js → common-CqnO92P8.js} +2 -2
- package/dist/plugin-sdk/config-CrQ5bCrw.js +17912 -0
- package/dist/plugin-sdk/{config-pRtEoVyZ.js → config-DYbtdrsT.js} +7 -7
- package/dist/plugin-sdk/deliver-D3xr5AkB.js +1694 -0
- package/dist/plugin-sdk/{deliver-FjlJrtZk.js → deliver-DG_7Uagn.js} +10 -10
- package/dist/plugin-sdk/deliver-runtime-B79ZQu69.js +32 -0
- package/dist/plugin-sdk/deliver-runtime-BdTC7uKE.js +32 -0
- package/dist/plugin-sdk/deps-send-discord.runtime-BOQZIqC8.js +23 -0
- package/dist/plugin-sdk/deps-send-discord.runtime-CObCNMt3.js +23 -0
- package/dist/plugin-sdk/deps-send-imessage.runtime-CuHOc9Ka.js +22 -0
- package/dist/plugin-sdk/deps-send-imessage.runtime-DlWgi2DH.js +22 -0
- package/dist/plugin-sdk/deps-send-signal.runtime-Cz7FT8J8.js +21 -0
- package/dist/plugin-sdk/deps-send-signal.runtime-iPynghkE.js +21 -0
- package/dist/plugin-sdk/deps-send-slack.runtime-D4vDoRsg.js +19 -0
- package/dist/plugin-sdk/deps-send-slack.runtime-DNTbE5jS.js +19 -0
- package/dist/plugin-sdk/deps-send-telegram.runtime-7CR-xtCF.js +24 -0
- package/dist/plugin-sdk/deps-send-telegram.runtime-DjTVED_m.js +24 -0
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-CRWOIKRC.js +57 -0
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-bUi8kghi.js +57 -0
- package/dist/plugin-sdk/diagnostic-BXkLYs_9.js +319 -0
- package/dist/plugin-sdk/{diagnostic-Dt2i3afe.js → diagnostic-CT7v_kM2.js} +2 -2
- package/dist/plugin-sdk/{errors-CgRPdp3o.js → errors-9oVz7reJ.js} +1 -1
- package/dist/plugin-sdk/errors-B8oJXuCF.js +54 -0
- package/dist/plugin-sdk/fetch-guard-C55uvn27.js +156 -0
- package/dist/plugin-sdk/{fetch-guard-DyPZh8r2.js → fetch-guard-Or5BCq0E.js} +2 -2
- package/dist/plugin-sdk/{fs-safe-DqCO1D4C.js → fs-safe-DFbwq9CS.js} +3 -3
- package/dist/plugin-sdk/fs-safe-Dqmpk-Fr.js +352 -0
- package/dist/plugin-sdk/image-3xW7IJdq.js +2310 -0
- package/dist/plugin-sdk/image-ops-BjK2qZZn.js +584 -0
- package/dist/plugin-sdk/{image-ops-sw0uZ0GN.js → image-ops-CMsocOob.js} +2 -2
- package/dist/plugin-sdk/image-runtime-CZZJJqcW.js +25 -0
- package/dist/plugin-sdk/image-runtime-Cjz368oj.js +25 -0
- package/dist/plugin-sdk/{image-CQ9TZ9vq.js → image-rycGCqJO.js} +6 -6
- package/dist/plugin-sdk/index.js +50 -50
- package/dist/plugin-sdk/ir-CS7uuQhN.js +1296 -0
- package/dist/plugin-sdk/{ir-BVZ5kUMb.js → ir-DihI2SIz.js} +7 -7
- package/dist/plugin-sdk/{local-roots-fO3ZgW3G.js → local-roots-1xVosTZ4.js} +4 -4
- package/dist/plugin-sdk/local-roots-DmOKwiNW.js +186 -0
- package/dist/plugin-sdk/{logger-DIb2cGHp.js → logger-Bg4vIUJn.js} +2 -2
- package/dist/plugin-sdk/logger-DDdrdbDu.js +1163 -0
- package/dist/plugin-sdk/login-BSEeU27Y.js +57 -0
- package/dist/plugin-sdk/{login-Dg5cxB_3.js → login-YhFrVUWo.js} +4 -4
- package/dist/plugin-sdk/login-qr-BwWJsDSj.js +320 -0
- package/dist/plugin-sdk/{login-qr-C3Vn30cq.js → login-qr-SpUTuwYv.js} +5 -5
- package/dist/plugin-sdk/manager-DiXPCubI.js +3917 -0
- package/dist/plugin-sdk/{manager-BR-TwWTH.js → manager-DrzOPeMD.js} +8 -8
- package/dist/plugin-sdk/manager-runtime-CF55pBNe.js +15 -0
- package/dist/plugin-sdk/manager-runtime-Ct0m9UJC.js +15 -0
- package/dist/plugin-sdk/mattermost.js +3 -3
- package/dist/plugin-sdk/{outbound-1a3Z_QJ2.js → outbound-Cc4cUn9K.js} +5 -5
- package/dist/plugin-sdk/outbound-attachment-BoFx05zw.js +19 -0
- package/dist/plugin-sdk/{outbound-attachment-BTQjD4YE.js → outbound-attachment-Dtp3hQgc.js} +2 -2
- package/dist/plugin-sdk/outbound-cpqK1GFe.js +212 -0
- package/dist/plugin-sdk/{path-alias-guards-TnxupPQC.js → path-alias-guards-DA0MhfkG.js} +1 -1
- package/dist/plugin-sdk/path-alias-guards-gBhrAn14.js +43 -0
- package/dist/plugin-sdk/paths-C6W4VHoa.js +166 -0
- package/dist/plugin-sdk/{paths-B7_75Pdr.js → paths-CP67O8eN.js} +1 -1
- package/dist/plugin-sdk/{pi-embedded-helpers-DZRNadD8.js → pi-embedded-helpers-BDJ_4Plh.js} +16 -16
- package/dist/plugin-sdk/pi-embedded-helpers-C-B9B6Sp.js +9627 -0
- package/dist/plugin-sdk/pi-model-discovery-BGEeoPzN.js +134 -0
- package/dist/plugin-sdk/{pi-model-discovery-DGh6xekX.js → pi-model-discovery-Mk0GTDJl.js} +1 -1
- package/dist/plugin-sdk/pi-model-discovery-runtime-BHZ_Htob.js +8 -0
- package/dist/plugin-sdk/pi-model-discovery-runtime-BrwtJHPU.js +8 -0
- package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-ByN_xThw.js +354 -0
- package/dist/plugin-sdk/{pi-tools.before-tool-call.runtime-BZ9XgG_x.js → pi-tools.before-tool-call.runtime-DV72wTDb.js} +4 -4
- package/dist/plugin-sdk/plugins-D5cdn70e.js +864 -0
- package/dist/plugin-sdk/{plugins-B8pWVYug.js → plugins-DSs2-fnK.js} +4 -4
- package/dist/plugin-sdk/{proxy-env-BOlkiW1-.js → proxy-env-Ib4-LUh-.js} +1 -1
- package/dist/plugin-sdk/{proxy-fetch-Dt5BedH8.js → proxy-fetch-Cf3IUSDw.js} +1 -1
- package/dist/plugin-sdk/proxy-fetch-ZPEvp58f.js +38 -0
- package/dist/plugin-sdk/pw-ai-C_QOIuin.js +1938 -0
- package/dist/plugin-sdk/{pw-ai-C17A1o4w.js → pw-ai-DIx2wpkY.js} +9 -9
- package/dist/plugin-sdk/qmd-manager-6bozlfFg.js +1448 -0
- package/dist/plugin-sdk/{qmd-manager-Bei6TaFq.js → qmd-manager-Ov9ElEfG.js} +7 -7
- package/dist/plugin-sdk/{query-expansion-POz2za8a.js → query-expansion-CzjwW461.js} +4 -4
- package/dist/plugin-sdk/query-expansion-eeVz_aEm.js +1011 -0
- package/dist/plugin-sdk/{redact-9WsNyb7S.js → redact-BoNEjbpF.js} +1 -1
- package/dist/plugin-sdk/redact-DfACyt0X.js +319 -0
- package/dist/plugin-sdk/reply-CQUX_haM.js +98828 -0
- package/dist/plugin-sdk/{reply-BFbijn6_.js → reply-CWWUd_JS.js} +73 -73
- package/dist/plugin-sdk/{resolve-outbound-target-B9iFEh0y.js → resolve-outbound-target-BOkvxZtM.js} +2 -2
- package/dist/plugin-sdk/resolve-outbound-target-Dbz0O8cR.js +40 -0
- package/dist/plugin-sdk/run-with-concurrency-5DMu9szx.js +1994 -0
- package/dist/plugin-sdk/{run-with-concurrency-DmTrN5JG.js → run-with-concurrency-kVooFCVo.js} +1 -1
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-DitS0I1z.js +10 -0
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-OthrtsLL.js +10 -0
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-CYCr6A3v.js +19 -0
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-Q2HL0zL3.js +19 -0
- package/dist/plugin-sdk/send-BACEu1Un.js +414 -0
- package/dist/plugin-sdk/{send-BGZo6HW1.js → send-BP1fSEBR.js} +5 -5
- package/dist/plugin-sdk/send-BU4OoR7u.js +2587 -0
- package/dist/plugin-sdk/{send-BqkUDZed.js → send-BeLBlAsQ.js} +13 -13
- package/dist/plugin-sdk/{send-BisREGBZ.js → send-D9CSOGul.js} +6 -6
- package/dist/plugin-sdk/{send-D6_nNvi0.js → send-DLKxJJYV.js} +8 -8
- package/dist/plugin-sdk/send-DbxOJ_BC.js +3135 -0
- package/dist/plugin-sdk/{send-Dj7XEcZN.js → send-XZ6IXCtL.js} +7 -7
- package/dist/plugin-sdk/send-n932vjT5.js +540 -0
- package/dist/plugin-sdk/send-uCPS53j8.js +503 -0
- package/dist/plugin-sdk/session-DenDKR_-.js +169 -0
- package/dist/plugin-sdk/{session-D4KDs7Hq.js → session-DtLUYWvY.js} +3 -3
- package/dist/plugin-sdk/skill-commands-BK1KDKmS.js +342 -0
- package/dist/plugin-sdk/{skill-commands-D_xeseiI.js → skill-commands-Bv7EZypt.js} +4 -4
- package/dist/plugin-sdk/{skills-Bs2b3JfV.js → skills-BzXN4uev.js} +6 -6
- package/dist/plugin-sdk/skills-D4am-zkO.js +1428 -0
- package/dist/plugin-sdk/slash-commands.runtime-Bx1K1iqP.js +13 -0
- package/dist/plugin-sdk/slash-commands.runtime-DWfFqMZw.js +13 -0
- package/dist/plugin-sdk/slash-dispatch.runtime-DVn338JI.js +52 -0
- package/dist/plugin-sdk/slash-dispatch.runtime-pnWH5AjM.js +52 -0
- package/dist/plugin-sdk/slash-skill-commands.runtime-Dbi_YzPO.js +16 -0
- package/dist/plugin-sdk/slash-skill-commands.runtime-DxvNWv_E.js +16 -0
- package/dist/plugin-sdk/ssrf-2WBi1Tzx.js +202 -0
- package/dist/plugin-sdk/store-BKDMuvyn.js +81 -0
- package/dist/plugin-sdk/{store-B7ESm9_L.js → store-DnJhFFW5.js} +2 -2
- package/dist/plugin-sdk/subagent-registry-runtime-FhP0l-Rw.js +52 -0
- package/dist/plugin-sdk/subagent-registry-runtime-hH9ADku1.js +52 -0
- package/dist/plugin-sdk/{tables-1vhBJPK_.js → tables-CpmqssLF.js} +1 -1
- package/dist/plugin-sdk/tables-CrDYcv_b.js +55 -0
- package/dist/plugin-sdk/target-errors-aOwE-MIU.js +195 -0
- package/dist/plugin-sdk/{thinking-DjaClmzi.js → thinking-1UCPuD9d.js} +7 -7
- package/dist/plugin-sdk/thinking-D41FMh9T.js +1206 -0
- package/dist/plugin-sdk/{tokens-CLE20fRI.js → tokens-CTIYTLWu.js} +1 -1
- package/dist/plugin-sdk/tokens-DAL_5WHL.js +52 -0
- package/dist/plugin-sdk/{tool-images-B95xcwiR.js → tool-images-CWc54lpI.js} +2 -2
- package/dist/plugin-sdk/tool-images-RX4QTMnt.js +274 -0
- package/dist/plugin-sdk/web-AtEy-48y.js +56 -0
- package/dist/plugin-sdk/web-DjKONHqF.js +56 -0
- package/dist/plugin-sdk/{whatsapp-actions-BYpcWkTN.js → whatsapp-actions-BF6ih4Gi.js} +17 -17
- package/dist/plugin-sdk/whatsapp-actions-DEZcm_CZ.js +80 -0
- package/dist/plugin-sdk/whatsapp.js +50 -50
- package/dist/{program-xNEHPhT8.js → program-2J-jgdfk.js} +2 -2
- package/dist/{program-context-J_FyEsaS.js → program-context-SjYSWx_N.js} +8 -8
- package/dist/{prompt-select-styled-B1LjjgQ0.js → prompt-select-styled-Baiu3mAU.js} +1 -1
- package/dist/{prompt-select-styled-BRiogP_P.js → prompt-select-styled-DPnVmH8f.js} +1 -1
- package/dist/{pw-ai-7kHgUGj0.js → pw-ai-BwRP3TWc.js} +1 -1
- package/dist/{pw-ai-BmGrTicP.js → pw-ai-zFPBSxaL.js} +1 -1
- package/dist/{register.configure-DezZ4Q1p.js → register.configure-Ao1K2uze.js} +1 -1
- package/dist/{register.configure-C4p9ad2q.js → register.configure-CCkfhF-7.js} +1 -1
- package/dist/{register.maintenance-CzMKTC2a.js → register.maintenance-DMbs8w2m.js} +4 -4
- package/dist/{register.maintenance-CTvFmkAm.js → register.maintenance-pLpE2oF-.js} +5 -5
- package/dist/{register.onboard-C39xhpv1.js → register.onboard-Cb8xLIye.js} +2 -2
- package/dist/{register.onboard-DZt2kSAg.js → register.onboard-IMrHOeW_.js} +2 -2
- package/dist/{register.setup-04L_8wfA.js → register.setup-CX8IUmew.js} +2 -2
- package/dist/{register.setup-DWctFmOd.js → register.setup-DU7IDzAv.js} +2 -2
- package/dist/{register.subclis-C3TphbCF.js → register.subclis-BIbL6FBV.js} +3 -3
- package/dist/{run-main-7tknx04F.js → run-main-CpDZPsC_.js} +5 -5
- package/dist/{slash-dispatch.runtime-BL3qA1O3.js → slash-dispatch.runtime-DkcAYuyK.js} +6 -6
- package/dist/{slash-dispatch.runtime-Dh2L_3Tg.js → slash-dispatch.runtime-DuJRl-LD.js} +6 -6
- package/dist/{subagent-registry-runtime-MtjBCcgn.js → subagent-registry-runtime-BlRAnw80.js} +6 -6
- package/dist/{subagent-registry-runtime-BRNDawlJ.js → subagent-registry-runtime-COygB9b1.js} +6 -6
- package/dist/{update-cli-0UiUaT3q.js → update-cli-BwhvSo1R.js} +5 -5
- package/dist/{update-cli-C-uyQcFS.js → update-cli-TQEfxhWF.js} +4 -4
- package/dist/{web-D1ZoRVB0.js → web-DddJa7ZT.js} +6 -6
- package/dist/{web-B7kbCskR.js → web-DyCuTR9b.js} +6 -6
- package/package.json +7 -7
- package/dist/plugin-sdk/deliver-runtime-DEzvpBW1.js +0 -32
- package/dist/plugin-sdk/deps-send-discord.runtime-Bhusa_Hi.js +0 -23
- package/dist/plugin-sdk/deps-send-imessage.runtime-bmakPm5f.js +0 -22
- package/dist/plugin-sdk/deps-send-signal.runtime-n00sfFto.js +0 -21
- package/dist/plugin-sdk/deps-send-slack.runtime-BvM3Z-Mr.js +0 -19
- package/dist/plugin-sdk/deps-send-telegram.runtime-CPuMkcmo.js +0 -24
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-BzO6S-KX.js +0 -57
- package/dist/plugin-sdk/image-runtime-17_mTqsy.js +0 -25
- package/dist/plugin-sdk/manager-runtime-CvI9wF8N.js +0 -15
- package/dist/plugin-sdk/pi-model-discovery-runtime-DjjBdPYt.js +0 -8
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-DzhkSmLi.js +0 -10
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-DyILWezU.js +0 -19
- package/dist/plugin-sdk/slash-commands.runtime-CUb5sqqf.js +0 -13
- package/dist/plugin-sdk/slash-dispatch.runtime-DCB6bGjB.js +0 -52
- package/dist/plugin-sdk/slash-skill-commands.runtime-BqEweE4K.js +0 -16
- package/dist/plugin-sdk/subagent-registry-runtime-CCUW4SbM.js +0 -52
- package/dist/plugin-sdk/web-DeRmHQ4_.js +0 -56
|
@@ -0,0 +1,1448 @@
|
|
|
1
|
+
import { c as resolveAgentWorkspaceDir, mt as parseAgentSessionKey } from "./run-with-concurrency-5DMu9szx.js";
|
|
2
|
+
import { c as resolveStateDir } from "./paths-8xF5kDne.js";
|
|
3
|
+
import { a as createSubsystemLogger } from "./logger-DDdrdbDu.js";
|
|
4
|
+
import "./paths-C6W4VHoa.js";
|
|
5
|
+
import "./redact-BoNEjbpF.js";
|
|
6
|
+
import "./path-alias-guards-gBhrAn14.js";
|
|
7
|
+
import { l as writeFileWithinRoot } from "./fs-safe-Dqmpk-Fr.js";
|
|
8
|
+
import { i as resolveWindowsSpawnProgram, n as materializeWindowsSpawnProgram } from "./windows-spawn-E2JqbJ-S.js";
|
|
9
|
+
import { _ as isFileMissingError, i as listSessionFilesForAgent, n as requireNodeSqlite, r as buildSessionEntry, t as extractKeywords, v as statRegularFile } from "./query-expansion-eeVz_aEm.js";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import readline from "node:readline";
|
|
15
|
+
|
|
16
|
+
//#region src/memory/qmd-scope.ts
|
|
17
|
+
function isQmdScopeAllowed(scope, sessionKey) {
|
|
18
|
+
if (!scope) return true;
|
|
19
|
+
const parsed = parseQmdSessionScope(sessionKey);
|
|
20
|
+
const channel = parsed.channel;
|
|
21
|
+
const chatType = parsed.chatType;
|
|
22
|
+
const normalizedKey = parsed.normalizedKey ?? "";
|
|
23
|
+
const rawKey = sessionKey?.trim().toLowerCase() ?? "";
|
|
24
|
+
for (const rule of scope.rules ?? []) {
|
|
25
|
+
if (!rule) continue;
|
|
26
|
+
const match = rule.match ?? {};
|
|
27
|
+
if (match.channel && match.channel !== channel) continue;
|
|
28
|
+
if (match.chatType && match.chatType !== chatType) continue;
|
|
29
|
+
const normalizedPrefix = match.keyPrefix?.trim().toLowerCase() || void 0;
|
|
30
|
+
const rawPrefix = match.rawKeyPrefix?.trim().toLowerCase() || void 0;
|
|
31
|
+
if (rawPrefix && !rawKey.startsWith(rawPrefix)) continue;
|
|
32
|
+
if (normalizedPrefix) {
|
|
33
|
+
if (normalizedPrefix.startsWith("agent:")) {
|
|
34
|
+
if (!rawKey.startsWith(normalizedPrefix)) continue;
|
|
35
|
+
} else if (!normalizedKey.startsWith(normalizedPrefix)) continue;
|
|
36
|
+
}
|
|
37
|
+
return rule.action === "allow";
|
|
38
|
+
}
|
|
39
|
+
return (scope.default ?? "allow") === "allow";
|
|
40
|
+
}
|
|
41
|
+
function deriveQmdScopeChannel(key) {
|
|
42
|
+
return parseQmdSessionScope(key).channel;
|
|
43
|
+
}
|
|
44
|
+
function deriveQmdScopeChatType(key) {
|
|
45
|
+
return parseQmdSessionScope(key).chatType;
|
|
46
|
+
}
|
|
47
|
+
function parseQmdSessionScope(key) {
|
|
48
|
+
const normalized = normalizeQmdSessionKey(key);
|
|
49
|
+
if (!normalized) return {};
|
|
50
|
+
const parts = normalized.split(":").filter(Boolean);
|
|
51
|
+
let chatType;
|
|
52
|
+
if (parts.length >= 2 && (parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")) {
|
|
53
|
+
if (parts.includes("group")) chatType = "group";
|
|
54
|
+
else if (parts.includes("channel")) chatType = "channel";
|
|
55
|
+
return {
|
|
56
|
+
normalizedKey: normalized,
|
|
57
|
+
channel: parts[0]?.toLowerCase(),
|
|
58
|
+
chatType: chatType ?? "direct"
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (normalized.includes(":group:")) return {
|
|
62
|
+
normalizedKey: normalized,
|
|
63
|
+
chatType: "group"
|
|
64
|
+
};
|
|
65
|
+
if (normalized.includes(":channel:")) return {
|
|
66
|
+
normalizedKey: normalized,
|
|
67
|
+
chatType: "channel"
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
normalizedKey: normalized,
|
|
71
|
+
chatType: "direct"
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function normalizeQmdSessionKey(key) {
|
|
75
|
+
if (!key) return;
|
|
76
|
+
const trimmed = key.trim();
|
|
77
|
+
if (!trimmed) return;
|
|
78
|
+
const normalized = (parseAgentSessionKey(trimmed)?.rest ?? trimmed).toLowerCase();
|
|
79
|
+
if (normalized.startsWith("subagent:")) return;
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/memory/qmd-query-parser.ts
|
|
85
|
+
const log$1 = createSubsystemLogger("memory");
|
|
86
|
+
function parseQmdQueryJson(stdout, stderr) {
|
|
87
|
+
const trimmedStdout = stdout.trim();
|
|
88
|
+
const trimmedStderr = stderr.trim();
|
|
89
|
+
const stdoutIsMarker = trimmedStdout.length > 0 && isQmdNoResultsOutput(trimmedStdout);
|
|
90
|
+
const stderrIsMarker = trimmedStderr.length > 0 && isQmdNoResultsOutput(trimmedStderr);
|
|
91
|
+
if (stdoutIsMarker || !trimmedStdout && stderrIsMarker) return [];
|
|
92
|
+
if (!trimmedStdout) {
|
|
93
|
+
const message = `stdout empty${trimmedStderr ? ` (stderr: ${summarizeQmdStderr(trimmedStderr)})` : ""}`;
|
|
94
|
+
log$1.warn(`qmd query returned invalid JSON: ${message}`);
|
|
95
|
+
throw new Error(`qmd query returned invalid JSON: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const parsed = parseQmdQueryResultArray(trimmedStdout);
|
|
99
|
+
if (parsed !== null) return parsed;
|
|
100
|
+
const noisyPayload = extractFirstJsonArray(trimmedStdout);
|
|
101
|
+
if (!noisyPayload) throw new Error("qmd query JSON response was not an array");
|
|
102
|
+
const fallback = parseQmdQueryResultArray(noisyPayload);
|
|
103
|
+
if (fallback !== null) return fallback;
|
|
104
|
+
throw new Error("qmd query JSON response was not an array");
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
107
|
+
log$1.warn(`qmd query returned invalid JSON: ${message}`);
|
|
108
|
+
throw new Error(`qmd query returned invalid JSON: ${message}`, { cause: err });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isQmdNoResultsOutput(raw) {
|
|
112
|
+
return raw.split(/\r?\n/).map((line) => line.trim().toLowerCase().replace(/\s+/g, " ")).filter((line) => line.length > 0).some((line) => isQmdNoResultsLine(line));
|
|
113
|
+
}
|
|
114
|
+
function isQmdNoResultsLine(line) {
|
|
115
|
+
if (line === "no results found" || line === "no results found.") return true;
|
|
116
|
+
return /^(?:\[[^\]]+\]\s*)?(?:(?:warn(?:ing)?|info|error|qmd)\s*:\s*)+no results found\.?$/.test(line);
|
|
117
|
+
}
|
|
118
|
+
function summarizeQmdStderr(raw) {
|
|
119
|
+
return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`;
|
|
120
|
+
}
|
|
121
|
+
function parseQmdQueryResultArray(raw) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
if (!Array.isArray(parsed)) return null;
|
|
125
|
+
return parsed;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function extractFirstJsonArray(raw) {
|
|
131
|
+
const start = raw.indexOf("[");
|
|
132
|
+
if (start < 0) return null;
|
|
133
|
+
let depth = 0;
|
|
134
|
+
let inString = false;
|
|
135
|
+
let escaped = false;
|
|
136
|
+
for (let i = start; i < raw.length; i += 1) {
|
|
137
|
+
const char = raw[i];
|
|
138
|
+
if (char === void 0) break;
|
|
139
|
+
if (inString) {
|
|
140
|
+
if (escaped) {
|
|
141
|
+
escaped = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (char === "\\") escaped = true;
|
|
145
|
+
else if (char === "\"") inString = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (char === "\"") {
|
|
149
|
+
inString = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (char === "[") depth += 1;
|
|
153
|
+
else if (char === "]") {
|
|
154
|
+
depth -= 1;
|
|
155
|
+
if (depth === 0) return raw.slice(start, i + 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/memory/qmd-manager.ts
|
|
163
|
+
const log = createSubsystemLogger("memory");
|
|
164
|
+
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
|
165
|
+
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
|
166
|
+
const MAX_QMD_OUTPUT_CHARS = 2e5;
|
|
167
|
+
const NUL_MARKER_RE = /(?:\^@|\\0|\\x00|\\u0000|null\s*byte|nul\s*byte)/i;
|
|
168
|
+
const QMD_EMBED_BACKOFF_BASE_MS = 6e4;
|
|
169
|
+
const QMD_EMBED_BACKOFF_MAX_MS = 3600 * 1e3;
|
|
170
|
+
const HAN_SCRIPT_RE = /[\u3400-\u9fff]/u;
|
|
171
|
+
const QMD_BM25_HAN_KEYWORD_LIMIT = 12;
|
|
172
|
+
let qmdEmbedQueueTail = Promise.resolve();
|
|
173
|
+
function resolveWindowsCommandShim(command) {
|
|
174
|
+
if (process.platform !== "win32") return command;
|
|
175
|
+
const trimmed = command.trim();
|
|
176
|
+
if (!trimmed) return command;
|
|
177
|
+
const ext = path.extname(trimmed).toLowerCase();
|
|
178
|
+
if (ext === ".cmd" || ext === ".exe" || ext === ".bat") return command;
|
|
179
|
+
const base = path.basename(trimmed).toLowerCase();
|
|
180
|
+
if (base === "qmd" || base === "mcporter") return `${trimmed}.cmd`;
|
|
181
|
+
return command;
|
|
182
|
+
}
|
|
183
|
+
function resolveSpawnInvocation(params) {
|
|
184
|
+
return materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
|
|
185
|
+
command: resolveWindowsCommandShim(params.command),
|
|
186
|
+
platform: process.platform,
|
|
187
|
+
env: params.env,
|
|
188
|
+
execPath: process.execPath,
|
|
189
|
+
packageName: params.packageName,
|
|
190
|
+
allowShellFallback: true
|
|
191
|
+
}), params.args);
|
|
192
|
+
}
|
|
193
|
+
function hasHanScript(value) {
|
|
194
|
+
return HAN_SCRIPT_RE.test(value);
|
|
195
|
+
}
|
|
196
|
+
function normalizeHanBm25Query(query) {
|
|
197
|
+
const trimmed = query.trim();
|
|
198
|
+
if (!trimmed || !hasHanScript(trimmed)) return trimmed;
|
|
199
|
+
const keywords = extractKeywords(trimmed);
|
|
200
|
+
const normalizedKeywords = [];
|
|
201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
202
|
+
for (const keyword of keywords) {
|
|
203
|
+
const token = keyword.trim();
|
|
204
|
+
if (!token || seen.has(token)) continue;
|
|
205
|
+
const includesHan = hasHanScript(token);
|
|
206
|
+
if (includesHan && Array.from(token).length < 2) continue;
|
|
207
|
+
if (!includesHan && token.length < 2) continue;
|
|
208
|
+
seen.add(token);
|
|
209
|
+
normalizedKeywords.push(token);
|
|
210
|
+
if (normalizedKeywords.length >= QMD_BM25_HAN_KEYWORD_LIMIT) break;
|
|
211
|
+
}
|
|
212
|
+
return normalizedKeywords.length > 0 ? normalizedKeywords.join(" ") : trimmed;
|
|
213
|
+
}
|
|
214
|
+
async function runWithQmdEmbedLock(task) {
|
|
215
|
+
const previous = qmdEmbedQueueTail;
|
|
216
|
+
let release;
|
|
217
|
+
qmdEmbedQueueTail = new Promise((resolve) => {
|
|
218
|
+
release = resolve;
|
|
219
|
+
});
|
|
220
|
+
await previous.catch(() => void 0);
|
|
221
|
+
try {
|
|
222
|
+
return await task();
|
|
223
|
+
} finally {
|
|
224
|
+
release?.();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
var QmdMemoryManager = class QmdMemoryManager {
|
|
228
|
+
static async create(params) {
|
|
229
|
+
const resolved = params.resolved.qmd;
|
|
230
|
+
if (!resolved) return null;
|
|
231
|
+
const manager = new QmdMemoryManager({
|
|
232
|
+
cfg: params.cfg,
|
|
233
|
+
agentId: params.agentId,
|
|
234
|
+
resolved
|
|
235
|
+
});
|
|
236
|
+
await manager.initialize(params.mode ?? "full");
|
|
237
|
+
return manager;
|
|
238
|
+
}
|
|
239
|
+
constructor(params) {
|
|
240
|
+
this.collectionRoots = /* @__PURE__ */ new Map();
|
|
241
|
+
this.sources = /* @__PURE__ */ new Set();
|
|
242
|
+
this.docPathCache = /* @__PURE__ */ new Map();
|
|
243
|
+
this.exportedSessionState = /* @__PURE__ */ new Map();
|
|
244
|
+
this.maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
|
|
245
|
+
this.updateTimer = null;
|
|
246
|
+
this.pendingUpdate = null;
|
|
247
|
+
this.queuedForcedUpdate = null;
|
|
248
|
+
this.queuedForcedRuns = 0;
|
|
249
|
+
this.closed = false;
|
|
250
|
+
this.db = null;
|
|
251
|
+
this.lastUpdateAt = null;
|
|
252
|
+
this.lastEmbedAt = null;
|
|
253
|
+
this.embedBackoffUntil = null;
|
|
254
|
+
this.embedFailureCount = 0;
|
|
255
|
+
this.attemptedNullByteCollectionRepair = false;
|
|
256
|
+
this.cfg = params.cfg;
|
|
257
|
+
this.agentId = params.agentId;
|
|
258
|
+
this.qmd = params.resolved;
|
|
259
|
+
this.workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
|
|
260
|
+
this.stateDir = resolveStateDir(process.env, os.homedir);
|
|
261
|
+
this.agentStateDir = path.join(this.stateDir, "agents", this.agentId);
|
|
262
|
+
this.qmdDir = path.join(this.agentStateDir, "qmd");
|
|
263
|
+
this.xdgConfigHome = path.join(this.qmdDir, "xdg-config");
|
|
264
|
+
this.xdgCacheHome = path.join(this.qmdDir, "xdg-cache");
|
|
265
|
+
this.indexPath = path.join(this.xdgCacheHome, "qmd", "index.sqlite");
|
|
266
|
+
this.env = {
|
|
267
|
+
...process.env,
|
|
268
|
+
XDG_CONFIG_HOME: this.xdgConfigHome,
|
|
269
|
+
QMD_CONFIG_DIR: this.xdgConfigHome,
|
|
270
|
+
XDG_CACHE_HOME: this.xdgCacheHome,
|
|
271
|
+
NO_COLOR: "1"
|
|
272
|
+
};
|
|
273
|
+
this.sessionExporter = this.qmd.sessions.enabled ? {
|
|
274
|
+
dir: this.qmd.sessions.exportDir ?? path.join(this.qmdDir, "sessions"),
|
|
275
|
+
retentionMs: this.qmd.sessions.retentionDays ? this.qmd.sessions.retentionDays * 24 * 60 * 60 * 1e3 : void 0,
|
|
276
|
+
collectionName: this.pickSessionCollectionName()
|
|
277
|
+
} : null;
|
|
278
|
+
if (this.sessionExporter) this.qmd.collections = [...this.qmd.collections, {
|
|
279
|
+
name: this.sessionExporter.collectionName,
|
|
280
|
+
path: this.sessionExporter.dir,
|
|
281
|
+
pattern: "**/*.md",
|
|
282
|
+
kind: "sessions"
|
|
283
|
+
}];
|
|
284
|
+
this.managedCollectionNames = this.computeManagedCollectionNames();
|
|
285
|
+
}
|
|
286
|
+
async initialize(mode) {
|
|
287
|
+
this.bootstrapCollections();
|
|
288
|
+
if (mode === "status") return;
|
|
289
|
+
await fs.mkdir(this.xdgConfigHome, { recursive: true });
|
|
290
|
+
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
|
291
|
+
await fs.mkdir(path.dirname(this.indexPath), { recursive: true });
|
|
292
|
+
if (this.sessionExporter) await fs.mkdir(this.sessionExporter.dir, { recursive: true });
|
|
293
|
+
await this.symlinkSharedModels();
|
|
294
|
+
await this.ensureCollections();
|
|
295
|
+
if (this.qmd.update.onBoot) {
|
|
296
|
+
const bootRun = this.runUpdate("boot", true);
|
|
297
|
+
if (this.qmd.update.waitForBootSync) await bootRun.catch((err) => {
|
|
298
|
+
log.warn(`qmd boot update failed: ${String(err)}`);
|
|
299
|
+
});
|
|
300
|
+
else bootRun.catch((err) => {
|
|
301
|
+
log.warn(`qmd boot update failed: ${String(err)}`);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (this.qmd.update.intervalMs > 0) this.updateTimer = setInterval(() => {
|
|
305
|
+
this.runUpdate("interval").catch((err) => {
|
|
306
|
+
log.warn(`qmd update failed (${String(err)})`);
|
|
307
|
+
});
|
|
308
|
+
}, this.qmd.update.intervalMs);
|
|
309
|
+
}
|
|
310
|
+
bootstrapCollections() {
|
|
311
|
+
this.collectionRoots.clear();
|
|
312
|
+
this.sources.clear();
|
|
313
|
+
for (const collection of this.qmd.collections) {
|
|
314
|
+
const kind = collection.kind === "sessions" ? "sessions" : "memory";
|
|
315
|
+
this.collectionRoots.set(collection.name, {
|
|
316
|
+
path: collection.path,
|
|
317
|
+
kind
|
|
318
|
+
});
|
|
319
|
+
this.sources.add(kind);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async ensureCollections() {
|
|
323
|
+
const existing = /* @__PURE__ */ new Map();
|
|
324
|
+
try {
|
|
325
|
+
const result = await this.runQmd([
|
|
326
|
+
"collection",
|
|
327
|
+
"list",
|
|
328
|
+
"--json"
|
|
329
|
+
], { timeoutMs: this.qmd.update.commandTimeoutMs });
|
|
330
|
+
const parsed = this.parseListedCollections(result.stdout);
|
|
331
|
+
for (const [name, details] of parsed) existing.set(name, details);
|
|
332
|
+
} catch {}
|
|
333
|
+
await this.migrateLegacyUnscopedCollections(existing);
|
|
334
|
+
for (const collection of this.qmd.collections) {
|
|
335
|
+
const listed = existing.get(collection.name);
|
|
336
|
+
if (listed && !this.shouldRebindCollection(collection, listed)) continue;
|
|
337
|
+
if (listed) try {
|
|
338
|
+
await this.removeCollection(collection.name);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
341
|
+
if (!this.isCollectionMissingError(message)) log.warn(`qmd collection remove failed for ${collection.name}: ${message}`);
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
await this.ensureCollectionPath(collection);
|
|
345
|
+
await this.addCollection(collection.path, collection.name, collection.pattern);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
348
|
+
if (this.isCollectionAlreadyExistsError(message)) continue;
|
|
349
|
+
log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async migrateLegacyUnscopedCollections(existing) {
|
|
354
|
+
for (const collection of this.qmd.collections) {
|
|
355
|
+
if (existing.has(collection.name)) continue;
|
|
356
|
+
const legacyName = this.deriveLegacyCollectionName(collection.name);
|
|
357
|
+
if (!legacyName) continue;
|
|
358
|
+
const listedLegacy = existing.get(legacyName);
|
|
359
|
+
if (!listedLegacy) continue;
|
|
360
|
+
if (!this.canMigrateLegacyCollection(collection, listedLegacy)) {
|
|
361
|
+
log.debug(`qmd legacy collection migration skipped for ${legacyName} (path/pattern mismatch)`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await this.removeCollection(legacyName);
|
|
366
|
+
existing.delete(legacyName);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
369
|
+
if (!this.isCollectionMissingError(message)) log.warn(`qmd collection remove failed for ${legacyName}: ${message}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
deriveLegacyCollectionName(scopedName) {
|
|
374
|
+
const agentSuffix = `-${this.sanitizeCollectionNameSegment(this.agentId)}`;
|
|
375
|
+
if (!scopedName.endsWith(agentSuffix)) return null;
|
|
376
|
+
return scopedName.slice(0, -agentSuffix.length).trim() || null;
|
|
377
|
+
}
|
|
378
|
+
canMigrateLegacyCollection(collection, listedLegacy) {
|
|
379
|
+
if (listedLegacy.path && !this.pathsMatch(listedLegacy.path, collection.path)) return false;
|
|
380
|
+
if (typeof listedLegacy.pattern === "string" && listedLegacy.pattern !== collection.pattern) return false;
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
async ensureCollectionPath(collection) {
|
|
384
|
+
if (!this.isDirectoryGlobPattern(collection.pattern)) return;
|
|
385
|
+
await fs.mkdir(collection.path, { recursive: true });
|
|
386
|
+
}
|
|
387
|
+
isDirectoryGlobPattern(pattern) {
|
|
388
|
+
return pattern.includes("*") || pattern.includes("?") || pattern.includes("[");
|
|
389
|
+
}
|
|
390
|
+
isCollectionAlreadyExistsError(message) {
|
|
391
|
+
const lower = message.toLowerCase();
|
|
392
|
+
return lower.includes("already exists") || lower.includes("exists");
|
|
393
|
+
}
|
|
394
|
+
isCollectionMissingError(message) {
|
|
395
|
+
const lower = message.toLowerCase();
|
|
396
|
+
return lower.includes("not found") || lower.includes("does not exist") || lower.includes("missing");
|
|
397
|
+
}
|
|
398
|
+
isMissingCollectionSearchError(err) {
|
|
399
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
400
|
+
return this.isCollectionMissingError(message) && message.toLowerCase().includes("collection");
|
|
401
|
+
}
|
|
402
|
+
async tryRepairMissingCollectionSearch(err) {
|
|
403
|
+
if (!this.isMissingCollectionSearchError(err)) return false;
|
|
404
|
+
log.warn("qmd search failed because a managed collection is missing; repairing collections and retrying once");
|
|
405
|
+
await this.ensureCollections();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
async addCollection(pathArg, name, pattern) {
|
|
409
|
+
await this.runQmd([
|
|
410
|
+
"collection",
|
|
411
|
+
"add",
|
|
412
|
+
pathArg,
|
|
413
|
+
"--name",
|
|
414
|
+
name,
|
|
415
|
+
"--mask",
|
|
416
|
+
pattern
|
|
417
|
+
], { timeoutMs: this.qmd.update.commandTimeoutMs });
|
|
418
|
+
}
|
|
419
|
+
async removeCollection(name) {
|
|
420
|
+
await this.runQmd([
|
|
421
|
+
"collection",
|
|
422
|
+
"remove",
|
|
423
|
+
name
|
|
424
|
+
], { timeoutMs: this.qmd.update.commandTimeoutMs });
|
|
425
|
+
}
|
|
426
|
+
parseListedCollections(output) {
|
|
427
|
+
const listed = /* @__PURE__ */ new Map();
|
|
428
|
+
const trimmed = output.trim();
|
|
429
|
+
if (!trimmed) return listed;
|
|
430
|
+
try {
|
|
431
|
+
const parsed = JSON.parse(trimmed);
|
|
432
|
+
if (Array.isArray(parsed)) {
|
|
433
|
+
for (const entry of parsed) {
|
|
434
|
+
if (typeof entry === "string") {
|
|
435
|
+
listed.set(entry, {});
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (!entry || typeof entry !== "object") continue;
|
|
439
|
+
const name = entry.name;
|
|
440
|
+
if (typeof name !== "string") continue;
|
|
441
|
+
const listedPath = entry.path;
|
|
442
|
+
const listedPattern = entry.pattern;
|
|
443
|
+
const listedMask = entry.mask;
|
|
444
|
+
listed.set(name, {
|
|
445
|
+
path: typeof listedPath === "string" ? listedPath : void 0,
|
|
446
|
+
pattern: typeof listedPattern === "string" ? listedPattern : typeof listedMask === "string" ? listedMask : void 0
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return listed;
|
|
450
|
+
}
|
|
451
|
+
} catch {}
|
|
452
|
+
let currentName = null;
|
|
453
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
454
|
+
const line = rawLine.trimEnd();
|
|
455
|
+
if (!line.trim()) {
|
|
456
|
+
currentName = null;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const collectionLine = /^\s*([a-z0-9._-]+)\s+\(qmd:\/\/[^)]+\)\s*$/i.exec(line);
|
|
460
|
+
if (collectionLine) {
|
|
461
|
+
currentName = collectionLine[1];
|
|
462
|
+
if (!listed.has(currentName)) listed.set(currentName, {});
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (/^\s*collections\b/i.test(line)) continue;
|
|
466
|
+
const bareNameLine = /^\s*([a-z0-9._-]+)\s*$/i.exec(line);
|
|
467
|
+
if (bareNameLine && !line.includes(":")) {
|
|
468
|
+
currentName = bareNameLine[1];
|
|
469
|
+
if (!listed.has(currentName)) listed.set(currentName, {});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (!currentName) continue;
|
|
473
|
+
const patternLine = /^\s*(?:pattern|mask)\s*:\s*(.+?)\s*$/i.exec(line);
|
|
474
|
+
if (patternLine) {
|
|
475
|
+
const existing = listed.get(currentName) ?? {};
|
|
476
|
+
existing.pattern = patternLine[1].trim();
|
|
477
|
+
listed.set(currentName, existing);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const pathLine = /^\s*path\s*:\s*(.+?)\s*$/i.exec(line);
|
|
481
|
+
if (pathLine) {
|
|
482
|
+
const existing = listed.get(currentName) ?? {};
|
|
483
|
+
existing.path = pathLine[1].trim();
|
|
484
|
+
listed.set(currentName, existing);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return listed;
|
|
488
|
+
}
|
|
489
|
+
shouldRebindCollection(collection, listed) {
|
|
490
|
+
if (!listed.path) return true;
|
|
491
|
+
if (!this.pathsMatch(listed.path, collection.path)) return true;
|
|
492
|
+
if (typeof listed.pattern === "string" && listed.pattern !== collection.pattern) return true;
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
pathsMatch(left, right) {
|
|
496
|
+
const normalize = (value) => {
|
|
497
|
+
const resolved = path.isAbsolute(value) ? path.resolve(value) : path.resolve(this.workspaceDir, value);
|
|
498
|
+
const normalized = path.normalize(resolved);
|
|
499
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
500
|
+
};
|
|
501
|
+
return normalize(left) === normalize(right);
|
|
502
|
+
}
|
|
503
|
+
shouldRepairNullByteCollectionError(err) {
|
|
504
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
505
|
+
const lower = message.toLowerCase();
|
|
506
|
+
return (lower.includes("enotdir") || lower.includes("not a directory")) && NUL_MARKER_RE.test(message);
|
|
507
|
+
}
|
|
508
|
+
async tryRepairNullByteCollections(err, reason) {
|
|
509
|
+
if (this.attemptedNullByteCollectionRepair) return false;
|
|
510
|
+
if (!this.shouldRepairNullByteCollectionError(err)) return false;
|
|
511
|
+
this.attemptedNullByteCollectionRepair = true;
|
|
512
|
+
log.warn(`qmd update failed with suspected null-byte collection metadata (${reason}); rebuilding managed collections and retrying once`);
|
|
513
|
+
for (const collection of this.qmd.collections) {
|
|
514
|
+
try {
|
|
515
|
+
await this.removeCollection(collection.name);
|
|
516
|
+
} catch (removeErr) {
|
|
517
|
+
const removeMessage = removeErr instanceof Error ? removeErr.message : String(removeErr);
|
|
518
|
+
if (!this.isCollectionMissingError(removeMessage)) log.warn(`qmd collection remove failed for ${collection.name}: ${removeMessage}`);
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await this.addCollection(collection.path, collection.name, collection.pattern);
|
|
522
|
+
} catch (addErr) {
|
|
523
|
+
const addMessage = addErr instanceof Error ? addErr.message : String(addErr);
|
|
524
|
+
if (!this.isCollectionAlreadyExistsError(addMessage)) log.warn(`qmd collection add failed for ${collection.name}: ${addMessage}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
async search(query, opts) {
|
|
530
|
+
if (!this.isScopeAllowed(opts?.sessionKey)) {
|
|
531
|
+
this.logScopeDenied(opts?.sessionKey);
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
const trimmed = query.trim();
|
|
535
|
+
if (!trimmed) return [];
|
|
536
|
+
await this.waitForPendingUpdateBeforeSearch();
|
|
537
|
+
const limit = Math.min(this.qmd.limits.maxResults, opts?.maxResults ?? this.qmd.limits.maxResults);
|
|
538
|
+
const collectionNames = this.listManagedCollectionNames();
|
|
539
|
+
if (collectionNames.length === 0) {
|
|
540
|
+
log.warn("qmd query skipped: no managed collections configured");
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
const qmdSearchCommand = this.qmd.searchMode;
|
|
544
|
+
const mcporterEnabled = this.qmd.mcporter.enabled;
|
|
545
|
+
const runSearchAttempt = async (allowMissingCollectionRepair) => {
|
|
546
|
+
try {
|
|
547
|
+
if (mcporterEnabled) {
|
|
548
|
+
const tool = qmdSearchCommand === "search" ? "search" : qmdSearchCommand === "vsearch" ? "vector_search" : "deep_search";
|
|
549
|
+
const minScore = opts?.minScore ?? 0;
|
|
550
|
+
if (collectionNames.length > 1) return await this.runMcporterAcrossCollections({
|
|
551
|
+
tool,
|
|
552
|
+
query: trimmed,
|
|
553
|
+
limit,
|
|
554
|
+
minScore,
|
|
555
|
+
collectionNames
|
|
556
|
+
});
|
|
557
|
+
return await this.runQmdSearchViaMcporter({
|
|
558
|
+
mcporter: this.qmd.mcporter,
|
|
559
|
+
tool,
|
|
560
|
+
query: trimmed,
|
|
561
|
+
limit,
|
|
562
|
+
minScore,
|
|
563
|
+
collection: collectionNames[0],
|
|
564
|
+
timeoutMs: this.qmd.limits.timeoutMs
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
if (collectionNames.length > 1) return await this.runQueryAcrossCollections(trimmed, limit, collectionNames, qmdSearchCommand);
|
|
568
|
+
const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit);
|
|
569
|
+
args.push(...this.buildCollectionFilterArgs(collectionNames));
|
|
570
|
+
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
|
571
|
+
return parseQmdQueryJson(result.stdout, result.stderr);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
if (allowMissingCollectionRepair && this.isMissingCollectionSearchError(err)) throw err;
|
|
574
|
+
if (!mcporterEnabled && qmdSearchCommand !== "query" && this.isUnsupportedQmdOptionError(err)) {
|
|
575
|
+
log.warn(`qmd ${qmdSearchCommand} does not support configured flags; retrying search with qmd query`);
|
|
576
|
+
try {
|
|
577
|
+
if (collectionNames.length > 1) return await this.runQueryAcrossCollections(trimmed, limit, collectionNames, "query");
|
|
578
|
+
const fallbackArgs = this.buildSearchArgs("query", trimmed, limit);
|
|
579
|
+
fallbackArgs.push(...this.buildCollectionFilterArgs(collectionNames));
|
|
580
|
+
const fallback = await this.runQmd(fallbackArgs, { timeoutMs: this.qmd.limits.timeoutMs });
|
|
581
|
+
return parseQmdQueryJson(fallback.stdout, fallback.stderr);
|
|
582
|
+
} catch (fallbackErr) {
|
|
583
|
+
log.warn(`qmd query fallback failed: ${String(fallbackErr)}`);
|
|
584
|
+
throw fallbackErr instanceof Error ? fallbackErr : new Error(String(fallbackErr));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const label = mcporterEnabled ? "mcporter/qmd" : `qmd ${qmdSearchCommand}`;
|
|
588
|
+
log.warn(`${label} failed: ${String(err)}`);
|
|
589
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
let parsed;
|
|
593
|
+
try {
|
|
594
|
+
parsed = await runSearchAttempt(true);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
if (!await this.tryRepairMissingCollectionSearch(err)) throw err instanceof Error ? err : new Error(String(err));
|
|
597
|
+
parsed = await runSearchAttempt(false);
|
|
598
|
+
}
|
|
599
|
+
const results = [];
|
|
600
|
+
for (const entry of parsed) {
|
|
601
|
+
const doc = await this.resolveDocLocation(entry.docid, {
|
|
602
|
+
preferredCollection: entry.collection,
|
|
603
|
+
preferredFile: entry.file
|
|
604
|
+
});
|
|
605
|
+
if (!doc) continue;
|
|
606
|
+
const snippet = entry.snippet?.slice(0, this.qmd.limits.maxSnippetChars) ?? "";
|
|
607
|
+
const lines = this.extractSnippetLines(snippet);
|
|
608
|
+
const score = typeof entry.score === "number" ? entry.score : 0;
|
|
609
|
+
if (score < (opts?.minScore ?? 0)) continue;
|
|
610
|
+
results.push({
|
|
611
|
+
path: doc.rel,
|
|
612
|
+
startLine: lines.startLine,
|
|
613
|
+
endLine: lines.endLine,
|
|
614
|
+
score,
|
|
615
|
+
snippet,
|
|
616
|
+
source: doc.source
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return this.clampResultsByInjectedChars(this.diversifyResultsBySource(results, limit));
|
|
620
|
+
}
|
|
621
|
+
async sync(params) {
|
|
622
|
+
if (params?.progress) params.progress({
|
|
623
|
+
completed: 0,
|
|
624
|
+
total: 1,
|
|
625
|
+
label: "Updating QMD index…"
|
|
626
|
+
});
|
|
627
|
+
await this.runUpdate(params?.reason ?? "manual", params?.force);
|
|
628
|
+
if (params?.progress) params.progress({
|
|
629
|
+
completed: 1,
|
|
630
|
+
total: 1,
|
|
631
|
+
label: "QMD index updated"
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
async readFile(params) {
|
|
635
|
+
const relPath = params.relPath?.trim();
|
|
636
|
+
if (!relPath) throw new Error("path required");
|
|
637
|
+
const absPath = this.resolveReadPath(relPath);
|
|
638
|
+
if (!absPath.endsWith(".md")) throw new Error("path required");
|
|
639
|
+
if ((await statRegularFile(absPath)).missing) return {
|
|
640
|
+
text: "",
|
|
641
|
+
path: relPath
|
|
642
|
+
};
|
|
643
|
+
if (params.from !== void 0 || params.lines !== void 0) {
|
|
644
|
+
const partial = await this.readPartialText(absPath, params.from, params.lines);
|
|
645
|
+
if (partial.missing) return {
|
|
646
|
+
text: "",
|
|
647
|
+
path: relPath
|
|
648
|
+
};
|
|
649
|
+
return {
|
|
650
|
+
text: partial.text,
|
|
651
|
+
path: relPath
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const full = await this.readFullText(absPath);
|
|
655
|
+
if (full.missing) return {
|
|
656
|
+
text: "",
|
|
657
|
+
path: relPath
|
|
658
|
+
};
|
|
659
|
+
if (!params.from && !params.lines) return {
|
|
660
|
+
text: full.text,
|
|
661
|
+
path: relPath
|
|
662
|
+
};
|
|
663
|
+
const lines = full.text.split("\n");
|
|
664
|
+
const start = Math.max(1, params.from ?? 1);
|
|
665
|
+
const count = Math.max(1, params.lines ?? lines.length);
|
|
666
|
+
return {
|
|
667
|
+
text: lines.slice(start - 1, start - 1 + count).join("\n"),
|
|
668
|
+
path: relPath
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
status() {
|
|
672
|
+
const counts = this.readCounts();
|
|
673
|
+
return {
|
|
674
|
+
backend: "qmd",
|
|
675
|
+
provider: "qmd",
|
|
676
|
+
model: "qmd",
|
|
677
|
+
requestedProvider: "qmd",
|
|
678
|
+
files: counts.totalDocuments,
|
|
679
|
+
chunks: counts.totalDocuments,
|
|
680
|
+
dirty: false,
|
|
681
|
+
workspaceDir: this.workspaceDir,
|
|
682
|
+
dbPath: this.indexPath,
|
|
683
|
+
sources: Array.from(this.sources),
|
|
684
|
+
sourceCounts: counts.sourceCounts,
|
|
685
|
+
vector: {
|
|
686
|
+
enabled: true,
|
|
687
|
+
available: true
|
|
688
|
+
},
|
|
689
|
+
batch: {
|
|
690
|
+
enabled: false,
|
|
691
|
+
failures: 0,
|
|
692
|
+
limit: 0,
|
|
693
|
+
wait: false,
|
|
694
|
+
concurrency: 0,
|
|
695
|
+
pollIntervalMs: 0,
|
|
696
|
+
timeoutMs: 0
|
|
697
|
+
},
|
|
698
|
+
custom: { qmd: {
|
|
699
|
+
collections: this.qmd.collections.length,
|
|
700
|
+
lastUpdateAt: this.lastUpdateAt
|
|
701
|
+
} }
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
async probeEmbeddingAvailability() {
|
|
705
|
+
return { ok: true };
|
|
706
|
+
}
|
|
707
|
+
async probeVectorAvailability() {
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
async close() {
|
|
711
|
+
if (this.closed) return;
|
|
712
|
+
this.closed = true;
|
|
713
|
+
if (this.updateTimer) {
|
|
714
|
+
clearInterval(this.updateTimer);
|
|
715
|
+
this.updateTimer = null;
|
|
716
|
+
}
|
|
717
|
+
this.queuedForcedRuns = 0;
|
|
718
|
+
await this.pendingUpdate?.catch(() => void 0);
|
|
719
|
+
await this.queuedForcedUpdate?.catch(() => void 0);
|
|
720
|
+
if (this.db) {
|
|
721
|
+
this.db.close();
|
|
722
|
+
this.db = null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async runUpdate(reason, force, opts) {
|
|
726
|
+
if (this.closed) return;
|
|
727
|
+
if (this.pendingUpdate) {
|
|
728
|
+
if (force) return this.enqueueForcedUpdate(reason);
|
|
729
|
+
return this.pendingUpdate;
|
|
730
|
+
}
|
|
731
|
+
if (this.queuedForcedUpdate && !opts?.fromForcedQueue) {
|
|
732
|
+
if (force) return this.enqueueForcedUpdate(reason);
|
|
733
|
+
return this.queuedForcedUpdate;
|
|
734
|
+
}
|
|
735
|
+
if (this.shouldSkipUpdate(force)) return;
|
|
736
|
+
const run = async () => {
|
|
737
|
+
if (this.sessionExporter) await this.exportSessions();
|
|
738
|
+
await this.runQmdUpdateWithRetry(reason);
|
|
739
|
+
if (this.shouldRunEmbed(force)) try {
|
|
740
|
+
await runWithQmdEmbedLock(async () => {
|
|
741
|
+
await this.runQmd(["embed"], {
|
|
742
|
+
timeoutMs: this.qmd.update.embedTimeoutMs,
|
|
743
|
+
discardOutput: true
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
this.lastEmbedAt = Date.now();
|
|
747
|
+
this.embedBackoffUntil = null;
|
|
748
|
+
this.embedFailureCount = 0;
|
|
749
|
+
} catch (err) {
|
|
750
|
+
this.noteEmbedFailure(reason, err);
|
|
751
|
+
}
|
|
752
|
+
this.lastUpdateAt = Date.now();
|
|
753
|
+
this.docPathCache.clear();
|
|
754
|
+
};
|
|
755
|
+
this.pendingUpdate = run().finally(() => {
|
|
756
|
+
this.pendingUpdate = null;
|
|
757
|
+
});
|
|
758
|
+
await this.pendingUpdate;
|
|
759
|
+
}
|
|
760
|
+
async runQmdUpdateWithRetry(reason) {
|
|
761
|
+
const maxAttempts = reason === "boot" || reason.startsWith("boot:") ? 3 : 1;
|
|
762
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
|
|
763
|
+
await this.runQmdUpdateOnce(reason);
|
|
764
|
+
return;
|
|
765
|
+
} catch (err) {
|
|
766
|
+
if (attempt >= maxAttempts || !this.isRetryableUpdateError(err)) throw err;
|
|
767
|
+
const delayMs = 500 * 2 ** (attempt - 1);
|
|
768
|
+
log.warn(`qmd update retry ${attempt}/${maxAttempts - 1} after failure (${reason}): ${String(err)}`);
|
|
769
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async runQmdUpdateOnce(reason) {
|
|
773
|
+
try {
|
|
774
|
+
await this.runQmd(["update"], {
|
|
775
|
+
timeoutMs: this.qmd.update.updateTimeoutMs,
|
|
776
|
+
discardOutput: true
|
|
777
|
+
});
|
|
778
|
+
} catch (err) {
|
|
779
|
+
if (!await this.tryRepairNullByteCollections(err, reason)) throw err;
|
|
780
|
+
await this.runQmd(["update"], {
|
|
781
|
+
timeoutMs: this.qmd.update.updateTimeoutMs,
|
|
782
|
+
discardOutput: true
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
isRetryableUpdateError(err) {
|
|
787
|
+
if (this.isSqliteBusyError(err)) return true;
|
|
788
|
+
return (err instanceof Error ? err.message : String(err)).toLowerCase().includes("timed out");
|
|
789
|
+
}
|
|
790
|
+
shouldRunEmbed(force) {
|
|
791
|
+
if (this.qmd.searchMode === "search") return false;
|
|
792
|
+
const now = Date.now();
|
|
793
|
+
if (this.embedBackoffUntil !== null && now < this.embedBackoffUntil) return false;
|
|
794
|
+
const embedIntervalMs = this.qmd.update.embedIntervalMs;
|
|
795
|
+
return Boolean(force) || this.lastEmbedAt === null || embedIntervalMs > 0 && now - this.lastEmbedAt > embedIntervalMs;
|
|
796
|
+
}
|
|
797
|
+
noteEmbedFailure(reason, err) {
|
|
798
|
+
this.embedFailureCount += 1;
|
|
799
|
+
const delayMs = Math.min(QMD_EMBED_BACKOFF_MAX_MS, QMD_EMBED_BACKOFF_BASE_MS * 2 ** Math.max(0, this.embedFailureCount - 1));
|
|
800
|
+
this.embedBackoffUntil = Date.now() + delayMs;
|
|
801
|
+
log.warn(`qmd embed failed (${reason}): ${String(err)}; backing off for ${Math.ceil(delayMs / 1e3)}s`);
|
|
802
|
+
}
|
|
803
|
+
enqueueForcedUpdate(reason) {
|
|
804
|
+
this.queuedForcedRuns += 1;
|
|
805
|
+
if (!this.queuedForcedUpdate) this.queuedForcedUpdate = this.drainForcedUpdates(reason).finally(() => {
|
|
806
|
+
this.queuedForcedUpdate = null;
|
|
807
|
+
});
|
|
808
|
+
return this.queuedForcedUpdate;
|
|
809
|
+
}
|
|
810
|
+
async drainForcedUpdates(reason) {
|
|
811
|
+
await this.pendingUpdate?.catch(() => void 0);
|
|
812
|
+
while (!this.closed && this.queuedForcedRuns > 0) {
|
|
813
|
+
this.queuedForcedRuns -= 1;
|
|
814
|
+
await this.runUpdate(`${reason}:queued`, true, { fromForcedQueue: true });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Symlink the default QMD models directory into our custom XDG_CACHE_HOME so
|
|
819
|
+
* that the pre-installed ML models (~/.cache/qmd/models/) are reused rather
|
|
820
|
+
* than re-downloaded for every agent. If the default models directory does
|
|
821
|
+
* not exist, or a models directory/symlink already exists in the target, this
|
|
822
|
+
* is a no-op.
|
|
823
|
+
*/
|
|
824
|
+
async symlinkSharedModels() {
|
|
825
|
+
const defaultCacheHome = process.env.XDG_CACHE_HOME || (process.platform === "win32" ? process.env.LOCALAPPDATA : void 0) || path.join(os.homedir(), ".cache");
|
|
826
|
+
const defaultModelsDir = path.join(defaultCacheHome, "qmd", "models");
|
|
827
|
+
const targetModelsDir = path.join(this.xdgCacheHome, "qmd", "models");
|
|
828
|
+
try {
|
|
829
|
+
if (!(await fs.stat(defaultModelsDir).catch((err) => {
|
|
830
|
+
if (err.code === "ENOENT") return null;
|
|
831
|
+
throw err;
|
|
832
|
+
}))?.isDirectory()) return;
|
|
833
|
+
try {
|
|
834
|
+
await fs.lstat(targetModelsDir);
|
|
835
|
+
return;
|
|
836
|
+
} catch {}
|
|
837
|
+
try {
|
|
838
|
+
await fs.symlink(defaultModelsDir, targetModelsDir, "dir");
|
|
839
|
+
} catch (symlinkErr) {
|
|
840
|
+
const code = symlinkErr.code;
|
|
841
|
+
if (process.platform === "win32" && (code === "EPERM" || code === "ENOTSUP")) await fs.symlink(defaultModelsDir, targetModelsDir, "junction");
|
|
842
|
+
else throw symlinkErr;
|
|
843
|
+
}
|
|
844
|
+
log.debug(`symlinked qmd models: ${defaultModelsDir} → ${targetModelsDir}`);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
log.warn(`failed to symlink qmd models directory: ${String(err)}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async runQmd(args, opts) {
|
|
850
|
+
return await new Promise((resolve, reject) => {
|
|
851
|
+
const spawnInvocation = resolveSpawnInvocation({
|
|
852
|
+
command: this.qmd.command,
|
|
853
|
+
args,
|
|
854
|
+
env: this.env,
|
|
855
|
+
packageName: "qmd"
|
|
856
|
+
});
|
|
857
|
+
const child = spawn(spawnInvocation.command, spawnInvocation.argv, {
|
|
858
|
+
env: this.env,
|
|
859
|
+
cwd: this.workspaceDir,
|
|
860
|
+
shell: spawnInvocation.shell,
|
|
861
|
+
windowsHide: spawnInvocation.windowsHide
|
|
862
|
+
});
|
|
863
|
+
let stdout = "";
|
|
864
|
+
let stderr = "";
|
|
865
|
+
let stdoutTruncated = false;
|
|
866
|
+
let stderrTruncated = false;
|
|
867
|
+
const discard = opts?.discardOutput === true;
|
|
868
|
+
const timer = opts?.timeoutMs ? setTimeout(() => {
|
|
869
|
+
child.kill("SIGKILL");
|
|
870
|
+
reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} timed out after ${opts.timeoutMs}ms`));
|
|
871
|
+
}, opts.timeoutMs) : null;
|
|
872
|
+
child.stdout.on("data", (data) => {
|
|
873
|
+
if (discard) return;
|
|
874
|
+
const next = appendOutputWithCap(stdout, data.toString("utf8"), this.maxQmdOutputChars);
|
|
875
|
+
stdout = next.text;
|
|
876
|
+
stdoutTruncated = stdoutTruncated || next.truncated;
|
|
877
|
+
});
|
|
878
|
+
child.stderr.on("data", (data) => {
|
|
879
|
+
const next = appendOutputWithCap(stderr, data.toString("utf8"), this.maxQmdOutputChars);
|
|
880
|
+
stderr = next.text;
|
|
881
|
+
stderrTruncated = stderrTruncated || next.truncated;
|
|
882
|
+
});
|
|
883
|
+
child.on("error", (err) => {
|
|
884
|
+
if (timer) clearTimeout(timer);
|
|
885
|
+
reject(err);
|
|
886
|
+
});
|
|
887
|
+
child.on("close", (code) => {
|
|
888
|
+
if (timer) clearTimeout(timer);
|
|
889
|
+
if (!discard && (stdoutTruncated || stderrTruncated)) {
|
|
890
|
+
reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} produced too much output (limit ${this.maxQmdOutputChars} chars)`));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (code === 0) resolve({
|
|
894
|
+
stdout,
|
|
895
|
+
stderr
|
|
896
|
+
});
|
|
897
|
+
else reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} failed (code ${code}): ${stderr || stdout}`));
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
async ensureMcporterDaemonStarted(mcporter) {
|
|
902
|
+
if (!mcporter.enabled) return;
|
|
903
|
+
if (!mcporter.startDaemon) {
|
|
904
|
+
const g = globalThis;
|
|
905
|
+
if (!g.__squidclawMcporterColdStartWarned) {
|
|
906
|
+
g.__squidclawMcporterColdStartWarned = true;
|
|
907
|
+
log.warn("mcporter qmd bridge enabled but startDaemon=false; each query may cold-start QMD MCP. Consider setting memory.qmd.mcporter.startDaemon=true to keep it warm.");
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const g = globalThis;
|
|
912
|
+
if (!g.__squidclawMcporterDaemonStart) g.__squidclawMcporterDaemonStart = (async () => {
|
|
913
|
+
try {
|
|
914
|
+
await this.runMcporter(["daemon", "start"], { timeoutMs: 1e4 });
|
|
915
|
+
} catch (err) {
|
|
916
|
+
log.warn(`mcporter daemon start failed: ${String(err)}`);
|
|
917
|
+
delete g.__squidclawMcporterDaemonStart;
|
|
918
|
+
}
|
|
919
|
+
})();
|
|
920
|
+
await g.__squidclawMcporterDaemonStart;
|
|
921
|
+
}
|
|
922
|
+
async runMcporter(args, opts) {
|
|
923
|
+
return await new Promise((resolve, reject) => {
|
|
924
|
+
const spawnInvocation = resolveSpawnInvocation({
|
|
925
|
+
command: "mcporter",
|
|
926
|
+
args,
|
|
927
|
+
env: this.env,
|
|
928
|
+
packageName: "mcporter"
|
|
929
|
+
});
|
|
930
|
+
const child = spawn(spawnInvocation.command, spawnInvocation.argv, {
|
|
931
|
+
env: this.env,
|
|
932
|
+
cwd: this.workspaceDir,
|
|
933
|
+
shell: spawnInvocation.shell,
|
|
934
|
+
windowsHide: spawnInvocation.windowsHide
|
|
935
|
+
});
|
|
936
|
+
let stdout = "";
|
|
937
|
+
let stderr = "";
|
|
938
|
+
let stdoutTruncated = false;
|
|
939
|
+
let stderrTruncated = false;
|
|
940
|
+
const timer = opts?.timeoutMs ? setTimeout(() => {
|
|
941
|
+
child.kill("SIGKILL");
|
|
942
|
+
reject(/* @__PURE__ */ new Error(`mcporter ${args.join(" ")} timed out after ${opts.timeoutMs}ms`));
|
|
943
|
+
}, opts.timeoutMs) : null;
|
|
944
|
+
child.stdout.on("data", (data) => {
|
|
945
|
+
const next = appendOutputWithCap(stdout, data.toString("utf8"), this.maxQmdOutputChars);
|
|
946
|
+
stdout = next.text;
|
|
947
|
+
stdoutTruncated = stdoutTruncated || next.truncated;
|
|
948
|
+
});
|
|
949
|
+
child.stderr.on("data", (data) => {
|
|
950
|
+
const next = appendOutputWithCap(stderr, data.toString("utf8"), this.maxQmdOutputChars);
|
|
951
|
+
stderr = next.text;
|
|
952
|
+
stderrTruncated = stderrTruncated || next.truncated;
|
|
953
|
+
});
|
|
954
|
+
child.on("error", (err) => {
|
|
955
|
+
if (timer) clearTimeout(timer);
|
|
956
|
+
reject(err);
|
|
957
|
+
});
|
|
958
|
+
child.on("close", (code) => {
|
|
959
|
+
if (timer) clearTimeout(timer);
|
|
960
|
+
if (stdoutTruncated || stderrTruncated) {
|
|
961
|
+
reject(/* @__PURE__ */ new Error(`mcporter ${args.join(" ")} produced too much output (limit ${this.maxQmdOutputChars} chars)`));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (code === 0) resolve({
|
|
965
|
+
stdout,
|
|
966
|
+
stderr
|
|
967
|
+
});
|
|
968
|
+
else reject(/* @__PURE__ */ new Error(`mcporter ${args.join(" ")} failed (code ${code}): ${stderr || stdout}`));
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async runQmdSearchViaMcporter(params) {
|
|
973
|
+
await this.ensureMcporterDaemonStarted(params.mcporter);
|
|
974
|
+
const selector = `${params.mcporter.serverName}.${params.tool}`;
|
|
975
|
+
const callArgs = {
|
|
976
|
+
query: params.query,
|
|
977
|
+
limit: params.limit,
|
|
978
|
+
minScore: params.minScore
|
|
979
|
+
};
|
|
980
|
+
if (params.collection) callArgs.collection = params.collection;
|
|
981
|
+
const result = await this.runMcporter([
|
|
982
|
+
"call",
|
|
983
|
+
selector,
|
|
984
|
+
"--args",
|
|
985
|
+
JSON.stringify(callArgs),
|
|
986
|
+
"--output",
|
|
987
|
+
"json",
|
|
988
|
+
"--timeout",
|
|
989
|
+
String(Math.max(0, params.timeoutMs))
|
|
990
|
+
], { timeoutMs: Math.max(params.timeoutMs + 2e3, 5e3) });
|
|
991
|
+
const parsedUnknown = JSON.parse(result.stdout);
|
|
992
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
993
|
+
const structured = isRecord(parsedUnknown) && isRecord(parsedUnknown.structuredContent) ? parsedUnknown.structuredContent : parsedUnknown;
|
|
994
|
+
const results = isRecord(structured) && Array.isArray(structured.results) ? structured.results : Array.isArray(structured) ? structured : [];
|
|
995
|
+
const out = [];
|
|
996
|
+
for (const item of results) {
|
|
997
|
+
if (!isRecord(item)) continue;
|
|
998
|
+
const docidRaw = item.docid;
|
|
999
|
+
const docid = typeof docidRaw === "string" ? docidRaw.replace(/^#/, "").trim() : "";
|
|
1000
|
+
if (!docid) continue;
|
|
1001
|
+
const scoreRaw = item.score;
|
|
1002
|
+
const score = typeof scoreRaw === "number" ? scoreRaw : Number(scoreRaw);
|
|
1003
|
+
const snippet = typeof item.snippet === "string" ? item.snippet : "";
|
|
1004
|
+
out.push({
|
|
1005
|
+
docid,
|
|
1006
|
+
score: Number.isFinite(score) ? score : 0,
|
|
1007
|
+
snippet
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
async readPartialText(absPath, from, lines) {
|
|
1013
|
+
const start = Math.max(1, from ?? 1);
|
|
1014
|
+
const count = Math.max(1, lines ?? Number.POSITIVE_INFINITY);
|
|
1015
|
+
let handle;
|
|
1016
|
+
try {
|
|
1017
|
+
handle = await fs.open(absPath);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
if (isFileMissingError(err)) return { missing: true };
|
|
1020
|
+
throw err;
|
|
1021
|
+
}
|
|
1022
|
+
const stream = handle.createReadStream({ encoding: "utf-8" });
|
|
1023
|
+
const rl = readline.createInterface({
|
|
1024
|
+
input: stream,
|
|
1025
|
+
crlfDelay: Infinity
|
|
1026
|
+
});
|
|
1027
|
+
const selected = [];
|
|
1028
|
+
let index = 0;
|
|
1029
|
+
try {
|
|
1030
|
+
for await (const line of rl) {
|
|
1031
|
+
index += 1;
|
|
1032
|
+
if (index < start) continue;
|
|
1033
|
+
if (selected.length >= count) break;
|
|
1034
|
+
selected.push(line);
|
|
1035
|
+
}
|
|
1036
|
+
} finally {
|
|
1037
|
+
rl.close();
|
|
1038
|
+
await handle.close();
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
missing: false,
|
|
1042
|
+
text: selected.slice(0, count).join("\n")
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
async readFullText(absPath) {
|
|
1046
|
+
try {
|
|
1047
|
+
return {
|
|
1048
|
+
missing: false,
|
|
1049
|
+
text: await fs.readFile(absPath, "utf-8")
|
|
1050
|
+
};
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
if (isFileMissingError(err)) return { missing: true };
|
|
1053
|
+
throw err;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
ensureDb() {
|
|
1057
|
+
if (this.db) return this.db;
|
|
1058
|
+
const { DatabaseSync } = requireNodeSqlite();
|
|
1059
|
+
this.db = new DatabaseSync(this.indexPath, { readOnly: true });
|
|
1060
|
+
this.db.exec("PRAGMA busy_timeout = 1");
|
|
1061
|
+
return this.db;
|
|
1062
|
+
}
|
|
1063
|
+
async exportSessions() {
|
|
1064
|
+
if (!this.sessionExporter) return;
|
|
1065
|
+
const exportDir = this.sessionExporter.dir;
|
|
1066
|
+
await fs.mkdir(exportDir, { recursive: true });
|
|
1067
|
+
const files = await listSessionFilesForAgent(this.agentId);
|
|
1068
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1069
|
+
const tracked = /* @__PURE__ */ new Set();
|
|
1070
|
+
const cutoff = this.sessionExporter.retentionMs ? Date.now() - this.sessionExporter.retentionMs : null;
|
|
1071
|
+
for (const sessionFile of files) {
|
|
1072
|
+
const entry = await buildSessionEntry(sessionFile);
|
|
1073
|
+
if (!entry) continue;
|
|
1074
|
+
if (cutoff && entry.mtimeMs < cutoff) continue;
|
|
1075
|
+
const targetName = `${path.basename(sessionFile, ".jsonl")}.md`;
|
|
1076
|
+
const target = path.join(exportDir, targetName);
|
|
1077
|
+
tracked.add(sessionFile);
|
|
1078
|
+
const state = this.exportedSessionState.get(sessionFile);
|
|
1079
|
+
if (!state || state.hash !== entry.hash || state.mtimeMs !== entry.mtimeMs) await writeFileWithinRoot({
|
|
1080
|
+
rootDir: exportDir,
|
|
1081
|
+
relativePath: targetName,
|
|
1082
|
+
data: this.renderSessionMarkdown(entry),
|
|
1083
|
+
encoding: "utf-8"
|
|
1084
|
+
});
|
|
1085
|
+
this.exportedSessionState.set(sessionFile, {
|
|
1086
|
+
hash: entry.hash,
|
|
1087
|
+
mtimeMs: entry.mtimeMs,
|
|
1088
|
+
target
|
|
1089
|
+
});
|
|
1090
|
+
keep.add(target);
|
|
1091
|
+
}
|
|
1092
|
+
const exported = await fs.readdir(exportDir).catch(() => []);
|
|
1093
|
+
for (const name of exported) {
|
|
1094
|
+
if (!name.endsWith(".md")) continue;
|
|
1095
|
+
const full = path.join(exportDir, name);
|
|
1096
|
+
if (!keep.has(full)) await fs.rm(full, { force: true });
|
|
1097
|
+
}
|
|
1098
|
+
for (const [sessionFile, state] of this.exportedSessionState) if (!tracked.has(sessionFile) || !state.target.startsWith(exportDir + path.sep)) this.exportedSessionState.delete(sessionFile);
|
|
1099
|
+
}
|
|
1100
|
+
renderSessionMarkdown(entry) {
|
|
1101
|
+
return `${`# Session ${path.basename(entry.absPath, path.extname(entry.absPath))}`}\n\n${entry.content?.trim().length ? entry.content.trim() : "(empty)"}\n`;
|
|
1102
|
+
}
|
|
1103
|
+
pickSessionCollectionName() {
|
|
1104
|
+
const existing = new Set(this.qmd.collections.map((collection) => collection.name));
|
|
1105
|
+
const base = `sessions-${this.sanitizeCollectionNameSegment(this.agentId)}`;
|
|
1106
|
+
if (!existing.has(base)) return base;
|
|
1107
|
+
let counter = 2;
|
|
1108
|
+
let candidate = `${base}-${counter}`;
|
|
1109
|
+
while (existing.has(candidate)) {
|
|
1110
|
+
counter += 1;
|
|
1111
|
+
candidate = `${base}-${counter}`;
|
|
1112
|
+
}
|
|
1113
|
+
return candidate;
|
|
1114
|
+
}
|
|
1115
|
+
sanitizeCollectionNameSegment(input) {
|
|
1116
|
+
return input.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
1117
|
+
}
|
|
1118
|
+
async resolveDocLocation(docid, hints) {
|
|
1119
|
+
if (!docid) return null;
|
|
1120
|
+
const normalized = docid.startsWith("#") ? docid.slice(1) : docid;
|
|
1121
|
+
if (!normalized) return null;
|
|
1122
|
+
const cacheKey = `${hints?.preferredCollection ?? "*"}:${normalized}`;
|
|
1123
|
+
const cached = this.docPathCache.get(cacheKey);
|
|
1124
|
+
if (cached) return cached;
|
|
1125
|
+
const db = this.ensureDb();
|
|
1126
|
+
let rows = [];
|
|
1127
|
+
try {
|
|
1128
|
+
rows = db.prepare("SELECT collection, path FROM documents WHERE hash = ? AND active = 1").all(normalized);
|
|
1129
|
+
if (rows.length === 0) rows = db.prepare("SELECT collection, path FROM documents WHERE hash LIKE ? AND active = 1").all(`${normalized}%`);
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
if (this.isSqliteBusyError(err)) {
|
|
1132
|
+
log.debug(`qmd index is busy while resolving doc path: ${String(err)}`);
|
|
1133
|
+
throw this.createQmdBusyError(err);
|
|
1134
|
+
}
|
|
1135
|
+
throw err;
|
|
1136
|
+
}
|
|
1137
|
+
if (rows.length === 0) return null;
|
|
1138
|
+
const location = this.pickDocLocation(rows, hints);
|
|
1139
|
+
if (!location) return null;
|
|
1140
|
+
this.docPathCache.set(cacheKey, location);
|
|
1141
|
+
return location;
|
|
1142
|
+
}
|
|
1143
|
+
pickDocLocation(rows, hints) {
|
|
1144
|
+
if (hints?.preferredCollection) for (const row of rows) {
|
|
1145
|
+
if (row.collection !== hints.preferredCollection) continue;
|
|
1146
|
+
const location = this.toDocLocation(row.collection, row.path);
|
|
1147
|
+
if (location) return location;
|
|
1148
|
+
}
|
|
1149
|
+
if (hints?.preferredFile) {
|
|
1150
|
+
const preferred = path.normalize(hints.preferredFile);
|
|
1151
|
+
for (const row of rows) {
|
|
1152
|
+
const rowPath = path.normalize(row.path);
|
|
1153
|
+
if (rowPath !== preferred && !rowPath.endsWith(path.sep + preferred)) continue;
|
|
1154
|
+
const location = this.toDocLocation(row.collection, row.path);
|
|
1155
|
+
if (location) return location;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
for (const row of rows) {
|
|
1159
|
+
const location = this.toDocLocation(row.collection, row.path);
|
|
1160
|
+
if (location) return location;
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
extractSnippetLines(snippet) {
|
|
1165
|
+
const match = SNIPPET_HEADER_RE.exec(snippet);
|
|
1166
|
+
if (match) {
|
|
1167
|
+
const start = Number(match[1]);
|
|
1168
|
+
const count = Number(match[2]);
|
|
1169
|
+
if (Number.isFinite(start) && Number.isFinite(count)) return {
|
|
1170
|
+
startLine: start,
|
|
1171
|
+
endLine: start + count - 1
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
startLine: 1,
|
|
1176
|
+
endLine: snippet.split("\n").length
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
readCounts() {
|
|
1180
|
+
try {
|
|
1181
|
+
const rows = this.ensureDb().prepare("SELECT collection, COUNT(*) as c FROM documents WHERE active = 1 GROUP BY collection").all();
|
|
1182
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1183
|
+
for (const source of this.sources) bySource.set(source, {
|
|
1184
|
+
files: 0,
|
|
1185
|
+
chunks: 0
|
|
1186
|
+
});
|
|
1187
|
+
let total = 0;
|
|
1188
|
+
for (const row of rows) {
|
|
1189
|
+
const source = this.collectionRoots.get(row.collection)?.kind ?? "memory";
|
|
1190
|
+
const entry = bySource.get(source) ?? {
|
|
1191
|
+
files: 0,
|
|
1192
|
+
chunks: 0
|
|
1193
|
+
};
|
|
1194
|
+
entry.files += row.c ?? 0;
|
|
1195
|
+
entry.chunks += row.c ?? 0;
|
|
1196
|
+
bySource.set(source, entry);
|
|
1197
|
+
total += row.c ?? 0;
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
totalDocuments: total,
|
|
1201
|
+
sourceCounts: Array.from(bySource.entries()).map(([source, value]) => ({
|
|
1202
|
+
source,
|
|
1203
|
+
files: value.files,
|
|
1204
|
+
chunks: value.chunks
|
|
1205
|
+
}))
|
|
1206
|
+
};
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
log.warn(`failed to read qmd index stats: ${String(err)}`);
|
|
1209
|
+
return {
|
|
1210
|
+
totalDocuments: 0,
|
|
1211
|
+
sourceCounts: Array.from(this.sources).map((source) => ({
|
|
1212
|
+
source,
|
|
1213
|
+
files: 0,
|
|
1214
|
+
chunks: 0
|
|
1215
|
+
}))
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
logScopeDenied(sessionKey) {
|
|
1220
|
+
const channel = deriveQmdScopeChannel(sessionKey) ?? "unknown";
|
|
1221
|
+
const chatType = deriveQmdScopeChatType(sessionKey) ?? "unknown";
|
|
1222
|
+
const key = sessionKey?.trim() || "<none>";
|
|
1223
|
+
log.warn(`qmd search denied by scope (channel=${channel}, chatType=${chatType}, session=${key})`);
|
|
1224
|
+
}
|
|
1225
|
+
isScopeAllowed(sessionKey) {
|
|
1226
|
+
return isQmdScopeAllowed(this.qmd.scope, sessionKey);
|
|
1227
|
+
}
|
|
1228
|
+
toDocLocation(collection, collectionRelativePath) {
|
|
1229
|
+
const root = this.collectionRoots.get(collection);
|
|
1230
|
+
if (!root) return null;
|
|
1231
|
+
const normalizedRelative = collectionRelativePath.replace(/\\/g, "/");
|
|
1232
|
+
const absPath = path.normalize(path.resolve(root.path, collectionRelativePath));
|
|
1233
|
+
const relativeToWorkspace = path.relative(this.workspaceDir, absPath);
|
|
1234
|
+
return {
|
|
1235
|
+
rel: this.buildSearchPath(collection, normalizedRelative, relativeToWorkspace, absPath),
|
|
1236
|
+
abs: absPath,
|
|
1237
|
+
source: root.kind
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
buildSearchPath(collection, collectionRelativePath, relativeToWorkspace, absPath) {
|
|
1241
|
+
if (this.isInsideWorkspace(relativeToWorkspace)) {
|
|
1242
|
+
const normalized = relativeToWorkspace.replace(/\\/g, "/");
|
|
1243
|
+
if (!normalized) return path.basename(absPath);
|
|
1244
|
+
return normalized;
|
|
1245
|
+
}
|
|
1246
|
+
return `qmd/${collection}/${collectionRelativePath.replace(/^\/+/, "")}`;
|
|
1247
|
+
}
|
|
1248
|
+
isInsideWorkspace(relativePath) {
|
|
1249
|
+
if (!relativePath) return true;
|
|
1250
|
+
if (relativePath.startsWith("..")) return false;
|
|
1251
|
+
if (relativePath.startsWith(`..${path.sep}`)) return false;
|
|
1252
|
+
return !path.isAbsolute(relativePath);
|
|
1253
|
+
}
|
|
1254
|
+
resolveReadPath(relPath) {
|
|
1255
|
+
if (relPath.startsWith("qmd/")) {
|
|
1256
|
+
const [, collection, ...rest] = relPath.split("/");
|
|
1257
|
+
if (!collection || rest.length === 0) throw new Error("invalid qmd path");
|
|
1258
|
+
const root = this.collectionRoots.get(collection);
|
|
1259
|
+
if (!root) throw new Error(`unknown qmd collection: ${collection}`);
|
|
1260
|
+
const joined = rest.join("/");
|
|
1261
|
+
const resolved = path.resolve(root.path, joined);
|
|
1262
|
+
if (!this.isWithinRoot(root.path, resolved)) throw new Error("qmd path escapes collection");
|
|
1263
|
+
return resolved;
|
|
1264
|
+
}
|
|
1265
|
+
const absPath = path.resolve(this.workspaceDir, relPath);
|
|
1266
|
+
if (!this.isWithinWorkspace(absPath)) throw new Error("path escapes workspace");
|
|
1267
|
+
return absPath;
|
|
1268
|
+
}
|
|
1269
|
+
isWithinWorkspace(absPath) {
|
|
1270
|
+
const normalizedWorkspace = this.workspaceDir.endsWith(path.sep) ? this.workspaceDir : `${this.workspaceDir}${path.sep}`;
|
|
1271
|
+
if (absPath === this.workspaceDir) return true;
|
|
1272
|
+
return (absPath.endsWith(path.sep) ? absPath : `${absPath}${path.sep}`).startsWith(normalizedWorkspace);
|
|
1273
|
+
}
|
|
1274
|
+
isWithinRoot(root, candidate) {
|
|
1275
|
+
const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
1276
|
+
if (candidate === root) return true;
|
|
1277
|
+
return (candidate.endsWith(path.sep) ? candidate : `${candidate}${path.sep}`).startsWith(normalizedRoot);
|
|
1278
|
+
}
|
|
1279
|
+
clampResultsByInjectedChars(results) {
|
|
1280
|
+
const budget = this.qmd.limits.maxInjectedChars;
|
|
1281
|
+
if (!budget || budget <= 0) return results;
|
|
1282
|
+
let remaining = budget;
|
|
1283
|
+
const clamped = [];
|
|
1284
|
+
for (const entry of results) {
|
|
1285
|
+
if (remaining <= 0) break;
|
|
1286
|
+
const snippet = entry.snippet ?? "";
|
|
1287
|
+
if (snippet.length <= remaining) {
|
|
1288
|
+
clamped.push(entry);
|
|
1289
|
+
remaining -= snippet.length;
|
|
1290
|
+
} else {
|
|
1291
|
+
const trimmed = snippet.slice(0, Math.max(0, remaining));
|
|
1292
|
+
clamped.push({
|
|
1293
|
+
...entry,
|
|
1294
|
+
snippet: trimmed
|
|
1295
|
+
});
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return clamped;
|
|
1300
|
+
}
|
|
1301
|
+
diversifyResultsBySource(results, limit) {
|
|
1302
|
+
const target = Math.max(0, limit);
|
|
1303
|
+
if (target <= 0) return [];
|
|
1304
|
+
if (results.length <= 1) return results.slice(0, target);
|
|
1305
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1306
|
+
for (const entry of results) {
|
|
1307
|
+
const list = bySource.get(entry.source) ?? [];
|
|
1308
|
+
list.push(entry);
|
|
1309
|
+
bySource.set(entry.source, list);
|
|
1310
|
+
}
|
|
1311
|
+
const hasSessions = bySource.has("sessions");
|
|
1312
|
+
const hasMemory = bySource.has("memory");
|
|
1313
|
+
if (!hasSessions || !hasMemory) return results.slice(0, target);
|
|
1314
|
+
const sourceOrder = Array.from(bySource.entries()).toSorted((a, b) => (b[1][0]?.score ?? 0) - (a[1][0]?.score ?? 0)).map(([source]) => source);
|
|
1315
|
+
const diversified = [];
|
|
1316
|
+
while (diversified.length < target) {
|
|
1317
|
+
let emitted = false;
|
|
1318
|
+
for (const source of sourceOrder) {
|
|
1319
|
+
const next = bySource.get(source)?.shift();
|
|
1320
|
+
if (!next) continue;
|
|
1321
|
+
diversified.push(next);
|
|
1322
|
+
emitted = true;
|
|
1323
|
+
if (diversified.length >= target) break;
|
|
1324
|
+
}
|
|
1325
|
+
if (!emitted) break;
|
|
1326
|
+
}
|
|
1327
|
+
return diversified;
|
|
1328
|
+
}
|
|
1329
|
+
shouldSkipUpdate(force) {
|
|
1330
|
+
if (force) return false;
|
|
1331
|
+
const debounceMs = this.qmd.update.debounceMs;
|
|
1332
|
+
if (debounceMs <= 0) return false;
|
|
1333
|
+
if (!this.lastUpdateAt) return false;
|
|
1334
|
+
return Date.now() - this.lastUpdateAt < debounceMs;
|
|
1335
|
+
}
|
|
1336
|
+
isSqliteBusyError(err) {
|
|
1337
|
+
const normalized = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
1338
|
+
return normalized.includes("sqlite_busy") || normalized.includes("database is locked");
|
|
1339
|
+
}
|
|
1340
|
+
isUnsupportedQmdOptionError(err) {
|
|
1341
|
+
const normalized = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
1342
|
+
return normalized.includes("unknown flag") || normalized.includes("unknown option") || normalized.includes("unrecognized option") || normalized.includes("flag provided but not defined") || normalized.includes("unexpected argument");
|
|
1343
|
+
}
|
|
1344
|
+
createQmdBusyError(err) {
|
|
1345
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1346
|
+
return /* @__PURE__ */ new Error(`qmd index busy while reading results: ${message}`);
|
|
1347
|
+
}
|
|
1348
|
+
async waitForPendingUpdateBeforeSearch() {
|
|
1349
|
+
const pending = this.pendingUpdate;
|
|
1350
|
+
if (!pending) return;
|
|
1351
|
+
await Promise.race([pending.catch(() => void 0), new Promise((resolve) => setTimeout(resolve, SEARCH_PENDING_UPDATE_WAIT_MS))]);
|
|
1352
|
+
}
|
|
1353
|
+
async runQueryAcrossCollections(query, limit, collectionNames, command) {
|
|
1354
|
+
log.debug(`qmd ${command} multi-collection workaround active (${collectionNames.length} collections)`);
|
|
1355
|
+
const bestByDocId = /* @__PURE__ */ new Map();
|
|
1356
|
+
for (const collectionName of collectionNames) {
|
|
1357
|
+
const args = this.buildSearchArgs(command, query, limit);
|
|
1358
|
+
args.push("-c", collectionName);
|
|
1359
|
+
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
|
1360
|
+
const parsed = parseQmdQueryJson(result.stdout, result.stderr);
|
|
1361
|
+
for (const entry of parsed) {
|
|
1362
|
+
const normalizedDocId = typeof entry.docid === "string" && entry.docid.trim().length > 0 ? entry.docid : void 0;
|
|
1363
|
+
if (!normalizedDocId) continue;
|
|
1364
|
+
const withCollection = {
|
|
1365
|
+
...entry,
|
|
1366
|
+
docid: normalizedDocId,
|
|
1367
|
+
collection: entry.collection ?? collectionName
|
|
1368
|
+
};
|
|
1369
|
+
const prev = bestByDocId.get(normalizedDocId);
|
|
1370
|
+
const prevScore = typeof prev?.score === "number" ? prev.score : Number.NEGATIVE_INFINITY;
|
|
1371
|
+
const nextScore = typeof withCollection.score === "number" ? withCollection.score : Number.NEGATIVE_INFINITY;
|
|
1372
|
+
if (!prev || nextScore > prevScore) bestByDocId.set(normalizedDocId, withCollection);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return [...bestByDocId.values()].toSorted((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
1376
|
+
}
|
|
1377
|
+
async runMcporterAcrossCollections(params) {
|
|
1378
|
+
const bestByDocId = /* @__PURE__ */ new Map();
|
|
1379
|
+
for (const collectionName of params.collectionNames) {
|
|
1380
|
+
const parsed = await this.runQmdSearchViaMcporter({
|
|
1381
|
+
mcporter: this.qmd.mcporter,
|
|
1382
|
+
tool: params.tool,
|
|
1383
|
+
query: params.query,
|
|
1384
|
+
limit: params.limit,
|
|
1385
|
+
minScore: params.minScore,
|
|
1386
|
+
collection: collectionName,
|
|
1387
|
+
timeoutMs: this.qmd.limits.timeoutMs
|
|
1388
|
+
});
|
|
1389
|
+
for (const entry of parsed) {
|
|
1390
|
+
if (typeof entry.docid !== "string" || !entry.docid.trim()) continue;
|
|
1391
|
+
const prev = bestByDocId.get(entry.docid);
|
|
1392
|
+
const prevScore = typeof prev?.score === "number" ? prev.score : Number.NEGATIVE_INFINITY;
|
|
1393
|
+
const nextScore = typeof entry.score === "number" ? entry.score : Number.NEGATIVE_INFINITY;
|
|
1394
|
+
if (!prev || nextScore > prevScore) bestByDocId.set(entry.docid, entry);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return [...bestByDocId.values()].toSorted((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
1398
|
+
}
|
|
1399
|
+
listManagedCollectionNames() {
|
|
1400
|
+
return this.managedCollectionNames;
|
|
1401
|
+
}
|
|
1402
|
+
computeManagedCollectionNames() {
|
|
1403
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1404
|
+
const names = [];
|
|
1405
|
+
for (const collection of this.qmd.collections) {
|
|
1406
|
+
const name = collection.name?.trim();
|
|
1407
|
+
if (!name || seen.has(name)) continue;
|
|
1408
|
+
seen.add(name);
|
|
1409
|
+
names.push(name);
|
|
1410
|
+
}
|
|
1411
|
+
return names;
|
|
1412
|
+
}
|
|
1413
|
+
buildCollectionFilterArgs(collectionNames) {
|
|
1414
|
+
if (collectionNames.length === 0) return [];
|
|
1415
|
+
return collectionNames.filter(Boolean).flatMap((name) => ["-c", name]);
|
|
1416
|
+
}
|
|
1417
|
+
buildSearchArgs(command, query, limit) {
|
|
1418
|
+
const normalizedQuery = command === "search" ? normalizeHanBm25Query(query) : query;
|
|
1419
|
+
if (command === "query") return [
|
|
1420
|
+
"query",
|
|
1421
|
+
normalizedQuery,
|
|
1422
|
+
"--json",
|
|
1423
|
+
"-n",
|
|
1424
|
+
String(limit)
|
|
1425
|
+
];
|
|
1426
|
+
return [
|
|
1427
|
+
command,
|
|
1428
|
+
normalizedQuery,
|
|
1429
|
+
"--json",
|
|
1430
|
+
"-n",
|
|
1431
|
+
String(limit)
|
|
1432
|
+
];
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
function appendOutputWithCap(current, chunk, maxChars) {
|
|
1436
|
+
const appended = current + chunk;
|
|
1437
|
+
if (appended.length <= maxChars) return {
|
|
1438
|
+
text: appended,
|
|
1439
|
+
truncated: false
|
|
1440
|
+
};
|
|
1441
|
+
return {
|
|
1442
|
+
text: appended.slice(-maxChars),
|
|
1443
|
+
truncated: true
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
//#endregion
|
|
1448
|
+
export { QmdMemoryManager };
|