@spacebar_ai/moldclaw-core 2026.3.41 → 2026.3.44
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/accounts-5qY-dKca.d.ts +103 -0
- package/dist/accounts-SqdHz2ZP.js +114 -0
- package/dist/acp-cli-E6bcNqiE.js +2093 -0
- package/dist/actions.runtime-BU_XMuLk.js +119 -0
- package/dist/actions.runtime-CY5h8lqH.js +133 -0
- package/dist/agent-scope-lZlwP1At.js +208 -0
- package/dist/agents-C4SkadR1.js +853 -0
- package/dist/agents-RfwqGCzE.js +222 -0
- package/dist/agents.config-CX9CPNfP.js +17 -0
- package/dist/agents.config-DF9Zwn9n.js +121 -0
- package/dist/allow-list-3WSjz1zl.js +81 -0
- package/dist/allowlist-DNbDjFjw.js +142 -0
- package/dist/api-BEOpJ7dR.js +117 -0
- package/dist/audit-CpJz_eu6.js +787 -0
- package/dist/audit-CpfSjvyo.js +54 -0
- package/dist/audit-channel.collect.runtime-BeGotloZ.js +605 -0
- package/dist/audit-channel.runtime-BJDZ7ETt.js +121 -0
- package/dist/audit-extra.async-C2G0mqmk.js +813 -0
- package/dist/audit-membership-runtime-B1FqJsPV.js +162 -0
- package/dist/audit.deep.runtime-DyL9O_sU.js +25 -0
- package/dist/audit.nondeep.runtime-C6jFgJfH.js +832 -0
- package/dist/audit.runtime-Dnlsn23e.js +118 -0
- package/dist/auth-Ch3Rchm4.js +101 -0
- package/dist/auth-choice-CEFSlnLT.js +122 -0
- package/dist/auth-choice-CVCef-eU.js +268 -0
- package/dist/auth-choice-Cez-pXrg.js +507 -0
- package/dist/auth-choice-options-DO78mvPe.js +123 -0
- package/dist/auth-choice-prompt-CUkC7Mmb.js +36 -0
- package/dist/auth-choice-prompt-DCuQRiVl.js +115 -0
- package/dist/auth-choice.apply-helpers-BhbNIV8X.js +66 -0
- package/dist/auth-choice.plugin-providers.runtime-4BhqvEw_.js +119 -0
- package/dist/auth-profiles-smABVXzp.js +128040 -0
- package/dist/auth-profiles.runtime-Cr-ojtTc.js +116 -0
- package/dist/banner-CojBHPWr.js +342 -0
- package/dist/bluebubbles-BnLsj2Fy.d.ts +6 -0
- package/dist/bluebubbles-CVk7M3Bl.js +64 -0
- package/dist/bot-DdyrB2z9.d.ts +478 -0
- package/dist/brave-w4Fo8WZ3.js +24 -0
- package/dist/browser-cli-DWFs3P_i.js +1494 -0
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.d.ts +1 -1
- package/dist/bundled/boot-md/handler.js +35 -35
- package/dist/bundled/bootstrap-extra-files/handler.d.ts +1 -1
- package/dist/bundled/bootstrap-extra-files/handler.js +1 -1
- package/dist/bundled/command-logger/handler.d.ts +1 -1
- package/dist/bundled/session-memory/handler.d.ts +1 -1
- package/dist/bundled/session-memory/handler.js +36 -36
- package/dist/call-Do7wTSr7.js +39 -0
- package/dist/call-gdDAt07d.js +640 -0
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/channel-B26pkce0.js +214 -0
- package/dist/channel-BJHp0AQC.js +352 -0
- package/dist/channel-BKFOv51P.js +4681 -0
- package/dist/channel-BNgpOY8v.js +538 -0
- package/dist/channel-BcQAAo2P.js +226 -0
- package/dist/channel-BvNdnhbx.js +1598 -0
- package/dist/channel-C1Rda3Jd.js +306 -0
- package/dist/channel-C87DG-F7.js +803 -0
- package/dist/channel-CIip0kvZ.js +619 -0
- package/dist/channel-CTPxoT_E2.js +316 -0
- package/dist/channel-CklaCzUG.js +562 -0
- package/dist/channel-CoJnAdLs.js +920 -0
- package/dist/channel-D3tafL1_.js +949 -0
- package/dist/channel-DFMrP2uu.js +542 -0
- package/dist/channel-DMd5cJQe.js +397 -0
- package/dist/channel-Dm34kxAJ.js +207 -0
- package/dist/channel-DmwF9udn.js +1321 -0
- package/dist/channel-account-context-Bjur9nlh.js +103 -0
- package/dist/channel-bGnST659.js +943 -0
- package/dist/channel-hIgbkTZf.js +575 -0
- package/dist/channel-m_TGrDKo.js +497 -0
- package/dist/channel-options-DoUPBMa8.js +50 -0
- package/dist/channel-plugin-ids-TZIY4hFs.js +26 -0
- package/dist/channel-summary-qD54bOBO.js +111 -0
- package/dist/channel.runtime-B0H04Dkk.js +199 -0
- package/dist/channel.runtime-BU1f3NkV.js +418 -0
- package/dist/channel.runtime-Bj1sfLep.js +4011 -0
- package/dist/channel.runtime-BtPAAJc3.js +870 -0
- package/dist/channel.runtime-Bx-10m_j.js +171 -0
- package/dist/channel.runtime-CI_TBywQ.js +179 -0
- package/dist/channel.runtime-CSLj14-Z.js +182 -0
- package/dist/channel.runtime-D-lTSYAd.js +404 -0
- package/dist/channel.runtime-DJqIOSji.js +127 -0
- package/dist/channel.runtime-Ec8aQ9V2.js +241 -0
- package/dist/channel.runtime-ax5a1jBm.js +218 -0
- package/dist/channel.setup-B-ncdYLT.js +9 -0
- package/dist/channel.setup-BY4bh5dm.js +9 -0
- package/dist/channel.setup-BovsdMnL.js +57 -0
- package/dist/channel.setup-CXzXA25h.js +6 -0
- package/dist/channel.setup-DcZUEufN.js +8 -0
- package/dist/channel.setup-E6zceRsE.js +8 -0
- package/dist/channel.setup-Pc7nGbdX.js +11 -0
- package/dist/channels/plugins/actions/discord.d.ts +2 -2
- package/dist/channels/plugins/actions/discord.js +35 -35
- package/dist/channels/plugins/actions/signal.d.ts +1 -1
- package/dist/channels/plugins/actions/signal.js +35 -35
- package/dist/channels/plugins/actions/telegram.d.ts +2 -2
- package/dist/channels/plugins/actions/telegram.js +35 -35
- package/dist/channels/plugins/agent-tools/whatsapp-login.d.ts +3 -3
- package/dist/channels/plugins/agent-tools/whatsapp-login.js +35 -35
- package/dist/channels-CPtE5ND6.js +404 -0
- package/dist/channels-Cj8ZolHI.js +1118 -0
- package/dist/channels-cli-D2sKrntt.js +291 -0
- package/dist/channels-status-issues-CzIHODg2.js +16 -0
- package/dist/clawbot-cli-BcwEDmUn.js +118 -0
- package/dist/cleanup-utils-D0L17RsX.js +96 -0
- package/dist/cli/daemon-cli.js +1 -1
- package/dist/cli-BvGVPKnD.js +154 -0
- package/dist/command-registry-CADQzTAg.js +14 -0
- package/dist/command-registry-ktiJNAJd.js +242 -0
- package/dist/command-secret-gateway-CXp10RTM.js +111 -0
- package/dist/compact.runtime-DyKL-Iar.js +116 -0
- package/dist/completion-cli-Bz4STrpt.js +17 -0
- package/dist/completion-cli-pVda2OFb.js +445 -0
- package/dist/config-BbvDRSYp.js +31 -0
- package/dist/config-CwBv71QC.js +44 -0
- package/dist/config-cli-Y0uXHbOw.js +678 -0
- package/dist/config-guard-BpW5g7JE.js +118 -0
- package/dist/config-validation-B-vLIsbo.js +262 -0
- package/dist/config-value-DT3-5958.js +132 -0
- package/dist/configure-B9U-jCqP.js +1100 -0
- package/dist/configure-BJ3Wrs5b.js +243 -0
- package/dist/control-ui-assets-C1YDYi82.js +232 -0
- package/dist/control-ui-shared-Dm5Dh0Lo.js +29 -0
- package/dist/core-BwKq3krw.js +150 -0
- package/dist/core-hjBwfDsW.d.ts +87 -0
- package/dist/cron-cli-DTDgfoMh.js +639 -0
- package/dist/daemon-cli-C-dkAXR1.js +339 -0
- package/dist/daemon-install-Oy0Q5pMF.js +180 -0
- package/dist/deliver-DNGnDqF9.js +111 -0
- package/dist/deliver-runtime-CCNZIhET.js +111 -0
- package/dist/device-id-cli-XvwZbIyC.js +52 -0
- package/dist/device-identity-IG5DngWM.js +365 -0
- package/dist/devices-cli-DIsxj4xp.js +342 -0
- package/dist/diagnostic-DTPopFvh.js +310 -0
- package/dist/directory-cli-DTSY3Ktr.js +311 -0
- package/dist/directory-config-helpers-DpFcAbmo.d.ts +38 -0
- package/dist/directory.static-CBRAUwUW.js +44 -0
- package/dist/discord-CrgxhEWw.js +114 -0
- package/dist/discovery-DrG7wmAR.js +48 -0
- package/dist/dm-policy-shared-DKoGdUpY.d.ts +95 -0
- package/dist/dns-cli-BJiz6CLK.js +217 -0
- package/dist/docs-cli-Dq2Yi5qO.js +174 -0
- package/dist/doctor-completion-D3GeVcFP.js +90 -0
- package/dist/doctor-config-flow-B1cMjr8h.js +112 -0
- package/dist/doctor-config-flow-BUe7JpV3.js +2437 -0
- package/dist/enable-Bc8bCuVe.js +24 -0
- package/dist/entry.js +4 -4
- package/dist/exec-approvals-cli-kLAev6bP.js +421 -0
- package/dist/extensions/acpx/index.d.ts +1 -1
- package/dist/extensions/amazon-bedrock/index.d.ts +1 -1
- package/dist/extensions/amazon-bedrock/index.js +4 -4
- package/dist/extensions/anthropic/index.d.ts +1 -1
- package/dist/extensions/anthropic/index.js +35 -35
- package/dist/extensions/bluebubbles/index.d.ts +1 -1
- package/dist/extensions/bluebubbles/index.js +39 -39
- package/dist/extensions/bluebubbles/setup-entry.d.ts +2 -2
- package/dist/extensions/bluebubbles/setup-entry.js +39 -39
- package/dist/extensions/brave/index.d.ts +1 -1
- package/dist/extensions/brave/index.js +5 -5
- package/dist/extensions/byteplus/index.d.ts +1 -1
- package/dist/extensions/byteplus/index.js +35 -35
- package/dist/extensions/cloudflare-ai-gateway/index.d.ts +1 -1
- package/dist/extensions/cloudflare-ai-gateway/index.js +36 -36
- package/dist/extensions/copilot-proxy/index.d.ts +1 -1
- package/dist/extensions/copilot-proxy/index.js +4 -4
- package/dist/extensions/device-pair/index.d.ts +1 -1
- package/dist/extensions/device-pair/index.js +4 -4
- package/dist/extensions/diagnostics-otel/index.d.ts +1 -1
- package/dist/extensions/diagnostics-otel/index.js +4 -4
- package/dist/extensions/diffs/index.d.ts +1 -1
- package/dist/extensions/discord/index.d.ts +1 -1
- package/dist/extensions/discord/index.js +40 -40
- package/dist/extensions/discord/setup-entry.d.ts +1 -1
- package/dist/extensions/discord/setup-entry.js +38 -38
- package/dist/extensions/elevenlabs/index.d.ts +1 -1
- package/dist/extensions/elevenlabs/index.js +35 -35
- package/dist/extensions/feishu/index.d.ts +2 -2
- package/dist/extensions/feishu/index.js +40 -40
- package/dist/extensions/feishu/setup-entry.d.ts +2 -2
- package/dist/extensions/feishu/setup-entry.js +37 -37
- package/dist/extensions/firecrawl/index.d.ts +1 -1
- package/dist/extensions/firecrawl/index.js +35 -35
- package/dist/extensions/github-copilot/index.d.ts +1 -1
- package/dist/extensions/github-copilot/index.js +35 -35
- package/dist/extensions/google/index.d.ts +1 -1
- package/dist/extensions/google/index.js +35 -35
- package/dist/extensions/googlechat/index.d.ts +1 -1
- package/dist/extensions/googlechat/index.js +38 -38
- package/dist/extensions/googlechat/setup-entry.d.ts +1 -1
- package/dist/extensions/googlechat/setup-entry.js +38 -38
- package/dist/extensions/huggingface/index.d.ts +1 -1
- package/dist/extensions/huggingface/index.js +35 -35
- package/dist/extensions/imessage/index.d.ts +1 -1
- package/dist/extensions/imessage/index.js +39 -39
- package/dist/extensions/imessage/setup-entry.d.ts +1 -1
- package/dist/extensions/imessage/setup-entry.js +39 -39
- package/dist/extensions/irc/index.d.ts +1 -1
- package/dist/extensions/irc/index.js +38 -38
- package/dist/extensions/irc/setup-entry.d.ts +2 -2
- package/dist/extensions/irc/setup-entry.js +38 -38
- package/dist/extensions/kakao-talkchannel/index.d.ts +1 -1
- package/dist/extensions/kakao-talkchannel/index.js +4 -4
- package/dist/extensions/kilocode/index.d.ts +1 -1
- package/dist/extensions/kilocode/index.js +35 -35
- package/dist/extensions/kimi-coding/index.d.ts +1 -1
- package/dist/extensions/kimi-coding/index.js +35 -35
- package/dist/extensions/line/index.d.ts +1 -1
- package/dist/extensions/line/index.js +37 -37
- package/dist/extensions/line/setup-entry.d.ts +1 -1
- package/dist/extensions/line/setup-entry.js +37 -37
- package/dist/extensions/llm-task/index.d.ts +1 -1
- package/dist/extensions/llm-task/index.js +35 -35
- package/dist/extensions/lobster/index.d.ts +1 -1
- package/dist/extensions/lobster/index.js +4 -4
- package/dist/extensions/matrix/index.d.ts +1 -1
- package/dist/extensions/matrix/index.js +40 -40
- package/dist/extensions/matrix/setup-entry.d.ts +2 -2
- package/dist/extensions/matrix/setup-entry.js +40 -40
- package/dist/extensions/mattermost/index.d.ts +1 -1
- package/dist/extensions/mattermost/index.js +37 -37
- package/dist/extensions/mattermost/setup-entry.d.ts +2 -2
- package/dist/extensions/mattermost/setup-entry.js +37 -37
- package/dist/extensions/memory-core/index.d.ts +1 -1
- package/dist/extensions/memory-core/index.js +4 -4
- package/dist/extensions/memory-lancedb/index.d.ts +1 -1
- package/dist/extensions/memory-lancedb/index.js +4 -4
- package/dist/extensions/microsoft/index.d.ts +1 -1
- package/dist/extensions/microsoft/index.js +35 -35
- package/dist/extensions/minimax/index.d.ts +1 -1
- package/dist/extensions/minimax/index.js +35 -35
- package/dist/extensions/mistral/index.d.ts +1 -1
- package/dist/extensions/mistral/index.js +35 -35
- package/dist/extensions/modelstudio/index.d.ts +1 -1
- package/dist/extensions/modelstudio/index.js +35 -35
- package/dist/extensions/moonshot/index.d.ts +1 -1
- package/dist/extensions/moonshot/index.js +35 -35
- package/dist/extensions/msteams/index.d.ts +1 -1
- package/dist/extensions/msteams/index.js +40 -40
- package/dist/extensions/msteams/setup-entry.d.ts +1 -1
- package/dist/extensions/msteams/setup-entry.js +40 -40
- package/dist/extensions/nextcloud-talk/index.d.ts +1 -1
- package/dist/extensions/nextcloud-talk/index.js +37 -37
- package/dist/extensions/nextcloud-talk/setup-entry.d.ts +2 -2
- package/dist/extensions/nextcloud-talk/setup-entry.js +37 -37
- package/dist/extensions/nostr/index.d.ts +1 -1
- package/dist/extensions/nostr/index.js +37 -37
- package/dist/extensions/nostr/setup-entry.d.ts +1 -1
- package/dist/extensions/nostr/setup-entry.js +37 -37
- package/dist/extensions/nvidia/index.d.ts +1 -1
- package/dist/extensions/nvidia/index.js +4 -4
- package/dist/extensions/ollama/index.d.ts +1 -1
- package/dist/extensions/ollama/index.js +7 -7
- package/dist/extensions/open-prose/index.d.ts +1 -1
- package/dist/extensions/open-prose/index.js +4 -4
- package/dist/extensions/openai/index.d.ts +1 -1
- package/dist/extensions/openai/index.js +35 -35
- package/dist/extensions/opencode/index.d.ts +1 -1
- package/dist/extensions/opencode/index.js +35 -35
- package/dist/extensions/opencode-go/index.d.ts +1 -1
- package/dist/extensions/opencode-go/index.js +35 -35
- package/dist/extensions/openrouter/index.d.ts +1 -1
- package/dist/extensions/openrouter/index.js +35 -35
- package/dist/extensions/openshell/index.d.ts +1 -1
- package/dist/extensions/openshell/index.js +35 -35
- package/dist/extensions/perplexity/index.d.ts +1 -1
- package/dist/extensions/perplexity/index.js +5 -5
- package/dist/extensions/phone-control/index.d.ts +1 -1
- package/dist/extensions/phone-control/index.js +4 -4
- package/dist/extensions/qianfan/index.d.ts +1 -1
- package/dist/extensions/qianfan/index.js +35 -35
- package/dist/extensions/qwen-portal-auth/index.d.ts +1 -1
- package/dist/extensions/qwen-portal-auth/index.js +35 -35
- package/dist/extensions/sglang/index.d.ts +1 -1
- package/dist/extensions/sglang/index.js +35 -35
- package/dist/extensions/signal/index.d.ts +1 -1
- package/dist/extensions/signal/index.js +38 -38
- package/dist/extensions/signal/setup-entry.d.ts +1 -1
- package/dist/extensions/signal/setup-entry.js +38 -38
- package/dist/extensions/slack/index.d.ts +1 -1
- package/dist/extensions/slack/index.js +39 -39
- package/dist/extensions/slack/setup-entry.d.ts +1 -1
- package/dist/extensions/slack/setup-entry.js +38 -38
- package/dist/extensions/synology-chat/index.d.ts +1 -1
- package/dist/extensions/synology-chat/index.js +37 -37
- package/dist/extensions/synology-chat/setup-entry.d.ts +1 -1
- package/dist/extensions/synology-chat/setup-entry.js +37 -37
- package/dist/extensions/synthetic/index.d.ts +1 -1
- package/dist/extensions/synthetic/index.js +35 -35
- package/dist/extensions/talk-voice/index.d.ts +1 -1
- package/dist/extensions/talk-voice/index.js +35 -35
- package/dist/extensions/telegram/index.d.ts +1 -1
- package/dist/extensions/telegram/index.js +38 -38
- package/dist/extensions/telegram/setup-entry.d.ts +1 -1
- package/dist/extensions/telegram/setup-entry.js +37 -37
- package/dist/extensions/thread-ownership/index.d.ts +1 -1
- package/dist/extensions/thread-ownership/index.js +4 -4
- package/dist/extensions/tlon/index.d.ts +1 -1
- package/dist/extensions/tlon/index.js +37 -37
- package/dist/extensions/tlon/setup-entry.d.ts +1 -1
- package/dist/extensions/tlon/setup-entry.js +37 -37
- package/dist/extensions/together/index.d.ts +1 -1
- package/dist/extensions/together/index.js +35 -35
- package/dist/extensions/twitch/index.d.ts +2 -2
- package/dist/extensions/twitch/index.js +37 -37
- package/dist/extensions/venice/index.d.ts +1 -1
- package/dist/extensions/venice/index.js +35 -35
- package/dist/extensions/vercel-ai-gateway/index.d.ts +1 -1
- package/dist/extensions/vercel-ai-gateway/index.js +36 -36
- package/dist/extensions/vllm/index.d.ts +1 -1
- package/dist/extensions/vllm/index.js +35 -35
- package/dist/extensions/voice-call/index.d.ts +1 -1
- package/dist/extensions/voice-call/index.js +35 -35
- package/dist/extensions/volcengine/index.d.ts +1 -1
- package/dist/extensions/volcengine/index.js +35 -35
- package/dist/extensions/whatsapp/index.d.ts +1 -1
- package/dist/extensions/whatsapp/index.js +38 -38
- package/dist/extensions/whatsapp/setup-entry.d.ts +1 -1
- package/dist/extensions/whatsapp/setup-entry.js +38 -38
- package/dist/extensions/xai/index.d.ts +1 -1
- package/dist/extensions/xai/index.js +35 -35
- package/dist/extensions/xiaomi/index.d.ts +1 -1
- package/dist/extensions/xiaomi/index.js +35 -35
- package/dist/extensions/zai/index.d.ts +1 -1
- package/dist/extensions/zai/index.js +35 -35
- package/dist/extensions/zalo/index.d.ts +1 -1
- package/dist/extensions/zalo/index.js +39 -39
- package/dist/extensions/zalo/setup-entry.d.ts +1 -1
- package/dist/extensions/zalo/setup-entry.js +39 -39
- package/dist/extensions/zalouser/index.d.ts +1 -1
- package/dist/extensions/zalouser/index.js +40 -40
- package/dist/extensions/zalouser/setup-entry.d.ts +1 -1
- package/dist/extensions/zalouser/setup-entry.js +40 -40
- package/dist/feishu-fIcnHDTd.d.ts +36 -0
- package/dist/gateway-cli-0c-8h93_.js +26437 -0
- package/dist/gateway-install-token-1PwJvrBY.js +163 -0
- package/dist/gateway-rpc-C0Vk51W7.js +26 -0
- package/dist/gateway-runtime-CBm3CCoA.js +69 -0
- package/dist/git-commit-BTWXFY41.js +177 -0
- package/dist/git-commit-D6GTN5Yt.js +2 -0
- package/dist/googlechat-BQr4xgoZ.js +307 -0
- package/dist/googlechat-BvwsCVKl.d.ts +12 -0
- package/dist/group-access-DpiQnd-G.d.ts +61 -0
- package/dist/health-6yZQGADY.js +113 -0
- package/dist/health-C9DYGyRe.js +570 -0
- package/dist/heartbeat-summary-Dct2lqJj.js +57 -0
- package/dist/help-CtwSApfq.js +81 -0
- package/dist/hooks-9gokOxZ5.d.ts +6 -0
- package/dist/hooks-cli-BegKzHZT.js +1000 -0
- package/dist/hooks-status-Bm_pGORf.js +78 -0
- package/dist/http-registry-D-S6a1Na.d.ts +20 -0
- package/dist/identity-file-Diub2a0t.js +60 -0
- package/dist/image-generation-CbIVzmAR.d.ts +9 -0
- package/dist/imessage-Bgok9kfl.js +31 -0
- package/dist/imessage-VIHePprL.js +115 -0
- package/dist/inbound-reply-dispatch-B53GAGWq.js +71 -0
- package/dist/inbound-reply-dispatch-n7U3qg15.d.ts +72 -0
- package/dist/index.js +2 -2
- package/dist/install-target-oz1pjfHH.js +574 -0
- package/dist/installs-CUFm5V8a.js +532 -0
- package/dist/io-BaBxjB1v.js +9739 -0
- package/dist/io-CgHb1Jld.js +29 -0
- package/dist/irc-CaRKzGvW.js +672 -0
- package/dist/library-C5SNBCMb.js +112 -0
- package/dist/lifecycle-core-Dn8PK6nk.js +382 -0
- package/dist/line/accounts.d.ts +2 -2
- package/dist/line/send.d.ts +1 -1
- package/dist/line/send.js +7 -7
- package/dist/line/template-messages.d.ts +1 -1
- package/dist/line-B5QFpgN_.d.ts +75 -0
- package/dist/line-fePrrQOD.js +530 -0
- package/dist/llm-slug-generator-hKae3XDA.js +67 -0
- package/dist/llm-slug-generator.d.ts +1 -1
- package/dist/llm-slug-generator.js +36 -36
- package/dist/logging-CdisccbY.js +13 -0
- package/dist/logging-LKQSgX1d.js +30 -0
- package/dist/login-qr-C1YWh4nE.js +233 -0
- package/dist/login-qr-WFluMDMb.js +112 -0
- package/dist/logs-cli-CNzOvZ2d.js +256 -0
- package/dist/manager-runtime-DgMhLTkR.js +111 -0
- package/dist/manager.runtime-hUWgpPt2.js +715 -0
- package/dist/manifest-registry-CS_p1OBQ.js +1329 -0
- package/dist/matrix-43_RGLZN.d.ts +68 -0
- package/dist/matrix-CCFxHfxa.js +1269 -0
- package/dist/matrix-DWs_qIkJ.js +1495 -0
- package/dist/mcp-cli-Ci2jvv3s.js +87 -0
- package/dist/media-understanding.runtime-Cdr6iTW6.js +116 -0
- package/dist/memory-cli-LZbyF0Iu.js +111 -0
- package/dist/memory-search-BHhETk6u.js +17 -0
- package/dist/memory-search-tTD5o_rU.js +204 -0
- package/dist/method-scopes-B2ZKSsxQ.js +2452 -0
- package/dist/model-auth-markers-LqZ4qhrZ.d.ts +20 -0
- package/dist/model-picker-CTR5mo4v.js +112 -0
- package/dist/model-picker-DG4z_dBs.js +390 -0
- package/dist/model-picker.runtime-DMQ9Pj9_.js +125 -0
- package/dist/model-selection-bBBxfXdb.js +653 -0
- package/dist/model-suppression.runtime-BVG75tZ7.js +116 -0
- package/dist/models-BjkVLfgw.js +2514 -0
- package/dist/models-ZO01Q4cx.js +118 -0
- package/dist/models-cli-DemdF-bm.js +309 -0
- package/dist/models-config-B2Jja8ua.js +111 -0
- package/dist/models-config.providers.discovery-puxTsH39.d.ts +18 -0
- package/dist/moldclaw-root-Cb6HRlUO.js +92 -0
- package/dist/monitor-BP4idxJD.js +782 -0
- package/dist/monitor-B_eP8Eim.js +772 -0
- package/dist/monitor-CRHYNl5J.js +3468 -0
- package/dist/monitor-Ci1Xg4g3.js +113 -0
- package/dist/monitor-DEodDl3z.js +6823 -0
- package/dist/monitor-DJlNKuMz.js +115 -0
- package/dist/monitor-DvFwDS9w.js +3076 -0
- package/dist/monitor-shared--cEjSf8s.js +444 -0
- package/dist/msteams-CV2a8uE8.js +852 -0
- package/dist/node-cli-Of2g7DSd.js +2503 -0
- package/dist/node-resolve-BYC2FbO2.js +835 -0
- package/dist/nodes-cli-CPHM6Upj.js +1380 -0
- package/dist/nostr-BFKRoOlz.d.ts +7 -0
- package/dist/nostr-lHpcBzz4.js +8744 -0
- package/dist/npm-resolution-kqHN85wB.js +60 -0
- package/dist/oauth-env-CLG8KOrz.js +10 -0
- package/dist/onboard-BON0C360.js +48 -0
- package/dist/onboard-CRkIBgOI.js +589 -0
- package/dist/onboard-DsKI17iq.js +25 -0
- package/dist/onboard-channels-BY3IbBBf.js +1241 -0
- package/dist/onboard-channels-CLKdRxvW.js +205 -0
- package/dist/onboard-custom-BjPrMo_R.js +571 -0
- package/dist/onboard-custom-DqcPiZBN.js +114 -0
- package/dist/onboard-helpers-BkrOY5OE.js +113 -0
- package/dist/onboard-helpers-DiSRTpZC.js +335 -0
- package/dist/onboard-hooks-pzEPZAvl.js +72 -0
- package/dist/onboard-remote-ChyLC6Dk.js +181 -0
- package/dist/onboard-remote-DHmK9ntl.js +117 -0
- package/dist/onboard-search-BgA3jEMW.js +302 -0
- package/dist/onboard-skills-BMo_NvnW.js +133 -0
- package/dist/onboard-skills-Bba-Z2p8.js +117 -0
- package/dist/outbound-media-BHD4aJEX.d.ts +11 -0
- package/dist/outbound-media-DSno0N82.js +11 -0
- package/dist/pairing-access-CzHpaM0R.d.ts +21 -0
- package/dist/pairing-cli-CmklqK0q.js +217 -0
- package/dist/perplexity-CXwMDD3u.js +24 -0
- package/dist/persistent-dedupe-B9vrAf8t.d.ts +26 -0
- package/dist/pi-model-discovery-runtime-BrK7tcaO.js +111 -0
- package/dist/pi-tools.before-tool-call.runtime-C5yLUogH.js +381 -0
- package/dist/plugin-install-C4AWJIFP.js +117 -0
- package/dist/plugin-install-CB3J1hfV.js +184 -0
- package/dist/plugin-install-plan-7itZiegi.js +49 -0
- package/dist/plugin-registry-DX_GFoiz.js +113 -0
- package/dist/plugin-registry-e3cxTtvb.js +49 -0
- package/dist/plugin-sdk/account-resolution.js +35 -35
- package/dist/plugin-sdk/acp-runtime.js +35 -35
- package/dist/plugin-sdk/agent-runtime.js +35 -35
- package/dist/plugin-sdk/bluebubbles.js +37 -37
- package/dist/plugin-sdk/channel-config-helpers.js +35 -35
- package/dist/plugin-sdk/channel-policy.js +35 -35
- package/dist/plugin-sdk/channel-runtime.js +35 -35
- package/dist/plugin-sdk/compat.js +36 -36
- package/dist/plugin-sdk/config-runtime.js +35 -35
- package/dist/plugin-sdk/conversation-runtime.js +35 -35
- package/dist/plugin-sdk/copilot-proxy.js +4 -4
- package/dist/plugin-sdk/core.js +4 -4
- package/dist/plugin-sdk/device-pair.js +4 -4
- package/dist/plugin-sdk/discord.js +35 -35
- package/dist/plugin-sdk/feishu.js +35 -35
- package/dist/plugin-sdk/gateway-runtime.js +10 -10
- package/dist/plugin-sdk/googlechat.js +37 -37
- package/dist/plugin-sdk/image-generation-runtime.js +35 -35
- package/dist/plugin-sdk/image-generation.js +35 -35
- package/dist/plugin-sdk/imessage.js +36 -36
- package/dist/plugin-sdk/index.js +35 -35
- package/dist/plugin-sdk/infra-runtime.js +35 -35
- package/dist/plugin-sdk/irc.js +37 -37
- package/dist/plugin-sdk/line.js +36 -36
- package/dist/plugin-sdk/llm-task.js +35 -35
- package/dist/plugin-sdk/lobster.js +4 -4
- package/dist/plugin-sdk/matrix.js +37 -37
- package/dist/plugin-sdk/mattermost.js +36 -36
- package/dist/plugin-sdk/media-runtime.js +35 -35
- package/dist/plugin-sdk/media-understanding-runtime.js +35 -35
- package/dist/plugin-sdk/media-understanding.js +35 -35
- package/dist/plugin-sdk/memory-lancedb.js +4 -4
- package/dist/plugin-sdk/minimax-portal-auth.js +4 -4
- package/dist/plugin-sdk/msteams.js +38 -38
- package/dist/plugin-sdk/nextcloud-talk.js +36 -36
- package/dist/plugin-sdk/nostr.js +36 -36
- package/dist/plugin-sdk/ollama-setup.js +9 -9
- package/dist/plugin-sdk/open-prose.js +4 -4
- package/dist/plugin-sdk/phone-control.js +4 -4
- package/dist/plugin-sdk/plugin-runtime.js +35 -35
- package/dist/plugin-sdk/provider-auth.js +35 -35
- package/dist/plugin-sdk/provider-models.js +5 -5
- package/dist/plugin-sdk/provider-onboard.js +4 -4
- package/dist/plugin-sdk/provider-setup.js +39 -39
- package/dist/plugin-sdk/provider-stream.js +4 -4
- package/dist/plugin-sdk/provider-usage.js +4 -4
- package/dist/plugin-sdk/qwen-portal-auth.js +35 -35
- package/dist/plugin-sdk/reply-history.js +35 -35
- package/dist/plugin-sdk/reply-runtime.js +35 -35
- package/dist/plugin-sdk/routing.js +3 -3
- package/dist/plugin-sdk/sandbox.js +35 -35
- package/dist/plugin-sdk/security-runtime.js +35 -35
- package/dist/plugin-sdk/self-hosted-provider-setup.js +37 -37
- package/dist/plugin-sdk/setup.js +35 -35
- package/dist/plugin-sdk/signal.js +35 -35
- package/dist/plugin-sdk/slack.js +35 -35
- package/dist/plugin-sdk/speech-runtime.js +35 -35
- package/dist/plugin-sdk/speech.js +35 -35
- package/dist/plugin-sdk/src/secrets/secure-file-store.d.ts +26 -0
- package/dist/plugin-sdk/src/subscription/provider.d.ts +5 -3
- package/dist/plugin-sdk/synology-chat.js +36 -36
- package/dist/plugin-sdk/talk-voice.js +4 -4
- package/dist/plugin-sdk/telegram.js +35 -35
- package/dist/plugin-sdk/text-runtime.js +7 -7
- package/dist/plugin-sdk/thread-ownership.js +4 -4
- package/dist/plugin-sdk/tlon.js +36 -36
- package/dist/plugin-sdk/twitch.js +35 -35
- package/dist/plugin-sdk/voice-call.js +35 -35
- package/dist/plugin-sdk/whatsapp.js +35 -35
- package/dist/plugin-sdk/zalo.js +38 -38
- package/dist/plugin-sdk/zalouser.js +38 -38
- package/dist/plugins/runtime/index.d.ts +1 -1
- package/dist/plugins/runtime/index.js +35 -35
- package/dist/plugins-DF5FaTO0.js +111 -0
- package/dist/plugins-cli-CvTJemqC.js +917 -0
- package/dist/policy-CNXISK_a.js +143 -0
- package/dist/preflight-audio.runtime-RP000oxo.js +116 -0
- package/dist/probe-BkM5pykD.js +21 -0
- package/dist/probe-DKbRTJv5.js +1793 -0
- package/dist/probe-DkrfRsjU.js +47 -0
- package/dist/probe-DpcJ0WeP.js +129 -0
- package/dist/probe-auth-BcNjX8hy.js +40 -0
- package/dist/probe-auth-DhuAb8ls.js +48 -0
- package/dist/probe-wciBj-aL.js +6329 -0
- package/dist/program-C8-p0mW5.js +253 -0
- package/dist/prompt-select-styled-DH0pVoc0.js +2673 -0
- package/dist/provider-api-key-auth.runtime-CAFeIQ1u.js +121 -0
- package/dist/provider-auth-choice-CB_HzdTl.js +126 -0
- package/dist/provider-auth-choice-helpers-hzDkh3f1.js +48 -0
- package/dist/provider-auth-choice-preference-BHCXvNSE.js +189 -0
- package/dist/provider-auth-choice.runtime-Dx4ms2C5.js +123 -0
- package/dist/provider-auth-choices-0KaDNPBQ.js +57 -0
- package/dist/provider-auth-guidance-BaAUiNr_.js +34 -0
- package/dist/provider-auth-result-Bto1bYtS.d.ts +18 -0
- package/dist/provider-models-DxOmeToO.d.ts +867 -0
- package/dist/provider-models-xnyxy6mO.js +2113 -0
- package/dist/provider-ollama-setup-DBYK__ov.d.ts +32 -0
- package/dist/provider-ollama-setup-QzgCxj44.js +314 -0
- package/dist/provider-onboard-B9ionepI.js +139 -0
- package/dist/provider-onboard-CURxJ_UX.d.ts +40 -0
- package/dist/provider-runtime.runtime-4xwmsl5L.js +111 -0
- package/dist/provider-self-hosted-setup-BHd24EDG.js +182 -0
- package/dist/provider-self-hosted-setup-qeY8BYSy.d.ts +61 -0
- package/dist/provider-stream-Chz_EFw3.js +512 -0
- package/dist/provider-usage-C11Q7UwS.js +111 -0
- package/dist/provider-usage-kxemdMp2.js +633 -0
- package/dist/provider-wizard-CanJoxNC.js +152 -0
- package/dist/push-apns-Dsajnm8C.js +1038 -0
- package/dist/pw-ai-DUe4BbH2.js +1867 -0
- package/dist/qmd-manager-CAAFp7qK.js +1570 -0
- package/dist/qr-cli-Bu2jqTPY.js +113 -0
- package/dist/qr-cli-Bu9Z-X48.js +369 -0
- package/dist/reactions-Cpfum4iU.js +281 -0
- package/dist/read-only-account-inspect.discord.runtime-BK0LaMgC.js +116 -0
- package/dist/read-only-account-inspect.slack.runtime-DgKiC5wT.js +116 -0
- package/dist/read-only-account-inspect.telegram.runtime-mxfgFVOU.js +116 -0
- package/dist/redact-snapshot-DD8A4tdd.js +2663 -0
- package/dist/register.agent-DU4FtrU2.js +439 -0
- package/dist/register.backup-8nOYtJqg.js +625 -0
- package/dist/register.configure-DmtecqIH.js +252 -0
- package/dist/register.maintenance-Dir3ulKP.js +574 -0
- package/dist/register.message-Cfl-f3Ju.js +709 -0
- package/dist/register.onboard-Bv7WVzEi.js +192 -0
- package/dist/register.setup-BIyeI8RY.js +212 -0
- package/dist/register.status-health-sessions-C69WQcF4.js +498 -0
- package/dist/register.subclis-B_4KCgTd.js +315 -0
- package/dist/register.subclis-BeXsmgBL.js +13 -0
- package/dist/replies-DdcFUmki.js +110 -0
- package/dist/resolve-channels-DRZqPV5o.js +226 -0
- package/dist/resolve-channels-DxW1kqxA.js +262 -0
- package/dist/resolve-route-DdX-HBVt.js +538 -0
- package/dist/resolve-users-rgCQvkLs.js +143 -0
- package/dist/root-help-QAkoA7GD.js +32 -0
- package/dist/routes-CcJNnwTF.js +7097 -0
- package/dist/rpc-DDUAlBbH.js +67 -0
- package/dist/run-main-D9ci5pn7.js +424 -0
- package/dist/runtime-Bitmi8Er.d.ts +26 -0
- package/dist/runtime-discord-ops.runtime-T4sf7aRB.js +9078 -0
- package/dist/runtime-slack-ops.runtime-BQpP48mC.js +4556 -0
- package/dist/runtime-telegram-ops.runtime-cVO5dqOp.js +133 -0
- package/dist/runtime-whatsapp-login.runtime-DtNx0dSY.js +114 -0
- package/dist/runtime-whatsapp-outbound.runtime-Bw47QbFK.js +117 -0
- package/dist/sandbox-cli-DsFwjbjC.js +535 -0
- package/dist/search-manager-BRAK8fEe.js +16 -0
- package/dist/search-manager-BS5Db0A6.js +386 -0
- package/dist/secrets-cli-D3J46TJp.js +2070 -0
- package/dist/security-cli-B866M9cB.js +575 -0
- package/dist/send-B1pX9_Oc.js +283 -0
- package/dist/send-B2RrLg83.js +100 -0
- package/dist/send-DFnV__Aq.js +1025 -0
- package/dist/send-DZIH6CJt.js +629 -0
- package/dist/send-sl9WnKbW.js +631 -0
- package/dist/server-node-events-BT6egg20.js +506 -0
- package/dist/server-zI_K-D05.js +107 -0
- package/dist/sessions-C8kiAcoJ.js +112 -0
- package/dist/sessions-DLBpp52_.js +218 -0
- package/dist/setup-C7eOzMiC.js +387 -0
- package/dist/setup-CFIMq-Pz.d.ts +37 -0
- package/dist/setup-binary-CcAv8NXz.js +406 -0
- package/dist/setup-browser-C4eRV3h6.js +70 -0
- package/dist/setup-core-BnR486P-.js +143 -0
- package/dist/setup-core-CIswIiu5.js +166 -0
- package/dist/setup-core-CcbcrXXg.js +47 -0
- package/dist/setup-core-nZSw5BSv.js +205 -0
- package/dist/setup-surface-C5iSpT4M.js +490 -0
- package/dist/setup-wizard-helpers-r0J6l8ST.d.ts +203 -0
- package/dist/setup.finalize-adiRfo0U.js +522 -0
- package/dist/setup.gateway-config-BwFWKDfT.js +343 -0
- package/dist/shared-12TimyeF.js +182 -0
- package/dist/shared-9EWO34-k.js +298 -0
- package/dist/shared-B4vUbaRR.js +75 -0
- package/dist/shared-bNWpW3Dd.js +96 -0
- package/dist/shared-lU1y5dvS.js +102 -0
- package/dist/signal-DBlETRu9.js +114 -0
- package/dist/skills-Bio8GwTE.js +20 -0
- package/dist/skills-DE_MXFSN.js +853 -0
- package/dist/skills-cli-BGuW-tKw.js +292 -0
- package/dist/skills-install--rnorIoJ.js +763 -0
- package/dist/skills-status-B08PtBc_.js +21 -0
- package/dist/skills-status-CzM008aB.js +169 -0
- package/dist/slack-C4T53Nc-.js +114 -0
- package/dist/slash-commands.runtime-B7fsD8Be.js +128 -0
- package/dist/slash-dispatch.runtime-t0PAX4vQ.js +141 -0
- package/dist/slash-skill-commands.runtime-DIhPnEfR.js +116 -0
- package/dist/src-DrDirlvw.js +1701 -0
- package/dist/status-Bld14WSA.js +131 -0
- package/dist/status-CgeO4RuH.js +43 -0
- package/dist/status-HlvixAOq.js +606 -0
- package/dist/status-Rom_Lf3c.js +1599 -0
- package/dist/status-TwbMH6Am.js +126 -0
- package/dist/status-json-DMW7cmuK.js +288 -0
- package/dist/status.link-channel-V4LkB6Gq.js +143 -0
- package/dist/status.scan.deps.runtime-BE3X-dcP.js +126 -0
- package/dist/status.scan.runtime-BxVY4mty.js +119 -0
- package/dist/status.summary-CzLM0vVr.js +592 -0
- package/dist/status.summary.runtime-BSBnHZ1Q.js +118 -0
- package/dist/status.update-BxblMS7P.js +77 -0
- package/dist/subagent-orphan-recovery-BpRPryEj.js +307 -0
- package/dist/subagent-registry-runtime-DYYU5p3X.js +111 -0
- package/dist/subscription-CpFdxuFS.js +33 -0
- package/dist/subscription-DaA1urx-.js +102 -0
- package/dist/subscription-cli-Bvto9EmO.js +134 -0
- package/dist/synology-chat-3nwk-Nj0.js +297 -0
- package/dist/system-cli-BvNps8sl.js +94 -0
- package/dist/telegram/audit.d.ts +1 -1
- package/dist/telegram/audit.js +1 -1
- package/dist/telegram/token.d.ts +1 -1
- package/dist/telegram/token.js +35 -35
- package/dist/telegram-RtKXoEsF.js +114 -0
- package/dist/text-chunking-BD5mQe2R.js +84 -0
- package/dist/text-chunking-DDUU_vAF.d.ts +79 -0
- package/dist/tlon-z-kYmJE-.js +433 -0
- package/dist/tui-cli-CzSK08Rh.js +137 -0
- package/dist/tui-wV7R1Tlc.js +3834 -0
- package/dist/types-2H_e7eWT.d.ts +45 -0
- package/dist/types-ZKnGUchG.d.ts +22692 -0
- package/dist/types.base-BFiQZ4J9.d.ts +188 -0
- package/dist/ui-BWVHreeR.js +31 -0
- package/dist/update-D1Wgh1Tj.js +1036 -0
- package/dist/update-cli-CZh99uyY.js +1503 -0
- package/dist/update-offset-store-D5xTdUr0.js +112 -0
- package/dist/update-runner-GbKfoCHs.js +1496 -0
- package/dist/upsert-with-lock-BZU7Le8n.js +33 -0
- package/dist/usage-Czgwvg0h.js +115 -0
- package/dist/web-CMczmL90.js +112 -0
- package/dist/web-shared-B5Q0mIJq.d.ts +45 -0
- package/dist/webhook-request-guards-CsKDhZJr.d.ts +76 -0
- package/dist/webhook-targets-BSmFtesN.js +181 -0
- package/dist/webhook-targets-CjxuEE9C.d.ts +106 -0
- package/dist/webhooks-cli-Wl9y6AWW.js +350 -0
- package/dist/whatsapp-VzRW8MdR.js +114 -0
- package/dist/whatsapp-actions-Cg1Wxv8W.js +167 -0
- package/dist/workspace-DJ_S272u.js +484 -0
- package/dist/workspace-DbZSqjw0.js +289 -0
- package/dist/workspace-cli-D93DLmAh.js +154 -0
- package/dist/workspace-dirs-CGeIPpGN.js +2003 -0
- package/dist/zalo-CK2dlGmu.d.ts +9 -0
- package/dist/zalo-Db7s2boL.js +415 -0
- package/dist/zalouser-Jh5YTJX3.js +30911 -0
- package/docs/reference/templates/AGENTS.dev.md +83 -0
- package/docs/reference/templates/AGENTS.md +219 -0
- package/docs/reference/templates/BOOT.md +11 -0
- package/docs/reference/templates/BOOTSTRAP.md +62 -0
- package/docs/reference/templates/HEARTBEAT.md +12 -0
- package/docs/reference/templates/IDENTITY.dev.md +47 -0
- package/docs/reference/templates/IDENTITY.md +29 -0
- package/docs/reference/templates/SOUL.dev.md +76 -0
- package/docs/reference/templates/SOUL.md +43 -0
- package/docs/reference/templates/TOOLS.dev.md +24 -0
- package/docs/reference/templates/TOOLS.md +47 -0
- package/docs/reference/templates/USER.dev.md +18 -0
- package/docs/reference/templates/USER.md +23 -0
- package/extensions/discord/src/monitor/allow-list.ts +8 -1
- package/extensions/discord/src/monitor/message-handler.preflight.ts +4 -1
- package/package.json +2 -1
- package/dist/accounts-CDr-lDaV.d.ts +0 -103
- package/dist/accounts-CS8U4v8C.js +0 -114
- package/dist/acp-cli-BGT0jXcC.js +0 -2093
- package/dist/actions.runtime-BfckTw6c.js +0 -119
- package/dist/actions.runtime-Cl9mBfqH.js +0 -133
- package/dist/agent-scope-C-YmLnnb.js +0 -208
- package/dist/agents-CydD54p8.js +0 -222
- package/dist/agents-DpQsZO6O.js +0 -853
- package/dist/agents.config-XU7IsYE-.js +0 -121
- package/dist/agents.config-ssoQXuvF.js +0 -17
- package/dist/allow-list-Cfn6lmMK.js +0 -81
- package/dist/allowlist-CCYXVpM9.js +0 -142
- package/dist/api-BoXoFKxy.js +0 -117
- package/dist/audit-Bv05N5o9.js +0 -787
- package/dist/audit-CIWW1Aqm.js +0 -54
- package/dist/audit-channel.collect.runtime-Bi7yrdcO.js +0 -605
- package/dist/audit-channel.runtime-C_NDweiW.js +0 -121
- package/dist/audit-extra.async-Dp7OKSXg.js +0 -813
- package/dist/audit-membership-runtime-B8FQ6VtN.js +0 -162
- package/dist/audit.deep.runtime-CXhobL6b.js +0 -25
- package/dist/audit.nondeep.runtime-CrEm3T16.js +0 -832
- package/dist/audit.runtime-CJPKj1Zg.js +0 -118
- package/dist/auth-Byfp0flq.js +0 -101
- package/dist/auth-choice-BgOjdeXN.js +0 -507
- package/dist/auth-choice-CD1Heq0M.js +0 -122
- package/dist/auth-choice-ePNfg0iQ.js +0 -268
- package/dist/auth-choice-options-BlewQWI0.js +0 -123
- package/dist/auth-choice-prompt-BP2b6aXz.js +0 -36
- package/dist/auth-choice-prompt-Cmwl4n97.js +0 -115
- package/dist/auth-choice.apply-helpers-Dq-nxuuX.js +0 -66
- package/dist/auth-choice.plugin-providers.runtime-B23kOUzQ.js +0 -119
- package/dist/auth-profiles-1kPLbBwI.js +0 -127823
- package/dist/auth-profiles.runtime-DAfSjku1.js +0 -116
- package/dist/banner-DeOsobLO.js +0 -342
- package/dist/bluebubbles-BsLGedBM.js +0 -64
- package/dist/bluebubbles-CnT9wiS4.d.ts +0 -6
- package/dist/bot-CuzVYwa_.d.ts +0 -478
- package/dist/brave-BoWimrLe.js +0 -24
- package/dist/browser-cli-D_S3wEYE.js +0 -1494
- package/dist/call-ByEzDJ1_.js +0 -640
- package/dist/call-CHCWVg-O.js +0 -39
- package/dist/channel-3VC0oOMu.js +0 -214
- package/dist/channel-B9fCBPiS.js +0 -207
- package/dist/channel-B9q775cM.js +0 -562
- package/dist/channel-BG3UK54j.js +0 -803
- package/dist/channel-BRQAdMML.js +0 -352
- package/dist/channel-BmlLp933.js +0 -1321
- package/dist/channel-By6KvdTG.js +0 -920
- package/dist/channel-C8rRsdf6.js +0 -226
- package/dist/channel-CLEDBbXE.js +0 -943
- package/dist/channel-CMvBAG7o.js +0 -306
- package/dist/channel-CmlxxjHY.js +0 -1598
- package/dist/channel-CqG6_xN0.js +0 -949
- package/dist/channel-DNueHKs92.js +0 -316
- package/dist/channel-DUtyN7BX.js +0 -4681
- package/dist/channel-DWD6GrfZ.js +0 -538
- package/dist/channel-DaRYMYzj.js +0 -619
- package/dist/channel-Dj6BgLp8.js +0 -575
- package/dist/channel-account-context-Ba3u5D21.js +0 -103
- package/dist/channel-crabk6Em.js +0 -542
- package/dist/channel-i8uqQaK2.js +0 -497
- package/dist/channel-options-xljvwHS2.js +0 -50
- package/dist/channel-plugin-ids-DAgknSG4.js +0 -26
- package/dist/channel-summary-dHTMCG75.js +0 -111
- package/dist/channel-xVWQ96Ni.js +0 -397
- package/dist/channel.runtime-B6PoZ4BV.js +0 -182
- package/dist/channel.runtime-BPZmo57e.js +0 -404
- package/dist/channel.runtime-B_1uGR-U.js +0 -199
- package/dist/channel.runtime-BiXnPU0d.js +0 -218
- package/dist/channel.runtime-BpvDc9sv.js +0 -870
- package/dist/channel.runtime-CUua3W80.js +0 -418
- package/dist/channel.runtime-CaCBTd0A.js +0 -179
- package/dist/channel.runtime-D0FfYvUj.js +0 -4011
- package/dist/channel.runtime-DhoJtpvJ.js +0 -241
- package/dist/channel.runtime-Kj9EXNE0.js +0 -127
- package/dist/channel.runtime-r4tPuPyh.js +0 -171
- package/dist/channel.setup-B7d_grfe.js +0 -6
- package/dist/channel.setup-C0vu1fhi.js +0 -9
- package/dist/channel.setup-CAI0FNHj.js +0 -11
- package/dist/channel.setup-CkDVwv5R.js +0 -57
- package/dist/channel.setup-Cpd00YqQ.js +0 -8
- package/dist/channel.setup-DbBz1-WT.js +0 -9
- package/dist/channel.setup-GZnAvD9g.js +0 -8
- package/dist/channels-5H484RSw.js +0 -1118
- package/dist/channels-BnPudfyx.js +0 -404
- package/dist/channels-cli-WIC-QeH_.js +0 -291
- package/dist/channels-status-issues-RDmzovJU.js +0 -16
- package/dist/clawbot-cli-BgutNwf8.js +0 -118
- package/dist/cleanup-utils-DBl1Aij1.js +0 -96
- package/dist/cli-1P7u6zqu.js +0 -154
- package/dist/command-registry-B8jovrws.js +0 -232
- package/dist/command-registry-DtDl1FVm.js +0 -14
- package/dist/command-secret-gateway-BgUo3FxJ.js +0 -111
- package/dist/compact.runtime-CXbXM0AU.js +0 -116
- package/dist/completion-cli-Cik_owAE.js +0 -17
- package/dist/completion-cli-RU3P2RSl.js +0 -445
- package/dist/config-5HUpB1L1.js +0 -31
- package/dist/config-cli-QHaUHoZI.js +0 -433
- package/dist/config-guard-C9Sn3pE-.js +0 -118
- package/dist/config-sW57gztj.js +0 -44
- package/dist/config-validation-5LkjIKNt.js +0 -262
- package/dist/config-value-CtTWALxG.js +0 -132
- package/dist/configure-BmR2TPLf.js +0 -243
- package/dist/configure-DaLN-5xM.js +0 -1100
- package/dist/control-ui-assets-CH3MYmAo.js +0 -232
- package/dist/control-ui-shared-CA77PTml.js +0 -29
- package/dist/core-CvDzLs7B.js +0 -150
- package/dist/core-jm751KJ9.d.ts +0 -87
- package/dist/cron-cli-tguLpzyq.js +0 -639
- package/dist/daemon-cli-ptosOkL8.js +0 -339
- package/dist/daemon-install-DzU4EnVa.js +0 -180
- package/dist/deliver-DwxFoHM3.js +0 -111
- package/dist/deliver-runtime-DOdDyaPI.js +0 -111
- package/dist/device-id-cli-GopvlxxZ.js +0 -52
- package/dist/device-identity-CRfhC3_s.js +0 -365
- package/dist/devices-cli-ain7ESqU.js +0 -342
- package/dist/diagnostic-D96Xaqrj.js +0 -310
- package/dist/directory-cli-fh1UxGgY.js +0 -311
- package/dist/directory-config-helpers-Coivm0Mt.d.ts +0 -38
- package/dist/directory.static-CKjJUNGl.js +0 -44
- package/dist/discord-CflhwDEM.js +0 -114
- package/dist/discovery-x0ZqY4AB.js +0 -48
- package/dist/dm-policy-shared-DKzsSLlO.d.ts +0 -95
- package/dist/dns-cli-DCHyKjGf.js +0 -217
- package/dist/docs-cli-D3OoqYSP.js +0 -174
- package/dist/doctor-completion-Bq2eP87s.js +0 -90
- package/dist/doctor-config-flow-D8XRG9Ku.js +0 -2437
- package/dist/doctor-config-flow-DGiF1HGc.js +0 -112
- package/dist/enable-0QSF4YGH.js +0 -24
- package/dist/exec-approvals-cli-Bncym0Gd.js +0 -421
- package/dist/feishu-C1dM8pl2.d.ts +0 -36
- package/dist/gateway-cli-DYscsmA-.js +0 -26437
- package/dist/gateway-install-token-CNv17ac9.js +0 -163
- package/dist/gateway-rpc-BGC1Rxvg.js +0 -26
- package/dist/gateway-runtime-D89mSQPB.js +0 -69
- package/dist/git-commit-CeLH5Ozm.js +0 -2
- package/dist/git-commit-DUKRiCP-.js +0 -177
- package/dist/googlechat-BgXeXjd1.js +0 -307
- package/dist/googlechat-CNZQb1jd.d.ts +0 -12
- package/dist/group-access-Deh1tVNr.d.ts +0 -61
- package/dist/health-BEjzWwaB.js +0 -570
- package/dist/health-FjqrWQL6.js +0 -113
- package/dist/heartbeat-summary-CfdSA9M1.js +0 -57
- package/dist/help-BZeVprq1.js +0 -81
- package/dist/hooks-06OUQvAV.d.ts +0 -6
- package/dist/hooks-cli-B7uGJs2O.js +0 -1000
- package/dist/hooks-status-CfceaUSg.js +0 -78
- package/dist/http-registry-DYskWhOr.d.ts +0 -20
- package/dist/identity-file-sshkKKIr.js +0 -60
- package/dist/image-generation-D4o3j8o6.d.ts +0 -9
- package/dist/imessage-BcV3WGx_.js +0 -31
- package/dist/imessage-Dhje7Ty-.js +0 -115
- package/dist/inbound-reply-dispatch-C73_7SOl.js +0 -71
- package/dist/inbound-reply-dispatch-D6_HNqH8.d.ts +0 -72
- package/dist/install-target-D7NRhfzc.js +0 -574
- package/dist/installs-Bj6jblqc.js +0 -532
- package/dist/io-CMfWWPXQ.js +0 -9738
- package/dist/io-CV844hAM.js +0 -29
- package/dist/irc-DKi1fDYI.js +0 -672
- package/dist/library-rygTG3oA.js +0 -112
- package/dist/lifecycle-core-BPlvShWY.js +0 -382
- package/dist/line-B8gTtl3Y.d.ts +0 -75
- package/dist/line-CGsemKWJ.js +0 -530
- package/dist/llm-slug-generator-DlhVyMqT.js +0 -67
- package/dist/logging-5wu9k6w4.js +0 -30
- package/dist/logging-CxP9suT8.js +0 -13
- package/dist/login-qr-BcDsiwHs.js +0 -233
- package/dist/login-qr-Y8pJ5yV4.js +0 -112
- package/dist/logs-cli-XI9oVXpH.js +0 -256
- package/dist/manager-runtime-DkIlXBhD.js +0 -111
- package/dist/manager.runtime-Q0q2rJCC.js +0 -715
- package/dist/manifest-registry-DAd0SRAP.js +0 -1329
- package/dist/matrix-BI0DBBrG.js +0 -1495
- package/dist/matrix-D2JoHzb4.d.ts +0 -68
- package/dist/matrix-DiABGjJR.js +0 -1269
- package/dist/mcp-cli-BOyn_DLL.js +0 -87
- package/dist/media-understanding.runtime-DjUa7Dka.js +0 -116
- package/dist/memory-cli-CJd_vl-Y.js +0 -111
- package/dist/memory-search-CEEItIFR.js +0 -17
- package/dist/memory-search-Cv1SBrn7.js +0 -204
- package/dist/method-scopes-CQE7-bZ-.js +0 -2452
- package/dist/model-auth-markers-BFoM4IPf.d.ts +0 -20
- package/dist/model-picker-D6_89XHg.js +0 -112
- package/dist/model-picker-Svaw-APs.js +0 -390
- package/dist/model-picker.runtime-Chi9nV7A.js +0 -125
- package/dist/model-selection-hL8i1Jbs.js +0 -653
- package/dist/model-suppression.runtime-DjWJZ0X-.js +0 -116
- package/dist/models-7qj1dG_W.js +0 -118
- package/dist/models-BPOB_xJF.js +0 -2514
- package/dist/models-cli-DdlOVUjS.js +0 -309
- package/dist/models-config-CBqUS-jX.js +0 -111
- package/dist/models-config.providers.discovery-Dc905FWG.d.ts +0 -18
- package/dist/moldclaw-root-D6PbhbZk.js +0 -88
- package/dist/monitor-BPYhkEqF.js +0 -782
- package/dist/monitor-BuTcQ24j.js +0 -3468
- package/dist/monitor-CuXvNhFh.js +0 -113
- package/dist/monitor-D-TqSIHF.js +0 -6823
- package/dist/monitor-DRSgo9u2.js +0 -3076
- package/dist/monitor-DcHch39z.js +0 -772
- package/dist/monitor-DsHBMrXp.js +0 -115
- package/dist/monitor-shared-CL8T4gt1.js +0 -444
- package/dist/msteams-7FMwTvQG.js +0 -852
- package/dist/node-cli-BCjaSCZM.js +0 -2503
- package/dist/node-resolve-D5Hvcgyx.js +0 -835
- package/dist/nodes-cli-Dd_SNbkt.js +0 -1380
- package/dist/nostr-DBTFTxKs.js +0 -8744
- package/dist/nostr-DLqaIuZx.d.ts +0 -7
- package/dist/npm-resolution-CYfb3MHG.js +0 -60
- package/dist/oauth-env-zPt5RywA.js +0 -10
- package/dist/onboard-BEFQQeig.js +0 -25
- package/dist/onboard-CJHNyxJh.js +0 -48
- package/dist/onboard-D_3UeLEN.js +0 -589
- package/dist/onboard-channels-B_JL0Djc.js +0 -1241
- package/dist/onboard-channels-CqZzHt2C.js +0 -205
- package/dist/onboard-custom-CER3Ggbq.js +0 -571
- package/dist/onboard-custom-bNRdGECb.js +0 -114
- package/dist/onboard-helpers-BK0Hsb7Y.js +0 -335
- package/dist/onboard-helpers-CXZ5RPoR.js +0 -113
- package/dist/onboard-hooks-1NsxEDjH.js +0 -72
- package/dist/onboard-remote-DuKhC_7W.js +0 -117
- package/dist/onboard-remote-OwRcDuB3.js +0 -181
- package/dist/onboard-search-Cy8dOq2W.js +0 -302
- package/dist/onboard-skills-D5phRa6r.js +0 -117
- package/dist/onboard-skills-c9qWCNe9.js +0 -133
- package/dist/outbound-media-CXKqTh2X.d.ts +0 -11
- package/dist/outbound-media-DYRO2vTD.js +0 -11
- package/dist/pairing-access-BwJu1mkk.d.ts +0 -21
- package/dist/pairing-cli-BOnv0TYn.js +0 -217
- package/dist/perplexity-EZwC3y2b.js +0 -24
- package/dist/persistent-dedupe-hNES5tS1.d.ts +0 -26
- package/dist/pi-model-discovery-runtime-BToY3A6K.js +0 -111
- package/dist/pi-tools.before-tool-call.runtime-D_acPtld.js +0 -381
- package/dist/plugin-install-CgJpSjYd.js +0 -184
- package/dist/plugin-install-Cl1A4EF6.js +0 -117
- package/dist/plugin-install-plan-Dc2Z4DeU.js +0 -49
- package/dist/plugin-registry-B1UaWrQD.js +0 -49
- package/dist/plugin-registry-Cy8biwnn.js +0 -113
- package/dist/plugins-CXwvg50F.js +0 -111
- package/dist/plugins-cli-Uvzp2aYV.js +0 -917
- package/dist/policy-DsMBbEe7.js +0 -143
- package/dist/preflight-audio.runtime-hWsZIYvc.js +0 -116
- package/dist/probe-CNsSf1Uf.js +0 -6329
- package/dist/probe-CqOIrPhb.js +0 -47
- package/dist/probe-DH6gDw-h.js +0 -129
- package/dist/probe-DM16PLf4.js +0 -21
- package/dist/probe-DvAEEWYr.js +0 -1793
- package/dist/probe-auth-COfgCble.js +0 -48
- package/dist/probe-auth-I_5TX1Eh.js +0 -40
- package/dist/program-Dz80sgTU.js +0 -253
- package/dist/prompt-select-styled-wQehwFxK.js +0 -2673
- package/dist/provider-api-key-auth.runtime-BR9GU4ya.js +0 -121
- package/dist/provider-auth-choice-CdhA84kr.js +0 -126
- package/dist/provider-auth-choice-helpers-kabp_0zA.js +0 -48
- package/dist/provider-auth-choice-preference-se3zAM_2.js +0 -189
- package/dist/provider-auth-choice.runtime-BMc8-xNQ.js +0 -123
- package/dist/provider-auth-choices-CYsCViGi.js +0 -57
- package/dist/provider-auth-guidance-CMjUWlNf.js +0 -34
- package/dist/provider-auth-result-5xgWoVGi.d.ts +0 -18
- package/dist/provider-models-BCId_Lfu.js +0 -2113
- package/dist/provider-models-D-eFl9oH.d.ts +0 -867
- package/dist/provider-ollama-setup-B6XJZ0So.js +0 -314
- package/dist/provider-ollama-setup-BF1vhob8.d.ts +0 -32
- package/dist/provider-onboard-BjXHP3IZ.d.ts +0 -40
- package/dist/provider-onboard-Ca0TaNud.js +0 -139
- package/dist/provider-runtime.runtime-DwwkHw_7.js +0 -111
- package/dist/provider-self-hosted-setup-BEKLVGpj.js +0 -182
- package/dist/provider-self-hosted-setup-BQ5BIlpi.d.ts +0 -61
- package/dist/provider-stream-DrUD69ai.js +0 -512
- package/dist/provider-usage-BgKHCnjr.js +0 -111
- package/dist/provider-usage-D8EZpFz9.js +0 -633
- package/dist/provider-wizard-DMdb-zj_.js +0 -152
- package/dist/push-apns-BPH6d4VV.js +0 -1038
- package/dist/pw-ai-DttfldtL.js +0 -1867
- package/dist/qmd-manager-CybcDUfk.js +0 -1570
- package/dist/qr-cli-8NcmJ8Ft.js +0 -369
- package/dist/qr-cli-DWe0Our3.js +0 -113
- package/dist/reactions-D6N0LR16.js +0 -281
- package/dist/read-only-account-inspect.discord.runtime-CqUWTRfl.js +0 -116
- package/dist/read-only-account-inspect.slack.runtime-9-jpln3q.js +0 -116
- package/dist/read-only-account-inspect.telegram.runtime-EKPI1D7n.js +0 -116
- package/dist/redact-snapshot-DwJEIVk9.js +0 -2663
- package/dist/register.agent-D3YdDirP.js +0 -439
- package/dist/register.backup-dR27qCuo.js +0 -625
- package/dist/register.configure-BjFhkkka.js +0 -252
- package/dist/register.maintenance-DiMQJIOa.js +0 -574
- package/dist/register.message-CdZsKYH1.js +0 -709
- package/dist/register.onboard-B0rV1eaO.js +0 -192
- package/dist/register.setup-wKMvohzo.js +0 -212
- package/dist/register.status-health-sessions-BJ68m6pt.js +0 -498
- package/dist/register.subclis-CnnrWt2a.js +0 -315
- package/dist/register.subclis-lSvTkC6z.js +0 -13
- package/dist/replies-BABt9b48.js +0 -110
- package/dist/resolve-channels-BqZFl2Ux.js +0 -262
- package/dist/resolve-channels-DjQLXb7B.js +0 -226
- package/dist/resolve-route-CSHDsa_m.js +0 -538
- package/dist/resolve-users-BG6HaSR5.js +0 -143
- package/dist/root-help-ohmaCyC_.js +0 -32
- package/dist/routes-4k2kpvoT.js +0 -7097
- package/dist/rpc-Cnwn4Q6L.js +0 -67
- package/dist/run-main-VYlacKA0.js +0 -424
- package/dist/runtime-Cy8pqYUB.d.ts +0 -26
- package/dist/runtime-discord-ops.runtime-DafrU-rI.js +0 -9078
- package/dist/runtime-slack-ops.runtime-CdXBKXwd.js +0 -4556
- package/dist/runtime-telegram-ops.runtime-B12sF7gE.js +0 -133
- package/dist/runtime-whatsapp-login.runtime-CqEudH37.js +0 -114
- package/dist/runtime-whatsapp-outbound.runtime-D5m2qyn-.js +0 -117
- package/dist/sandbox-cli-CHJiEWXB.js +0 -535
- package/dist/search-manager-BtNC3-i_.js +0 -16
- package/dist/search-manager-C7J7B3_a.js +0 -386
- package/dist/secrets-cli-C6yIWBbN.js +0 -2070
- package/dist/security-cli-BVu9BkjD.js +0 -575
- package/dist/send-BSreC7rr.js +0 -631
- package/dist/send-BsLHQG_B.js +0 -1025
- package/dist/send-BuNhp8PH.js +0 -283
- package/dist/send-DOCswVar.js +0 -100
- package/dist/send-Dl0LLErk.js +0 -629
- package/dist/server-node-events-Bq2067EG.js +0 -506
- package/dist/server-y38L7N5H.js +0 -107
- package/dist/sessions-BV8gXURR.js +0 -112
- package/dist/sessions-dl1Kc-Ci.js +0 -218
- package/dist/setup-DGszQH0_.js +0 -387
- package/dist/setup-DR5rRw9y.d.ts +0 -37
- package/dist/setup-binary-C17YnmA8.js +0 -406
- package/dist/setup-browser-CPx-nEsr.js +0 -70
- package/dist/setup-core-BByHN1ME.js +0 -143
- package/dist/setup-core-C0KPlBmL.js +0 -47
- package/dist/setup-core-Cq37G6of.js +0 -166
- package/dist/setup-core-uO84_Y75.js +0 -205
- package/dist/setup-surface-BEMi7Rmb.js +0 -490
- package/dist/setup-wizard-helpers-BtuGx_gN.d.ts +0 -203
- package/dist/setup.finalize-BzPBa8zW.js +0 -522
- package/dist/setup.gateway-config-DdwkF-8e.js +0 -343
- package/dist/shared-BCw4SKjB.js +0 -96
- package/dist/shared-CjNzsULP.js +0 -75
- package/dist/shared-Cu1BE7ZE.js +0 -298
- package/dist/shared-DSClmyUn.js +0 -182
- package/dist/shared-DyJdGH6y.js +0 -102
- package/dist/signal-Dyv4NZsB.js +0 -114
- package/dist/skills-CbB5b27M.js +0 -853
- package/dist/skills-CnfI7Szw.js +0 -20
- package/dist/skills-cli-CavB1f_3.js +0 -292
- package/dist/skills-install-B1OBdgd0.js +0 -763
- package/dist/skills-status-B3gAmIbW.js +0 -169
- package/dist/skills-status-DrHhFgU9.js +0 -21
- package/dist/slack-BRzqnoAz.js +0 -114
- package/dist/slash-commands.runtime-BK88kgds.js +0 -128
- package/dist/slash-dispatch.runtime-COGywwJE.js +0 -141
- package/dist/slash-skill-commands.runtime-Ti4brxgh.js +0 -116
- package/dist/src-DUR6OQxI.js +0 -1701
- package/dist/status-C6dgQY9a.js +0 -131
- package/dist/status-CNK0Q7QH.js +0 -606
- package/dist/status-DBcX0DSC.js +0 -43
- package/dist/status-DKgFgbwv.js +0 -1599
- package/dist/status-Wn5lhNAc.js +0 -126
- package/dist/status-json-D2EkWqAl.js +0 -288
- package/dist/status.link-channel-D3ULIdEa.js +0 -143
- package/dist/status.scan.deps.runtime-BsjWTAm4.js +0 -126
- package/dist/status.scan.runtime-D4HbzROD.js +0 -119
- package/dist/status.summary-C3YxPrDK.js +0 -592
- package/dist/status.summary.runtime-DAkXPSaK.js +0 -118
- package/dist/status.update-B4NnN9P1.js +0 -77
- package/dist/subagent-orphan-recovery-QiQEBv36.js +0 -307
- package/dist/subagent-registry-runtime-BJatPQFK.js +0 -111
- package/dist/subscription-BhZORXN9.js +0 -100
- package/dist/subscription-QEUjQRMv.js +0 -33
- package/dist/subscription-cli-HrULlAgc.js +0 -134
- package/dist/synology-chat-DB76GWMN.js +0 -297
- package/dist/system-cli-D8jDwWuL.js +0 -94
- package/dist/telegram-BHiiqKkQ.js +0 -114
- package/dist/text-chunking-Baonm9Lu.js +0 -84
- package/dist/text-chunking-Y3dPBOuZ.d.ts +0 -79
- package/dist/tlon-DLESxNgD.js +0 -433
- package/dist/tui-C75zi2Cl.js +0 -3834
- package/dist/tui-cli-DFwx5e6i.js +0 -137
- package/dist/types-BBJ3Qz7j.d.ts +0 -45
- package/dist/types-Ckufs_BY.d.ts +0 -22692
- package/dist/types.base-Cw0-zIvE.d.ts +0 -188
- package/dist/ui-B55NOIB6.js +0 -31
- package/dist/update--ojavYQ4.js +0 -1036
- package/dist/update-cli-Cvj5aWYM.js +0 -1503
- package/dist/update-offset-store-upatuWwX.js +0 -112
- package/dist/update-runner-DHkY_-76.js +0 -1496
- package/dist/upsert-with-lock-C171GLaR.js +0 -33
- package/dist/usage-N3bxnbmt.js +0 -115
- package/dist/web-RdvT7gKa.js +0 -112
- package/dist/web-shared-HSGD3yGt.d.ts +0 -45
- package/dist/webhook-request-guards-CosLyl01.d.ts +0 -76
- package/dist/webhook-targets-Bfnag-du.js +0 -181
- package/dist/webhook-targets-Di17rt8e.d.ts +0 -106
- package/dist/webhooks-cli-ZpnXrq7G.js +0 -350
- package/dist/whatsapp-DNTAyZHt.js +0 -114
- package/dist/whatsapp-actions-o1zKQzKZ.js +0 -167
- package/dist/workspace-CpWi5wPr.js +0 -479
- package/dist/workspace-Ii7aRS7c.js +0 -289
- package/dist/workspace-dirs-x10McA9t.js +0 -2003
- package/dist/zalo-BN3VCrRY.d.ts +0 -9
- package/dist/zalo-zm_bYCKg.js +0 -415
- package/dist/zalouser-CvVEUvc5.js +0 -30911
- /package/dist/{account-id-B3YSn4hl.d.ts → account-id-B8ce6G_4.d.ts} +0 -0
- /package/dist/{acpx-CnNv70m2.d.ts → acpx-Ci50I9T2.d.ts} +0 -0
- /package/dist/{agent-media-payload-DE2pEcsz.d.ts → agent-media-payload-en-gS5p6.d.ts} +0 -0
- /package/dist/{allow-from-DPpHnT2A.d.ts → allow-from-cMeQ47Ot.d.ts} +0 -0
- /package/dist/{allowlist-resolution-CLFiZ6nE.d.ts → allowlist-resolution-DoAWbfXV.d.ts} +0 -0
- /package/dist/{bluebubbles-Duhu-Jer.d.ts → bluebubbles-C6yYmUl0.d.ts} +0 -0
- /package/dist/{boolean-param-BhFjB3gp.d.ts → boolean-param-CdO2TFTk.d.ts} +0 -0
- /package/dist/{channel-config-schema-DnnVMdjR.d.ts → channel-config-schema-Chp38wel.d.ts} +0 -0
- /package/dist/{channel-policy-Baq-Z06b.d.ts → channel-policy-g2h6AbYQ.d.ts} +0 -0
- /package/dist/{chat-type-DpiBgwuG.d.ts → chat-type-BLt59pPT.d.ts} +0 -0
- /package/dist/{command-format-vi4xq8e8.d.ts → command-format-BDJC05Jp.d.ts} +0 -0
- /package/dist/{diffs-DK7fVSDo.d.ts → diffs-D_iNKCyn.d.ts} +0 -0
- /package/dist/{directory-runtime-BTLPaysA.d.ts → directory-runtime-DhMex6HY.d.ts} +0 -0
- /package/dist/{exec-C01wtBHu.d.ts → exec-pjfUY4KM.d.ts} +0 -0
- /package/dist/{gaxios-fetch-compat-wZ38b3w3.js → gaxios-fetch-compat-B_vtINdV.js} +0 -0
- /package/dist/{history-CwXuP2TW.d.ts → history-aqSS5VGQ.d.ts} +0 -0
- /package/dist/{inbound-envelope-SggrBs9m.d.ts → inbound-envelope-C5hWuZod.d.ts} +0 -0
- /package/dist/{index-apAZHsDo.d.ts → index-DXVQFYGX.d.ts} +0 -0
- /package/dist/{json-store-r75IZGk9.d.ts → json-store-UnqQ5aV3.d.ts} +0 -0
- /package/dist/{keyed-async-queue-DHIr7yNe.d.ts → keyed-async-queue-guucpLw3.d.ts} +0 -0
- /package/dist/{links-HeQ3r_L0.d.ts → links-Bar0meEK.d.ts} +0 -0
- /package/dist/{markdown-to-line-CDb4Jy3V.d.ts → markdown-to-line-D8uH_KOj.d.ts} +0 -0
- /package/dist/{mattermost-DtCsxpgg.d.ts → mattermost-xl7jAFJL.d.ts} +0 -0
- /package/dist/{net-BATPDwdQ.d.ts → net-rGOKGds6.d.ts} +0 -0
- /package/dist/{nextcloud-talk-Bb2wHOwp.d.ts → nextcloud-talk-De2CZ9dV.d.ts} +0 -0
- /package/dist/{oauth-utils-u567CLT0.d.ts → oauth-utils-DzN1AlEH.d.ts} +0 -0
- /package/dist/{parse-finite-number-l3tNlrZh.d.ts → parse-finite-number-odgyqhi0.d.ts} +0 -0
- /package/dist/{provider-usage.types-C6061OVN.d.ts → provider-usage.types-EDE9o-H_.d.ts} +0 -0
- /package/dist/{reply-history-BDsFnZFl.d.ts → reply-history-CVuU31xe.d.ts} +0 -0
- /package/dist/{reply-payload-CCvM4W9u.d.ts → reply-payload-CHkpBYwL.d.ts} +0 -0
- /package/dist/{request-url-C54l4-xC.d.ts → request-url-DHisbiHY.d.ts} +0 -0
- /package/dist/{run-command-D3RqWcHu.d.ts → run-command-y0Cndsb1.d.ts} +0 -0
- /package/dist/{secret-input-schema-BLBt-NAP.d.ts → secret-input-schema-b1vpYDQN.d.ts} +0 -0
- /package/dist/{session-key-BQ2-bR-9.d.ts → session-key-DTHQl57f.d.ts} +0 -0
- /package/dist/{ssh-config-C4mcH9Ly.js → ssh-config-hEHBfU2_.js} +0 -0
- /package/dist/{testing-DLkhGsoz.d.ts → testing-DszuZXgK.d.ts} +0 -0
- /package/dist/{thinking-DRkjX18p.d.ts → thinking-IwXTGSeT.d.ts} +0 -0
- /package/dist/{tool-send-CMMD1uDu.d.ts → tool-send-DWHRmKpz.d.ts} +0 -0
- /package/dist/{vllm-defaults-CcGuf4hL.d.ts → vllm-defaults-CrxZgE6-.d.ts} +0 -0
- /package/dist/{wait-Daog8bxM.d.ts → wait-wDWw_MTI.d.ts} +0 -0
- /package/dist/{webhook-memory-guards-C5MrExwT.d.ts → webhook-memory-guards-DreORuJy.d.ts} +0 -0
- /package/dist/{windows-spawn-j2l-dqu8.d.ts → windows-spawn-BIzH92x2.d.ts} +0 -0
- /package/dist/{zod-schema.agent-runtime-krMrBnIn.d.ts → zod-schema.agent-runtime-CP2rmis3.d.ts} +0 -0
- /package/dist/{zod-schema.core-BNDieZDZ.d.ts → zod-schema.core-Foi1tYwi.d.ts} +0 -0
|
@@ -0,0 +1,4681 @@
|
|
|
1
|
+
import { t as formatDocsLink } from "./links-BcahUP5U.js";
|
|
2
|
+
import { Ah as createScopedPairingAccess, Ew as applySetupAccountConfigPatch, HC as createAccountListHelpers, Is as formatInboundFromLabel$1, KC as buildComputedAccountStatusSnapshot, LT as collectAllowlistProviderRestrictSendersWarnings, Nd as resolveDmGroupAccessWithLists, Od as DM_GROUP_ACCESS_REASON, Pd as resolveEffectiveAllowFromLists, Qg as buildPendingHistoryContextFromMap, Tw as applyAccountNameToChannelSection, UT as deleteAccountFromConfigSection, Ug as logInboundDrop, Uw as resolveControlCommandGate, Vs as buildModelsProviderData, WS as createDedupeCache, WT as setAccountEnabledInConfigSection, Wg as logTypingFailure, Za as resolveStoredModelOverride, ag as createTypingCallbacks, bS as readRequestBodyWithLimit, dT as createScopedAccountConfigAccessors, e_ as clearHistoryEntriesIfEnabled, ec as listSkillCommandsForAgents, ig as buildSecretInputSchema, jh as buildAgentMediaPayload, kT as buildAccountScopedDmSecurityPolicy, kd as readStoreAllowFromForDmPolicy, kw as migrateBaseNameToDefaultAccount, nT as resolveAllowlistMatchSimple, nh as resolveChannelMediaMaxBytes, ow as resolveChannelGroupRequireMention, qy as getAgentScopedMediaLocalRoots, rC as loadSessionStore, r_ as recordPendingHistoryEntryIfEnabled, sg as createReplyPrefixOptions, tC as isDangerousNameMatchingEnabled, vS as isRequestBodyLimitError, xs as registerPluginHttpRoute } from "./auth-profiles-smABVXzp.js";
|
|
3
|
+
import { w as normalizeProviderId } from "./model-selection-bBBxfXdb.js";
|
|
4
|
+
import { d as resolveThreadSessionKeys$1 } from "./session-key-UoG7Kfw5.js";
|
|
5
|
+
import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-BuyZMNja.js";
|
|
6
|
+
import { a as hasConfiguredSecretInput, c as normalizeResolvedSecretInputString, l as normalizeSecretInputString } from "./types.secrets-Ca-9L8vU.js";
|
|
7
|
+
import { F as requireOpenAllowFrom, a as DmPolicySchema, c as GroupPolicySchema, m as MarkdownConfigSchema, n as BlockStreamingCoalesceSchema } from "./zod-schema.core-DvwgNmpd.js";
|
|
8
|
+
import { a as isTrustedProxyAddress, l as resolveClientIp } from "./net-K181nxTH.js";
|
|
9
|
+
import { t as rawDataToString } from "./ws-Bd4lOIoI.js";
|
|
10
|
+
import { c as resolveAllowlistProviderRuntimeGroupPolicy, f as warnMissingProviderGroupPolicyFallbackOnce, i as evaluateSenderGroupAccessForPolicy, l as resolveDefaultGroupPolicy } from "./group-access-UAqUyJod.js";
|
|
11
|
+
import { r as buildChannelConfigSchema } from "./config-schema-BNU4GQh_.js";
|
|
12
|
+
import { i as parseStrictPositiveInteger } from "./parse-finite-number-U5TetQpk.js";
|
|
13
|
+
import { l as resolveStorePath } from "./paths-ApLcu1Uu.js";
|
|
14
|
+
import { n as formatNormalizedAllowFromEntries } from "./allow-from-Brz0jyla.js";
|
|
15
|
+
import { t as createPluginRuntimeStore } from "./runtime-store-DTqHvPYo.js";
|
|
16
|
+
import { t as createAccountStatusSink } from "./channel-lifecycle-DA5pCpey.js";
|
|
17
|
+
import { t as loadOutboundMediaFromUrl } from "./outbound-media-DSno0N82.js";
|
|
18
|
+
import { n as buildPassiveProbedChannelStatusSummary } from "./channel-status-summary-DfvQV-Ir.js";
|
|
19
|
+
import { t as requireChannelOpenAllowFrom } from "./config-schema-helpers-D4ZGZ7dA.js";
|
|
20
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import WebSocket$1 from "ws";
|
|
23
|
+
//#region extensions/mattermost/src/config-schema.ts
|
|
24
|
+
const MattermostSlashCommandsSchema = z.object({
|
|
25
|
+
native: z.union([z.boolean(), z.literal("auto")]).optional(),
|
|
26
|
+
nativeSkills: z.union([z.boolean(), z.literal("auto")]).optional(),
|
|
27
|
+
callbackPath: z.string().optional(),
|
|
28
|
+
callbackUrl: z.string().optional()
|
|
29
|
+
}).strict().optional();
|
|
30
|
+
const MattermostAccountSchemaBase = z.object({
|
|
31
|
+
name: z.string().optional(),
|
|
32
|
+
capabilities: z.array(z.string()).optional(),
|
|
33
|
+
dangerouslyAllowNameMatching: z.boolean().optional(),
|
|
34
|
+
markdown: MarkdownConfigSchema,
|
|
35
|
+
enabled: z.boolean().optional(),
|
|
36
|
+
configWrites: z.boolean().optional(),
|
|
37
|
+
botToken: buildSecretInputSchema().optional(),
|
|
38
|
+
baseUrl: z.string().optional(),
|
|
39
|
+
chatmode: z.enum([
|
|
40
|
+
"oncall",
|
|
41
|
+
"onmessage",
|
|
42
|
+
"onchar"
|
|
43
|
+
]).optional(),
|
|
44
|
+
oncharPrefixes: z.array(z.string()).optional(),
|
|
45
|
+
requireMention: z.boolean().optional(),
|
|
46
|
+
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
47
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
48
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
49
|
+
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
|
50
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
51
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
52
|
+
blockStreaming: z.boolean().optional(),
|
|
53
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
54
|
+
replyToMode: z.enum([
|
|
55
|
+
"off",
|
|
56
|
+
"first",
|
|
57
|
+
"all"
|
|
58
|
+
]).optional(),
|
|
59
|
+
responsePrefix: z.string().optional(),
|
|
60
|
+
actions: z.object({ reactions: z.boolean().optional() }).optional(),
|
|
61
|
+
commands: MattermostSlashCommandsSchema,
|
|
62
|
+
interactions: z.object({
|
|
63
|
+
callbackBaseUrl: z.string().optional(),
|
|
64
|
+
allowedSourceIps: z.array(z.string()).optional()
|
|
65
|
+
}).optional()
|
|
66
|
+
}).strict();
|
|
67
|
+
const MattermostAccountSchema = MattermostAccountSchemaBase.superRefine((value, ctx) => {
|
|
68
|
+
requireChannelOpenAllowFrom({
|
|
69
|
+
channel: "mattermost",
|
|
70
|
+
policy: value.dmPolicy,
|
|
71
|
+
allowFrom: value.allowFrom,
|
|
72
|
+
ctx,
|
|
73
|
+
requireOpenAllowFrom
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const MattermostConfigSchema = MattermostAccountSchemaBase.extend({
|
|
77
|
+
accounts: z.record(z.string(), MattermostAccountSchema.optional()).optional(),
|
|
78
|
+
defaultAccount: z.string().optional()
|
|
79
|
+
}).superRefine((value, ctx) => {
|
|
80
|
+
requireChannelOpenAllowFrom({
|
|
81
|
+
channel: "mattermost",
|
|
82
|
+
policy: value.dmPolicy,
|
|
83
|
+
allowFrom: value.allowFrom,
|
|
84
|
+
ctx,
|
|
85
|
+
requireOpenAllowFrom
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region extensions/mattermost/src/mattermost/client.ts
|
|
90
|
+
function normalizeMattermostBaseUrl(raw) {
|
|
91
|
+
const trimmed = raw?.trim();
|
|
92
|
+
if (!trimmed) return;
|
|
93
|
+
return trimmed.replace(/\/+$/, "").replace(/\/api\/v4$/i, "");
|
|
94
|
+
}
|
|
95
|
+
function buildMattermostApiUrl(baseUrl, path) {
|
|
96
|
+
const normalized = normalizeMattermostBaseUrl(baseUrl);
|
|
97
|
+
if (!normalized) throw new Error("Mattermost baseUrl is required");
|
|
98
|
+
return `${normalized}/api/v4${path.startsWith("/") ? path : `/${path}`}`;
|
|
99
|
+
}
|
|
100
|
+
async function readMattermostError(res) {
|
|
101
|
+
if ((res.headers.get("content-type") ?? "").includes("application/json")) {
|
|
102
|
+
const data = await res.json();
|
|
103
|
+
if (data?.message) return data.message;
|
|
104
|
+
return JSON.stringify(data);
|
|
105
|
+
}
|
|
106
|
+
return await res.text();
|
|
107
|
+
}
|
|
108
|
+
function createMattermostClient(params) {
|
|
109
|
+
const baseUrl = normalizeMattermostBaseUrl(params.baseUrl);
|
|
110
|
+
if (!baseUrl) throw new Error("Mattermost baseUrl is required");
|
|
111
|
+
const apiBaseUrl = `${baseUrl}/api/v4`;
|
|
112
|
+
const token = params.botToken.trim();
|
|
113
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
114
|
+
const request = async (path, init) => {
|
|
115
|
+
const url = buildMattermostApiUrl(baseUrl, path);
|
|
116
|
+
const headers = new Headers(init?.headers);
|
|
117
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
118
|
+
if (typeof init?.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
119
|
+
const res = await fetchImpl(url, {
|
|
120
|
+
...init,
|
|
121
|
+
headers
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const detail = await readMattermostError(res);
|
|
125
|
+
throw new Error(`Mattermost API ${res.status} ${res.statusText}: ${detail || "unknown error"}`);
|
|
126
|
+
}
|
|
127
|
+
if (res.status === 204) return;
|
|
128
|
+
if ((res.headers.get("content-type") ?? "").includes("application/json")) return await res.json();
|
|
129
|
+
return await res.text();
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
baseUrl,
|
|
133
|
+
apiBaseUrl,
|
|
134
|
+
token,
|
|
135
|
+
request
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function fetchMattermostMe(client) {
|
|
139
|
+
return await client.request("/users/me");
|
|
140
|
+
}
|
|
141
|
+
async function fetchMattermostUser(client, userId) {
|
|
142
|
+
return await client.request(`/users/${userId}`);
|
|
143
|
+
}
|
|
144
|
+
async function fetchMattermostUserByUsername(client, username) {
|
|
145
|
+
return await client.request(`/users/username/${encodeURIComponent(username)}`);
|
|
146
|
+
}
|
|
147
|
+
async function fetchMattermostChannel(client, channelId) {
|
|
148
|
+
return await client.request(`/channels/${channelId}`);
|
|
149
|
+
}
|
|
150
|
+
async function fetchMattermostChannelByName(client, teamId, channelName) {
|
|
151
|
+
return await client.request(`/teams/${teamId}/channels/name/${encodeURIComponent(channelName)}`);
|
|
152
|
+
}
|
|
153
|
+
async function sendMattermostTyping(client, params) {
|
|
154
|
+
const payload = { channel_id: params.channelId };
|
|
155
|
+
const parentId = params.parentId?.trim();
|
|
156
|
+
if (parentId) payload.parent_id = parentId;
|
|
157
|
+
await client.request("/users/me/typing", {
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: JSON.stringify(payload)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function createMattermostDirectChannel(client, userIds) {
|
|
163
|
+
return await client.request("/channels/direct", {
|
|
164
|
+
method: "POST",
|
|
165
|
+
body: JSON.stringify(userIds)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function createMattermostPost(client, params) {
|
|
169
|
+
const payload = {
|
|
170
|
+
channel_id: params.channelId,
|
|
171
|
+
message: params.message
|
|
172
|
+
};
|
|
173
|
+
if (params.rootId) payload.root_id = params.rootId;
|
|
174
|
+
if (params.fileIds?.length) payload.file_ids = params.fileIds;
|
|
175
|
+
if (params.props) payload.props = params.props;
|
|
176
|
+
return await client.request("/posts", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify(payload)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function fetchMattermostUserTeams(client, userId) {
|
|
182
|
+
return await client.request(`/users/${userId}/teams`);
|
|
183
|
+
}
|
|
184
|
+
async function updateMattermostPost(client, postId, params) {
|
|
185
|
+
const payload = { id: postId };
|
|
186
|
+
if (params.message !== void 0) payload.message = params.message;
|
|
187
|
+
if (params.props !== void 0) payload.props = params.props;
|
|
188
|
+
return await client.request(`/posts/${postId}`, {
|
|
189
|
+
method: "PUT",
|
|
190
|
+
body: JSON.stringify(payload)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function uploadMattermostFile(client, params) {
|
|
194
|
+
const form = new FormData();
|
|
195
|
+
const fileName = params.fileName?.trim() || "upload";
|
|
196
|
+
const bytes = Uint8Array.from(params.buffer);
|
|
197
|
+
const blob = params.contentType ? new Blob([bytes], { type: params.contentType }) : new Blob([bytes]);
|
|
198
|
+
form.append("files", blob, fileName);
|
|
199
|
+
form.append("channel_id", params.channelId);
|
|
200
|
+
const res = await fetch(`${client.apiBaseUrl}/files`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { Authorization: `Bearer ${client.token}` },
|
|
203
|
+
body: form
|
|
204
|
+
});
|
|
205
|
+
if (!res.ok) {
|
|
206
|
+
const detail = await readMattermostError(res);
|
|
207
|
+
throw new Error(`Mattermost API ${res.status} ${res.statusText}: ${detail || "unknown error"}`);
|
|
208
|
+
}
|
|
209
|
+
const info = (await res.json()).file_infos?.[0];
|
|
210
|
+
if (!info?.id) throw new Error("Mattermost file upload failed");
|
|
211
|
+
return info;
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region extensions/mattermost/src/mattermost/accounts.ts
|
|
215
|
+
const { listAccountIds: listMattermostAccountIds, resolveDefaultAccountId: resolveDefaultMattermostAccountId } = createAccountListHelpers("mattermost");
|
|
216
|
+
function resolveAccountConfig(cfg, accountId) {
|
|
217
|
+
const accounts = cfg.channels?.mattermost?.accounts;
|
|
218
|
+
if (!accounts || typeof accounts !== "object") return;
|
|
219
|
+
return accounts[accountId];
|
|
220
|
+
}
|
|
221
|
+
function mergeMattermostAccountConfig(cfg, accountId) {
|
|
222
|
+
const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...base } = cfg.channels?.mattermost ?? {};
|
|
223
|
+
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
224
|
+
const mergedCommands = {
|
|
225
|
+
...base.commands ?? {},
|
|
226
|
+
...account.commands ?? {}
|
|
227
|
+
};
|
|
228
|
+
const merged = {
|
|
229
|
+
...base,
|
|
230
|
+
...account
|
|
231
|
+
};
|
|
232
|
+
if (Object.keys(mergedCommands).length > 0) merged.commands = mergedCommands;
|
|
233
|
+
return merged;
|
|
234
|
+
}
|
|
235
|
+
function resolveMattermostRequireMention(config) {
|
|
236
|
+
if (config.chatmode === "oncall") return true;
|
|
237
|
+
if (config.chatmode === "onmessage") return false;
|
|
238
|
+
if (config.chatmode === "onchar") return true;
|
|
239
|
+
return config.requireMention;
|
|
240
|
+
}
|
|
241
|
+
function resolveMattermostAccount(params) {
|
|
242
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
243
|
+
const baseEnabled = params.cfg.channels?.mattermost?.enabled !== false;
|
|
244
|
+
const merged = mergeMattermostAccountConfig(params.cfg, accountId);
|
|
245
|
+
const accountEnabled = merged.enabled !== false;
|
|
246
|
+
const enabled = baseEnabled && accountEnabled;
|
|
247
|
+
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
248
|
+
const envToken = allowEnv ? process.env.MATTERMOST_BOT_TOKEN?.trim() : void 0;
|
|
249
|
+
const envUrl = allowEnv ? process.env.MATTERMOST_URL?.trim() : void 0;
|
|
250
|
+
const configToken = params.allowUnresolvedSecretRef ? normalizeSecretInputString(merged.botToken) : normalizeResolvedSecretInputString({
|
|
251
|
+
value: merged.botToken,
|
|
252
|
+
path: `channels.mattermost.accounts.${accountId}.botToken`
|
|
253
|
+
});
|
|
254
|
+
const configUrl = merged.baseUrl?.trim();
|
|
255
|
+
const botToken = configToken || envToken;
|
|
256
|
+
const baseUrl = normalizeMattermostBaseUrl(configUrl || envUrl);
|
|
257
|
+
const requireMention = resolveMattermostRequireMention(merged);
|
|
258
|
+
const botTokenSource = configToken ? "config" : envToken ? "env" : "none";
|
|
259
|
+
const baseUrlSource = configUrl ? "config" : envUrl ? "env" : "none";
|
|
260
|
+
return {
|
|
261
|
+
accountId,
|
|
262
|
+
enabled,
|
|
263
|
+
name: merged.name?.trim() || void 0,
|
|
264
|
+
botToken,
|
|
265
|
+
baseUrl,
|
|
266
|
+
botTokenSource,
|
|
267
|
+
baseUrlSource,
|
|
268
|
+
config: merged,
|
|
269
|
+
chatmode: merged.chatmode,
|
|
270
|
+
oncharPrefixes: merged.oncharPrefixes,
|
|
271
|
+
requireMention,
|
|
272
|
+
textChunkLimit: merged.textChunkLimit,
|
|
273
|
+
blockStreaming: merged.blockStreaming,
|
|
274
|
+
blockStreamingCoalesce: merged.blockStreamingCoalesce
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Resolve the effective replyToMode for a given chat type.
|
|
279
|
+
* Mattermost auto-threading only applies to channel and group messages.
|
|
280
|
+
*/
|
|
281
|
+
function resolveMattermostReplyToMode(account, kind) {
|
|
282
|
+
if (kind === "direct") return "off";
|
|
283
|
+
return account.config.replyToMode ?? "off";
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region extensions/mattermost/src/group-mentions.ts
|
|
287
|
+
function resolveMattermostGroupRequireMention(params) {
|
|
288
|
+
const account = resolveMattermostAccount({
|
|
289
|
+
cfg: params.cfg,
|
|
290
|
+
accountId: params.accountId
|
|
291
|
+
});
|
|
292
|
+
const requireMentionOverride = typeof params.requireMentionOverride === "boolean" ? params.requireMentionOverride : account.requireMention;
|
|
293
|
+
return resolveChannelGroupRequireMention({
|
|
294
|
+
cfg: params.cfg,
|
|
295
|
+
channel: "mattermost",
|
|
296
|
+
groupId: params.groupId,
|
|
297
|
+
accountId: params.accountId,
|
|
298
|
+
requireMentionOverride
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region extensions/mattermost/src/mattermost/directory.ts
|
|
303
|
+
function buildClient(params) {
|
|
304
|
+
const account = resolveMattermostAccount({
|
|
305
|
+
cfg: params.cfg,
|
|
306
|
+
accountId: params.accountId
|
|
307
|
+
});
|
|
308
|
+
if (!account.enabled || !account.botToken || !account.baseUrl) return null;
|
|
309
|
+
return createMattermostClient({
|
|
310
|
+
baseUrl: account.baseUrl,
|
|
311
|
+
botToken: account.botToken
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Build clients from ALL enabled accounts (deduplicated by token).
|
|
316
|
+
*
|
|
317
|
+
* We always scan every account because:
|
|
318
|
+
* - Private channels are only visible to bots that are members
|
|
319
|
+
* - The requesting agent's account may have an expired/invalid token
|
|
320
|
+
*
|
|
321
|
+
* This means a single healthy bot token is enough for directory discovery.
|
|
322
|
+
*/
|
|
323
|
+
function buildClients(params) {
|
|
324
|
+
const accountIds = listMattermostAccountIds(params.cfg);
|
|
325
|
+
const seen = /* @__PURE__ */ new Set();
|
|
326
|
+
const clients = [];
|
|
327
|
+
for (const id of accountIds) {
|
|
328
|
+
const client = buildClient({
|
|
329
|
+
cfg: params.cfg,
|
|
330
|
+
accountId: id
|
|
331
|
+
});
|
|
332
|
+
if (client && !seen.has(client.token)) {
|
|
333
|
+
seen.add(client.token);
|
|
334
|
+
clients.push(client);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return clients;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* List channels (public + private) visible to any configured bot account.
|
|
341
|
+
*
|
|
342
|
+
* NOTE: Uses per_page=200 which covers most instances. Mattermost does not
|
|
343
|
+
* return a "has more" indicator, so very large instances (200+ channels per bot)
|
|
344
|
+
* may see incomplete results. Pagination can be added if needed.
|
|
345
|
+
*/
|
|
346
|
+
async function listMattermostDirectoryGroups(params) {
|
|
347
|
+
const clients = buildClients(params);
|
|
348
|
+
if (!clients.length) return [];
|
|
349
|
+
const q = params.query?.trim().toLowerCase() || "";
|
|
350
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
351
|
+
const entries = [];
|
|
352
|
+
for (const client of clients) try {
|
|
353
|
+
const me = await fetchMattermostMe(client);
|
|
354
|
+
const channels = await client.request(`/users/${me.id}/channels?per_page=200`);
|
|
355
|
+
for (const ch of channels) {
|
|
356
|
+
if (ch.type !== "O" && ch.type !== "P") continue;
|
|
357
|
+
if (seenIds.has(ch.id)) continue;
|
|
358
|
+
if (q) {
|
|
359
|
+
const name = (ch.name ?? "").toLowerCase();
|
|
360
|
+
const display = (ch.display_name ?? "").toLowerCase();
|
|
361
|
+
if (!name.includes(q) && !display.includes(q)) continue;
|
|
362
|
+
}
|
|
363
|
+
seenIds.add(ch.id);
|
|
364
|
+
entries.push({
|
|
365
|
+
kind: "group",
|
|
366
|
+
id: `channel:${ch.id}`,
|
|
367
|
+
name: ch.name ?? void 0,
|
|
368
|
+
handle: ch.display_name ?? void 0
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.debug?.("[mattermost-directory] listGroups: skipping account:", err?.message);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
return params.limit && params.limit > 0 ? entries.slice(0, params.limit) : entries;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* List team members as peer directory entries.
|
|
379
|
+
*
|
|
380
|
+
* Uses only the first available client since all bots in a team see the same
|
|
381
|
+
* user list (unlike channels where membership varies). Uses the first team
|
|
382
|
+
* returned — multi-team setups will only see members from that team.
|
|
383
|
+
*
|
|
384
|
+
* NOTE: per_page=200 for member listing; same pagination caveat as groups.
|
|
385
|
+
*/
|
|
386
|
+
async function listMattermostDirectoryPeers(params) {
|
|
387
|
+
const clients = buildClients(params);
|
|
388
|
+
if (!clients.length) return [];
|
|
389
|
+
const client = clients[0];
|
|
390
|
+
try {
|
|
391
|
+
const me = await fetchMattermostMe(client);
|
|
392
|
+
const teams = await client.request("/users/me/teams");
|
|
393
|
+
if (!teams.length) return [];
|
|
394
|
+
const teamId = teams[0].id;
|
|
395
|
+
const q = params.query?.trim().toLowerCase() || "";
|
|
396
|
+
let users;
|
|
397
|
+
if (q) users = await client.request("/users/search", {
|
|
398
|
+
method: "POST",
|
|
399
|
+
body: JSON.stringify({
|
|
400
|
+
term: q,
|
|
401
|
+
team_id: teamId
|
|
402
|
+
})
|
|
403
|
+
});
|
|
404
|
+
else {
|
|
405
|
+
const userIds = (await client.request(`/teams/${teamId}/members?per_page=200`)).map((m) => m.user_id).filter((id) => id !== me.id);
|
|
406
|
+
if (!userIds.length) return [];
|
|
407
|
+
users = await client.request("/users/ids", {
|
|
408
|
+
method: "POST",
|
|
409
|
+
body: JSON.stringify(userIds)
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const entries = users.filter((u) => u.id !== me.id).map((u) => ({
|
|
413
|
+
kind: "user",
|
|
414
|
+
id: `user:${u.id}`,
|
|
415
|
+
name: u.username ?? void 0,
|
|
416
|
+
handle: [u.first_name, u.last_name].filter(Boolean).join(" ").trim() || u.nickname || void 0
|
|
417
|
+
}));
|
|
418
|
+
return params.limit && params.limit > 0 ? entries.slice(0, params.limit) : entries;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.debug?.("[mattermost-directory] listPeers failed:", err?.message);
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region extensions/mattermost/src/runtime.ts
|
|
426
|
+
const { setRuntime: setMattermostRuntime, getRuntime: getMattermostRuntime } = createPluginRuntimeStore("Mattermost runtime not initialized");
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region extensions/mattermost/src/mattermost/interactions.ts
|
|
429
|
+
const INTERACTION_MAX_BODY_BYTES = 64 * 1024;
|
|
430
|
+
const INTERACTION_BODY_TIMEOUT_MS = 1e4;
|
|
431
|
+
const SIGNED_CHANNEL_ID_CONTEXT_KEY = "__moldclaw_channel_id";
|
|
432
|
+
const callbackUrls = /* @__PURE__ */ new Map();
|
|
433
|
+
function setInteractionCallbackUrl(accountId, url) {
|
|
434
|
+
callbackUrls.set(accountId, url);
|
|
435
|
+
}
|
|
436
|
+
function resolveInteractionCallbackPath(accountId) {
|
|
437
|
+
return `/mattermost/interactions/${accountId}`;
|
|
438
|
+
}
|
|
439
|
+
function isWildcardBindHost(rawHost) {
|
|
440
|
+
const trimmed = rawHost.trim();
|
|
441
|
+
if (!trimmed) return false;
|
|
442
|
+
const host = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
443
|
+
return host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0" || host === "::0";
|
|
444
|
+
}
|
|
445
|
+
function normalizeCallbackBaseUrl(baseUrl) {
|
|
446
|
+
return baseUrl.trim().replace(/\/+$/, "");
|
|
447
|
+
}
|
|
448
|
+
function headerValue(value) {
|
|
449
|
+
if (Array.isArray(value)) return value[0]?.trim() || void 0;
|
|
450
|
+
return value?.trim() || void 0;
|
|
451
|
+
}
|
|
452
|
+
function isAllowedInteractionSource(params) {
|
|
453
|
+
const { allowedSourceIps } = params;
|
|
454
|
+
if (!allowedSourceIps?.length) return true;
|
|
455
|
+
return isTrustedProxyAddress(resolveClientIp({
|
|
456
|
+
remoteAddr: params.req.socket?.remoteAddress,
|
|
457
|
+
forwardedFor: headerValue(params.req.headers["x-forwarded-for"]),
|
|
458
|
+
realIp: headerValue(params.req.headers["x-real-ip"]),
|
|
459
|
+
trustedProxies: params.trustedProxies,
|
|
460
|
+
allowRealIpFallback: params.allowRealIpFallback
|
|
461
|
+
}), allowedSourceIps);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Resolve the interaction callback URL for an account.
|
|
465
|
+
* Falls back to computing it from interactions.callbackBaseUrl or gateway host config.
|
|
466
|
+
*/
|
|
467
|
+
function computeInteractionCallbackUrl(accountId, cfg) {
|
|
468
|
+
const path = resolveInteractionCallbackPath(accountId);
|
|
469
|
+
const callbackBaseUrl = cfg?.interactions?.callbackBaseUrl?.trim() ?? cfg?.channels?.mattermost?.interactions?.callbackBaseUrl?.trim();
|
|
470
|
+
if (callbackBaseUrl) return `${normalizeCallbackBaseUrl(callbackBaseUrl)}${path}`;
|
|
471
|
+
const port = typeof cfg?.gateway?.port === "number" ? cfg.gateway.port : 18789;
|
|
472
|
+
let host = cfg?.gateway?.customBindHost && !isWildcardBindHost(cfg.gateway.customBindHost) ? cfg.gateway.customBindHost.trim() : "localhost";
|
|
473
|
+
if (host.includes(":") && !(host.startsWith("[") && host.endsWith("]"))) host = `[${host}]`;
|
|
474
|
+
return `http://${host}:${port}${path}`;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Resolve the interaction callback URL for an account.
|
|
478
|
+
* Prefers the in-memory registered URL (set by the gateway monitor) so callers outside the
|
|
479
|
+
* monitor lifecycle can reuse the runtime-validated callback destination.
|
|
480
|
+
*/
|
|
481
|
+
function resolveInteractionCallbackUrl(accountId, cfg) {
|
|
482
|
+
const cached = callbackUrls.get(accountId);
|
|
483
|
+
if (cached) return cached;
|
|
484
|
+
return computeInteractionCallbackUrl(accountId, cfg);
|
|
485
|
+
}
|
|
486
|
+
const interactionSecrets = /* @__PURE__ */ new Map();
|
|
487
|
+
let defaultInteractionSecret;
|
|
488
|
+
function deriveInteractionSecret(botToken) {
|
|
489
|
+
return createHmac("sha256", "moldclaw-mattermost-interactions").update(botToken).digest("hex");
|
|
490
|
+
}
|
|
491
|
+
function setInteractionSecret(accountIdOrBotToken, botToken) {
|
|
492
|
+
if (typeof botToken === "string") {
|
|
493
|
+
interactionSecrets.set(accountIdOrBotToken, deriveInteractionSecret(botToken));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
defaultInteractionSecret = deriveInteractionSecret(accountIdOrBotToken);
|
|
497
|
+
}
|
|
498
|
+
function getInteractionSecret(accountId) {
|
|
499
|
+
const scoped = accountId ? interactionSecrets.get(accountId) : void 0;
|
|
500
|
+
if (scoped) return scoped;
|
|
501
|
+
if (defaultInteractionSecret) return defaultInteractionSecret;
|
|
502
|
+
if (interactionSecrets.size === 1) {
|
|
503
|
+
const first = interactionSecrets.values().next().value;
|
|
504
|
+
if (typeof first === "string") return first;
|
|
505
|
+
}
|
|
506
|
+
throw new Error("Interaction secret not initialized — call setInteractionSecret(accountId, botToken) first");
|
|
507
|
+
}
|
|
508
|
+
function canonicalizeInteractionContext(value) {
|
|
509
|
+
if (Array.isArray(value)) return value.map((item) => canonicalizeInteractionContext(item));
|
|
510
|
+
if (value && typeof value === "object") {
|
|
511
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, canonicalizeInteractionContext(entryValue)]);
|
|
512
|
+
return Object.fromEntries(entries);
|
|
513
|
+
}
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
function generateInteractionToken(context, accountId) {
|
|
517
|
+
const secret = getInteractionSecret(accountId);
|
|
518
|
+
const payload = JSON.stringify(canonicalizeInteractionContext(context));
|
|
519
|
+
return createHmac("sha256", secret).update(payload).digest("hex");
|
|
520
|
+
}
|
|
521
|
+
function verifyInteractionToken(context, token, accountId) {
|
|
522
|
+
const expected = generateInteractionToken(context, accountId);
|
|
523
|
+
if (expected.length !== token.length) return false;
|
|
524
|
+
return timingSafeEqual(Buffer.from(expected), Buffer.from(token));
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Build Mattermost `props.attachments` with interactive buttons.
|
|
528
|
+
*
|
|
529
|
+
* Each button includes an HMAC token in its integration context so the
|
|
530
|
+
* callback handler can verify the request originated from a legitimate
|
|
531
|
+
* button click (Mattermost's recommended security pattern).
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* Sanitize a button ID so Mattermost's action router can match it.
|
|
535
|
+
* Mattermost uses the action ID in the URL path `/api/v4/posts/{id}/actions/{actionId}`
|
|
536
|
+
* and IDs containing hyphens or underscores break the server-side routing.
|
|
537
|
+
* See: https://github.com/mattermost/mattermost/issues/25747
|
|
538
|
+
*/
|
|
539
|
+
function sanitizeActionId(id) {
|
|
540
|
+
return id.replace(/[-_]/g, "");
|
|
541
|
+
}
|
|
542
|
+
function buildButtonAttachments(params) {
|
|
543
|
+
const actions = params.buttons.map((btn) => {
|
|
544
|
+
const safeId = sanitizeActionId(btn.id);
|
|
545
|
+
const context = {
|
|
546
|
+
action_id: safeId,
|
|
547
|
+
...btn.context
|
|
548
|
+
};
|
|
549
|
+
const token = generateInteractionToken(context, params.accountId);
|
|
550
|
+
return {
|
|
551
|
+
id: safeId,
|
|
552
|
+
type: "button",
|
|
553
|
+
name: btn.name,
|
|
554
|
+
style: btn.style,
|
|
555
|
+
integration: {
|
|
556
|
+
url: params.callbackUrl,
|
|
557
|
+
context: {
|
|
558
|
+
...context,
|
|
559
|
+
_token: token
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
return [{
|
|
565
|
+
text: params.text ?? "",
|
|
566
|
+
actions
|
|
567
|
+
}];
|
|
568
|
+
}
|
|
569
|
+
function buildButtonProps(params) {
|
|
570
|
+
const buttons = params.buttons.flatMap((item) => Array.isArray(item) ? item : [item]).map((btn) => ({
|
|
571
|
+
id: String(btn.id ?? btn.callback_data ?? "").trim(),
|
|
572
|
+
name: String(btn.text ?? btn.name ?? btn.label ?? "").trim(),
|
|
573
|
+
style: btn.style ?? "default",
|
|
574
|
+
context: typeof btn.context === "object" && btn.context !== null ? {
|
|
575
|
+
...btn.context,
|
|
576
|
+
[SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId
|
|
577
|
+
} : { [SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId }
|
|
578
|
+
})).filter((btn) => btn.id && btn.name);
|
|
579
|
+
if (buttons.length === 0) return;
|
|
580
|
+
return { attachments: buildButtonAttachments({
|
|
581
|
+
callbackUrl: params.callbackUrl,
|
|
582
|
+
accountId: params.accountId,
|
|
583
|
+
buttons,
|
|
584
|
+
text: params.text
|
|
585
|
+
}) };
|
|
586
|
+
}
|
|
587
|
+
function readInteractionBody(req) {
|
|
588
|
+
return new Promise((resolve, reject) => {
|
|
589
|
+
const chunks = [];
|
|
590
|
+
let totalBytes = 0;
|
|
591
|
+
const timer = setTimeout(() => {
|
|
592
|
+
req.destroy();
|
|
593
|
+
reject(/* @__PURE__ */ new Error("Request body read timeout"));
|
|
594
|
+
}, INTERACTION_BODY_TIMEOUT_MS);
|
|
595
|
+
req.on("data", (chunk) => {
|
|
596
|
+
totalBytes += chunk.length;
|
|
597
|
+
if (totalBytes > INTERACTION_MAX_BODY_BYTES) {
|
|
598
|
+
req.destroy();
|
|
599
|
+
clearTimeout(timer);
|
|
600
|
+
reject(/* @__PURE__ */ new Error("Request body too large"));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
chunks.push(chunk);
|
|
604
|
+
});
|
|
605
|
+
req.on("end", () => {
|
|
606
|
+
clearTimeout(timer);
|
|
607
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
608
|
+
});
|
|
609
|
+
req.on("error", (err) => {
|
|
610
|
+
clearTimeout(timer);
|
|
611
|
+
reject(err);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
function createMattermostInteractionHandler(params) {
|
|
616
|
+
const { client, accountId, log } = params;
|
|
617
|
+
const core = getMattermostRuntime();
|
|
618
|
+
return async (req, res) => {
|
|
619
|
+
if (req.method !== "POST") {
|
|
620
|
+
res.statusCode = 405;
|
|
621
|
+
res.setHeader("Allow", "POST");
|
|
622
|
+
res.setHeader("Content-Type", "application/json");
|
|
623
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (!isAllowedInteractionSource({
|
|
627
|
+
req,
|
|
628
|
+
allowedSourceIps: params.allowedSourceIps,
|
|
629
|
+
trustedProxies: params.trustedProxies,
|
|
630
|
+
allowRealIpFallback: params.allowRealIpFallback
|
|
631
|
+
})) {
|
|
632
|
+
log?.(`mattermost interaction: rejected callback source remote=${req.socket?.remoteAddress ?? "?"}`);
|
|
633
|
+
res.statusCode = 403;
|
|
634
|
+
res.setHeader("Content-Type", "application/json");
|
|
635
|
+
res.end(JSON.stringify({ error: "Forbidden origin" }));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
let payload;
|
|
639
|
+
try {
|
|
640
|
+
const raw = await readInteractionBody(req);
|
|
641
|
+
payload = JSON.parse(raw);
|
|
642
|
+
} catch (err) {
|
|
643
|
+
log?.(`mattermost interaction: failed to parse body: ${String(err)}`);
|
|
644
|
+
res.statusCode = 400;
|
|
645
|
+
res.setHeader("Content-Type", "application/json");
|
|
646
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const context = payload.context;
|
|
650
|
+
if (!context) {
|
|
651
|
+
res.statusCode = 400;
|
|
652
|
+
res.setHeader("Content-Type", "application/json");
|
|
653
|
+
res.end(JSON.stringify({ error: "Missing context" }));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const token = context._token;
|
|
657
|
+
if (typeof token !== "string") {
|
|
658
|
+
log?.("mattermost interaction: missing _token in context");
|
|
659
|
+
res.statusCode = 403;
|
|
660
|
+
res.setHeader("Content-Type", "application/json");
|
|
661
|
+
res.end(JSON.stringify({ error: "Missing token" }));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const { _token, ...contextWithoutToken } = context;
|
|
665
|
+
if (!verifyInteractionToken(contextWithoutToken, token, accountId)) {
|
|
666
|
+
log?.("mattermost interaction: invalid _token");
|
|
667
|
+
res.statusCode = 403;
|
|
668
|
+
res.setHeader("Content-Type", "application/json");
|
|
669
|
+
res.end(JSON.stringify({ error: "Invalid token" }));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const actionId = context.action_id;
|
|
673
|
+
if (typeof actionId !== "string") {
|
|
674
|
+
res.statusCode = 400;
|
|
675
|
+
res.setHeader("Content-Type", "application/json");
|
|
676
|
+
res.end(JSON.stringify({ error: "Missing action_id in context" }));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const signedChannelId = typeof contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY] === "string" ? contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY].trim() : "";
|
|
680
|
+
if (signedChannelId && signedChannelId !== payload.channel_id) {
|
|
681
|
+
log?.(`mattermost interaction: signed channel mismatch payload=${payload.channel_id} signed=${signedChannelId}`);
|
|
682
|
+
res.statusCode = 403;
|
|
683
|
+
res.setHeader("Content-Type", "application/json");
|
|
684
|
+
res.end(JSON.stringify({ error: "Channel mismatch" }));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const userName = payload.user_name ?? payload.user_id;
|
|
688
|
+
let originalMessage = "";
|
|
689
|
+
let originalPost = null;
|
|
690
|
+
let clickedButtonName = null;
|
|
691
|
+
try {
|
|
692
|
+
originalPost = await client.request(`/posts/${payload.post_id}`);
|
|
693
|
+
const postChannelId = originalPost.channel_id?.trim();
|
|
694
|
+
if (!postChannelId || postChannelId !== payload.channel_id) {
|
|
695
|
+
log?.(`mattermost interaction: post channel mismatch payload=${payload.channel_id} post=${postChannelId ?? "<missing>"}`);
|
|
696
|
+
res.statusCode = 403;
|
|
697
|
+
res.setHeader("Content-Type", "application/json");
|
|
698
|
+
res.end(JSON.stringify({ error: "Post/channel mismatch" }));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
originalMessage = originalPost.message ?? "";
|
|
702
|
+
const postAttachments = Array.isArray(originalPost?.props?.attachments) ? originalPost.props.attachments : [];
|
|
703
|
+
for (const att of postAttachments) {
|
|
704
|
+
const match = att.actions?.find((a) => a.id === actionId);
|
|
705
|
+
if (match?.name) {
|
|
706
|
+
clickedButtonName = match.name;
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (clickedButtonName === null) {
|
|
711
|
+
log?.(`mattermost interaction: action ${actionId} not found in post ${payload.post_id}`);
|
|
712
|
+
res.statusCode = 403;
|
|
713
|
+
res.setHeader("Content-Type", "application/json");
|
|
714
|
+
res.end(JSON.stringify({ error: "Unknown action" }));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
} catch (err) {
|
|
718
|
+
log?.(`mattermost interaction: failed to validate post ${payload.post_id}: ${String(err)}`);
|
|
719
|
+
res.statusCode = 500;
|
|
720
|
+
res.setHeader("Content-Type", "application/json");
|
|
721
|
+
res.end(JSON.stringify({ error: "Failed to validate interaction" }));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (!originalPost) {
|
|
725
|
+
log?.(`mattermost interaction: missing fetched post ${payload.post_id}`);
|
|
726
|
+
res.statusCode = 500;
|
|
727
|
+
res.setHeader("Content-Type", "application/json");
|
|
728
|
+
res.end(JSON.stringify({ error: "Failed to load interaction post" }));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
log?.(`mattermost interaction: action=${actionId} user=${payload.user_name ?? payload.user_id} post=${payload.post_id} channel=${payload.channel_id}`);
|
|
732
|
+
if (params.authorizeButtonClick) try {
|
|
733
|
+
const authorization = await params.authorizeButtonClick({
|
|
734
|
+
payload,
|
|
735
|
+
post: originalPost
|
|
736
|
+
});
|
|
737
|
+
if (!authorization.ok) {
|
|
738
|
+
res.statusCode = authorization.statusCode ?? 200;
|
|
739
|
+
res.setHeader("Content-Type", "application/json");
|
|
740
|
+
res.end(JSON.stringify(authorization.response ?? { ephemeral_text: "You are not allowed to use this action here." }));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
} catch (err) {
|
|
744
|
+
log?.(`mattermost interaction: authorization failed: ${String(err)}`);
|
|
745
|
+
res.statusCode = 500;
|
|
746
|
+
res.setHeader("Content-Type", "application/json");
|
|
747
|
+
res.end(JSON.stringify({ error: "Interaction authorization failed" }));
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (params.handleInteraction) try {
|
|
751
|
+
const response = await params.handleInteraction({
|
|
752
|
+
payload,
|
|
753
|
+
userName,
|
|
754
|
+
actionId,
|
|
755
|
+
actionName: clickedButtonName,
|
|
756
|
+
originalMessage,
|
|
757
|
+
context: contextWithoutToken,
|
|
758
|
+
post: originalPost
|
|
759
|
+
});
|
|
760
|
+
if (response !== null) {
|
|
761
|
+
res.statusCode = 200;
|
|
762
|
+
res.setHeader("Content-Type", "application/json");
|
|
763
|
+
res.end(JSON.stringify(response));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
} catch (err) {
|
|
767
|
+
log?.(`mattermost interaction: custom handler failed: ${String(err)}`);
|
|
768
|
+
res.statusCode = 500;
|
|
769
|
+
res.setHeader("Content-Type", "application/json");
|
|
770
|
+
res.end(JSON.stringify({ error: "Interaction handler failed" }));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const eventLabel = `Mattermost button click: action="${actionId}" by ${payload.user_name ?? payload.user_id} in channel ${payload.channel_id}`;
|
|
775
|
+
const sessionKey = params.resolveSessionKey ? await params.resolveSessionKey({
|
|
776
|
+
channelId: payload.channel_id,
|
|
777
|
+
userId: payload.user_id,
|
|
778
|
+
post: originalPost
|
|
779
|
+
}) : `agent:main:mattermost:${accountId}:${payload.channel_id}`;
|
|
780
|
+
core.system.enqueueSystemEvent(eventLabel, {
|
|
781
|
+
sessionKey,
|
|
782
|
+
contextKey: `mattermost:interaction:${payload.post_id}:${actionId}`
|
|
783
|
+
});
|
|
784
|
+
} catch (err) {
|
|
785
|
+
log?.(`mattermost interaction: system event dispatch failed: ${String(err)}`);
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
await updateMattermostPost(client, payload.post_id, {
|
|
789
|
+
message: originalMessage,
|
|
790
|
+
props: { attachments: [{ text: `✓ **${clickedButtonName}** selected by @${userName}` }] }
|
|
791
|
+
});
|
|
792
|
+
} catch (err) {
|
|
793
|
+
log?.(`mattermost interaction: failed to update post ${payload.post_id}: ${String(err)}`);
|
|
794
|
+
}
|
|
795
|
+
res.statusCode = 200;
|
|
796
|
+
res.setHeader("Content-Type", "application/json");
|
|
797
|
+
res.end("{}");
|
|
798
|
+
if (params.dispatchButtonClick) try {
|
|
799
|
+
await params.dispatchButtonClick({
|
|
800
|
+
channelId: payload.channel_id,
|
|
801
|
+
userId: payload.user_id,
|
|
802
|
+
userName,
|
|
803
|
+
actionId,
|
|
804
|
+
actionName: clickedButtonName,
|
|
805
|
+
postId: payload.post_id,
|
|
806
|
+
post: originalPost
|
|
807
|
+
});
|
|
808
|
+
} catch (err) {
|
|
809
|
+
log?.(`mattermost interaction: dispatchButtonClick failed: ${String(err)}`);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region extensions/mattermost/src/mattermost/model-picker.ts
|
|
815
|
+
const MATTERMOST_MODEL_PICKER_CONTEXT_KEY = "oc_model_picker";
|
|
816
|
+
const MODELS_PAGE_SIZE = 8;
|
|
817
|
+
const ACTION_IDS = {
|
|
818
|
+
providers: "mdlprov",
|
|
819
|
+
list: "mdllist",
|
|
820
|
+
select: "mdlsel",
|
|
821
|
+
back: "mdlback"
|
|
822
|
+
};
|
|
823
|
+
function splitModelRef(modelRef) {
|
|
824
|
+
const match = (modelRef?.trim())?.match(/^([^/]+)\/(.+)$/u);
|
|
825
|
+
if (!match) return null;
|
|
826
|
+
const provider = normalizeProviderId(match[1]);
|
|
827
|
+
const model = match[2].trim();
|
|
828
|
+
if (!provider || !model) return null;
|
|
829
|
+
return {
|
|
830
|
+
provider,
|
|
831
|
+
model
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function normalizePage(value) {
|
|
835
|
+
if (!Number.isFinite(value)) return 1;
|
|
836
|
+
return Math.max(1, Math.floor(value));
|
|
837
|
+
}
|
|
838
|
+
function paginateItems(items, page, pageSize = MODELS_PAGE_SIZE) {
|
|
839
|
+
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
840
|
+
const safePage = Math.max(1, Math.min(normalizePage(page), totalPages));
|
|
841
|
+
const start = (safePage - 1) * pageSize;
|
|
842
|
+
return {
|
|
843
|
+
items: items.slice(start, start + pageSize),
|
|
844
|
+
page: safePage,
|
|
845
|
+
totalPages,
|
|
846
|
+
hasPrev: safePage > 1,
|
|
847
|
+
hasNext: safePage < totalPages,
|
|
848
|
+
totalItems: items.length
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function buildContext(state) {
|
|
852
|
+
return {
|
|
853
|
+
[MATTERMOST_MODEL_PICKER_CONTEXT_KEY]: true,
|
|
854
|
+
...state
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function buildButtonId(state) {
|
|
858
|
+
const digest = createHash("sha256").update(JSON.stringify(state)).digest("hex").slice(0, 12);
|
|
859
|
+
return `${ACTION_IDS[state.action]}${digest}`;
|
|
860
|
+
}
|
|
861
|
+
function buildButton(params) {
|
|
862
|
+
const baseState = params.action === "providers" || params.action === "back" ? {
|
|
863
|
+
action: params.action,
|
|
864
|
+
ownerUserId: params.ownerUserId
|
|
865
|
+
} : params.action === "list" ? {
|
|
866
|
+
action: "list",
|
|
867
|
+
ownerUserId: params.ownerUserId,
|
|
868
|
+
provider: normalizeProviderId(params.provider ?? ""),
|
|
869
|
+
page: normalizePage(params.page)
|
|
870
|
+
} : {
|
|
871
|
+
action: "select",
|
|
872
|
+
ownerUserId: params.ownerUserId,
|
|
873
|
+
provider: normalizeProviderId(params.provider ?? ""),
|
|
874
|
+
page: normalizePage(params.page),
|
|
875
|
+
model: String(params.model ?? "").trim()
|
|
876
|
+
};
|
|
877
|
+
return {
|
|
878
|
+
id: buildButtonId(baseState),
|
|
879
|
+
text: params.text,
|
|
880
|
+
...params.style ? { style: params.style } : {},
|
|
881
|
+
context: buildContext(baseState)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function getProviderModels(data, provider) {
|
|
885
|
+
return [...data.byProvider.get(normalizeProviderId(provider)) ?? /* @__PURE__ */ new Set()].toSorted();
|
|
886
|
+
}
|
|
887
|
+
function formatCurrentModelLine(currentModel) {
|
|
888
|
+
const parsed = splitModelRef(currentModel);
|
|
889
|
+
if (!parsed) return "Current: default";
|
|
890
|
+
return `Current: ${parsed.provider}/${parsed.model}`;
|
|
891
|
+
}
|
|
892
|
+
function resolveMattermostModelPickerEntry(commandText) {
|
|
893
|
+
const normalized = commandText.trim().replace(/\s+/g, " ");
|
|
894
|
+
if (/^\/model$/i.test(normalized)) return { kind: "summary" };
|
|
895
|
+
if (/^\/models$/i.test(normalized)) return { kind: "providers" };
|
|
896
|
+
const providerMatch = normalized.match(/^\/models\s+(\S+)$/i);
|
|
897
|
+
if (!providerMatch?.[1]) return null;
|
|
898
|
+
return {
|
|
899
|
+
kind: "models",
|
|
900
|
+
provider: normalizeProviderId(providerMatch[1])
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function parseMattermostModelPickerContext(context) {
|
|
904
|
+
if (!context || context[MATTERMOST_MODEL_PICKER_CONTEXT_KEY] !== true) return null;
|
|
905
|
+
const ownerUserId = String(context.ownerUserId ?? "").trim();
|
|
906
|
+
const action = String(context.action ?? "").trim();
|
|
907
|
+
if (!ownerUserId) return null;
|
|
908
|
+
if (action === "providers" || action === "back") return {
|
|
909
|
+
action,
|
|
910
|
+
ownerUserId
|
|
911
|
+
};
|
|
912
|
+
const provider = normalizeProviderId(String(context.provider ?? ""));
|
|
913
|
+
const page = Number.parseInt(String(context.page ?? "1"), 10);
|
|
914
|
+
if (!provider) return null;
|
|
915
|
+
if (action === "list") return {
|
|
916
|
+
action,
|
|
917
|
+
ownerUserId,
|
|
918
|
+
provider,
|
|
919
|
+
page: normalizePage(page)
|
|
920
|
+
};
|
|
921
|
+
if (action === "select") {
|
|
922
|
+
const model = String(context.model ?? "").trim();
|
|
923
|
+
if (!model) return null;
|
|
924
|
+
return {
|
|
925
|
+
action,
|
|
926
|
+
ownerUserId,
|
|
927
|
+
provider,
|
|
928
|
+
page: normalizePage(page),
|
|
929
|
+
model
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
function buildMattermostAllowedModelRefs(data) {
|
|
935
|
+
const refs = /* @__PURE__ */ new Set();
|
|
936
|
+
for (const provider of data.providers) for (const model of data.byProvider.get(provider) ?? []) refs.add(`${provider}/${model}`);
|
|
937
|
+
return refs;
|
|
938
|
+
}
|
|
939
|
+
function resolveMattermostModelPickerCurrentModel(params) {
|
|
940
|
+
const fallback = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
|
|
941
|
+
try {
|
|
942
|
+
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.route.agentId });
|
|
943
|
+
const sessionStore = params.skipCache ? loadSessionStore(storePath, { skipCache: true }) : loadSessionStore(storePath);
|
|
944
|
+
const sessionEntry = sessionStore[params.route.sessionKey];
|
|
945
|
+
const override = resolveStoredModelOverride({
|
|
946
|
+
sessionEntry,
|
|
947
|
+
sessionStore,
|
|
948
|
+
sessionKey: params.route.sessionKey
|
|
949
|
+
});
|
|
950
|
+
if (!override?.model) return fallback;
|
|
951
|
+
const provider = (override.provider || params.data.resolvedDefault.provider).trim();
|
|
952
|
+
return provider ? `${provider}/${override.model}` : fallback;
|
|
953
|
+
} catch {
|
|
954
|
+
return fallback;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function renderMattermostModelSummaryView(params) {
|
|
958
|
+
return {
|
|
959
|
+
text: [
|
|
960
|
+
formatCurrentModelLine(params.currentModel),
|
|
961
|
+
"",
|
|
962
|
+
"Tap below to browse models, or use:",
|
|
963
|
+
"/oc_model <provider/model> to switch",
|
|
964
|
+
"/oc_model status for details"
|
|
965
|
+
].join("\n"),
|
|
966
|
+
buttons: [[buildButton({
|
|
967
|
+
action: "providers",
|
|
968
|
+
ownerUserId: params.ownerUserId,
|
|
969
|
+
text: "Browse providers",
|
|
970
|
+
style: "primary"
|
|
971
|
+
})]]
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function renderMattermostProviderPickerView(params) {
|
|
975
|
+
const currentProvider = splitModelRef(params.currentModel)?.provider;
|
|
976
|
+
const rows = params.data.providers.map((provider) => [buildButton({
|
|
977
|
+
action: "list",
|
|
978
|
+
ownerUserId: params.ownerUserId,
|
|
979
|
+
text: `${provider} (${params.data.byProvider.get(provider)?.size ?? 0})`,
|
|
980
|
+
provider,
|
|
981
|
+
page: 1,
|
|
982
|
+
style: provider === currentProvider ? "primary" : "default"
|
|
983
|
+
})]);
|
|
984
|
+
return {
|
|
985
|
+
text: [
|
|
986
|
+
formatCurrentModelLine(params.currentModel),
|
|
987
|
+
"",
|
|
988
|
+
"Select a provider:"
|
|
989
|
+
].join("\n"),
|
|
990
|
+
buttons: rows
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function renderMattermostModelsPickerView(params) {
|
|
994
|
+
const provider = normalizeProviderId(params.provider);
|
|
995
|
+
const models = getProviderModels(params.data, provider);
|
|
996
|
+
const current = splitModelRef(params.currentModel);
|
|
997
|
+
if (models.length === 0) return {
|
|
998
|
+
text: [
|
|
999
|
+
formatCurrentModelLine(params.currentModel),
|
|
1000
|
+
"",
|
|
1001
|
+
`Unknown provider: ${provider}`
|
|
1002
|
+
].join("\n"),
|
|
1003
|
+
buttons: [[buildButton({
|
|
1004
|
+
action: "back",
|
|
1005
|
+
ownerUserId: params.ownerUserId,
|
|
1006
|
+
text: "Back to providers"
|
|
1007
|
+
})]]
|
|
1008
|
+
};
|
|
1009
|
+
const page = paginateItems(models, params.page);
|
|
1010
|
+
const rows = page.items.map((model) => {
|
|
1011
|
+
const isCurrent = current?.provider === provider && current.model === model;
|
|
1012
|
+
return [buildButton({
|
|
1013
|
+
action: "select",
|
|
1014
|
+
ownerUserId: params.ownerUserId,
|
|
1015
|
+
text: isCurrent ? `${model} [current]` : model,
|
|
1016
|
+
provider,
|
|
1017
|
+
model,
|
|
1018
|
+
page: page.page,
|
|
1019
|
+
style: isCurrent ? "primary" : "default"
|
|
1020
|
+
})];
|
|
1021
|
+
});
|
|
1022
|
+
const navRow = [];
|
|
1023
|
+
if (page.hasPrev) navRow.push(buildButton({
|
|
1024
|
+
action: "list",
|
|
1025
|
+
ownerUserId: params.ownerUserId,
|
|
1026
|
+
text: "Prev",
|
|
1027
|
+
provider,
|
|
1028
|
+
page: page.page - 1
|
|
1029
|
+
}));
|
|
1030
|
+
if (page.hasNext) navRow.push(buildButton({
|
|
1031
|
+
action: "list",
|
|
1032
|
+
ownerUserId: params.ownerUserId,
|
|
1033
|
+
text: "Next",
|
|
1034
|
+
provider,
|
|
1035
|
+
page: page.page + 1
|
|
1036
|
+
}));
|
|
1037
|
+
if (navRow.length > 0) rows.push(navRow);
|
|
1038
|
+
rows.push([buildButton({
|
|
1039
|
+
action: "back",
|
|
1040
|
+
ownerUserId: params.ownerUserId,
|
|
1041
|
+
text: "Back to providers"
|
|
1042
|
+
})]);
|
|
1043
|
+
return {
|
|
1044
|
+
text: [
|
|
1045
|
+
`Models (${provider}) - ${page.totalItems} available`,
|
|
1046
|
+
formatCurrentModelLine(params.currentModel),
|
|
1047
|
+
`Page ${page.page}/${page.totalPages}`,
|
|
1048
|
+
"Select a model to switch immediately."
|
|
1049
|
+
].join("\n"),
|
|
1050
|
+
buttons: rows
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
//#endregion
|
|
1054
|
+
//#region extensions/mattermost/src/mattermost/monitor-auth.ts
|
|
1055
|
+
function normalizeMattermostAllowEntry(entry) {
|
|
1056
|
+
const trimmed = entry.trim();
|
|
1057
|
+
if (!trimmed) return "";
|
|
1058
|
+
if (trimmed === "*") return "*";
|
|
1059
|
+
return trimmed.replace(/^(mattermost|user):/i, "").replace(/^@/, "").toLowerCase();
|
|
1060
|
+
}
|
|
1061
|
+
function normalizeMattermostAllowList(entries) {
|
|
1062
|
+
const normalized = entries.map((entry) => normalizeMattermostAllowEntry(String(entry))).filter(Boolean);
|
|
1063
|
+
return Array.from(new Set(normalized));
|
|
1064
|
+
}
|
|
1065
|
+
function resolveMattermostEffectiveAllowFromLists(params) {
|
|
1066
|
+
return resolveEffectiveAllowFromLists({
|
|
1067
|
+
allowFrom: normalizeMattermostAllowList(params.allowFrom ?? []),
|
|
1068
|
+
groupAllowFrom: normalizeMattermostAllowList(params.groupAllowFrom ?? []),
|
|
1069
|
+
storeAllowFrom: normalizeMattermostAllowList(params.storeAllowFrom ?? []),
|
|
1070
|
+
dmPolicy: params.dmPolicy
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function isMattermostSenderAllowed(params) {
|
|
1074
|
+
const allowFrom = normalizeMattermostAllowList(params.allowFrom);
|
|
1075
|
+
if (allowFrom.length === 0) return false;
|
|
1076
|
+
return resolveAllowlistMatchSimple({
|
|
1077
|
+
allowFrom,
|
|
1078
|
+
senderId: normalizeMattermostAllowEntry(params.senderId),
|
|
1079
|
+
senderName: params.senderName ? normalizeMattermostAllowEntry(params.senderName) : void 0,
|
|
1080
|
+
allowNameMatching: params.allowNameMatching
|
|
1081
|
+
}).allowed;
|
|
1082
|
+
}
|
|
1083
|
+
function mapMattermostChannelKind(channelType) {
|
|
1084
|
+
const normalized = channelType?.trim().toUpperCase();
|
|
1085
|
+
if (normalized === "D") return "direct";
|
|
1086
|
+
if (normalized === "G" || normalized === "P") return "group";
|
|
1087
|
+
return "channel";
|
|
1088
|
+
}
|
|
1089
|
+
function authorizeMattermostCommandInvocation(params) {
|
|
1090
|
+
const { account, cfg, senderId, senderName, channelId, channelInfo, storeAllowFrom, allowTextCommands, hasControlCommand } = params;
|
|
1091
|
+
if (!channelInfo) return {
|
|
1092
|
+
ok: false,
|
|
1093
|
+
denyReason: "unknown-channel",
|
|
1094
|
+
commandAuthorized: false,
|
|
1095
|
+
channelInfo: null,
|
|
1096
|
+
kind: "channel",
|
|
1097
|
+
chatType: "channel",
|
|
1098
|
+
channelName: "",
|
|
1099
|
+
channelDisplay: "",
|
|
1100
|
+
roomLabel: `#${channelId}`
|
|
1101
|
+
};
|
|
1102
|
+
const kind = mapMattermostChannelKind(channelInfo.type);
|
|
1103
|
+
const chatType = kind;
|
|
1104
|
+
const channelName = channelInfo.name ?? "";
|
|
1105
|
+
const channelDisplay = channelInfo.display_name ?? channelName;
|
|
1106
|
+
const roomLabel = channelName ? `#${channelName}` : channelDisplay || `#${channelId}`;
|
|
1107
|
+
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
1108
|
+
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
1109
|
+
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
1110
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
1111
|
+
const configAllowFrom = normalizeMattermostAllowList(account.config.allowFrom ?? []);
|
|
1112
|
+
const configGroupAllowFrom = normalizeMattermostAllowList(account.config.groupAllowFrom ?? []);
|
|
1113
|
+
const { effectiveAllowFrom, effectiveGroupAllowFrom } = resolveMattermostEffectiveAllowFromLists({
|
|
1114
|
+
allowFrom: configAllowFrom,
|
|
1115
|
+
groupAllowFrom: configGroupAllowFrom,
|
|
1116
|
+
storeAllowFrom: normalizeMattermostAllowList(storeAllowFrom ?? []),
|
|
1117
|
+
dmPolicy
|
|
1118
|
+
});
|
|
1119
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
1120
|
+
const commandDmAllowFrom = kind === "direct" ? effectiveAllowFrom : configAllowFrom;
|
|
1121
|
+
const commandGroupAllowFrom = kind === "direct" ? effectiveGroupAllowFrom : configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom;
|
|
1122
|
+
const senderAllowedForCommands = isMattermostSenderAllowed({
|
|
1123
|
+
senderId,
|
|
1124
|
+
senderName,
|
|
1125
|
+
allowFrom: commandDmAllowFrom,
|
|
1126
|
+
allowNameMatching
|
|
1127
|
+
});
|
|
1128
|
+
const groupAllowedForCommands = isMattermostSenderAllowed({
|
|
1129
|
+
senderId,
|
|
1130
|
+
senderName,
|
|
1131
|
+
allowFrom: commandGroupAllowFrom,
|
|
1132
|
+
allowNameMatching
|
|
1133
|
+
});
|
|
1134
|
+
const commandGate = resolveControlCommandGate({
|
|
1135
|
+
useAccessGroups,
|
|
1136
|
+
authorizers: [{
|
|
1137
|
+
configured: commandDmAllowFrom.length > 0,
|
|
1138
|
+
allowed: senderAllowedForCommands
|
|
1139
|
+
}, {
|
|
1140
|
+
configured: commandGroupAllowFrom.length > 0,
|
|
1141
|
+
allowed: groupAllowedForCommands
|
|
1142
|
+
}],
|
|
1143
|
+
allowTextCommands,
|
|
1144
|
+
hasControlCommand: allowTextCommands && hasControlCommand
|
|
1145
|
+
});
|
|
1146
|
+
const commandAuthorized = kind === "direct" ? dmPolicy === "open" || senderAllowedForCommands : commandGate.commandAuthorized;
|
|
1147
|
+
if (kind === "direct") {
|
|
1148
|
+
if (dmPolicy === "disabled") return {
|
|
1149
|
+
ok: false,
|
|
1150
|
+
denyReason: "dm-disabled",
|
|
1151
|
+
commandAuthorized: false,
|
|
1152
|
+
channelInfo,
|
|
1153
|
+
kind,
|
|
1154
|
+
chatType,
|
|
1155
|
+
channelName,
|
|
1156
|
+
channelDisplay,
|
|
1157
|
+
roomLabel
|
|
1158
|
+
};
|
|
1159
|
+
if (dmPolicy !== "open" && !senderAllowedForCommands) return {
|
|
1160
|
+
ok: false,
|
|
1161
|
+
denyReason: dmPolicy === "pairing" ? "dm-pairing" : "unauthorized",
|
|
1162
|
+
commandAuthorized: false,
|
|
1163
|
+
channelInfo,
|
|
1164
|
+
kind,
|
|
1165
|
+
chatType,
|
|
1166
|
+
channelName,
|
|
1167
|
+
channelDisplay,
|
|
1168
|
+
roomLabel
|
|
1169
|
+
};
|
|
1170
|
+
} else {
|
|
1171
|
+
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
|
1172
|
+
groupPolicy,
|
|
1173
|
+
groupAllowFrom: effectiveGroupAllowFrom,
|
|
1174
|
+
senderId,
|
|
1175
|
+
isSenderAllowed: (_senderId, allowFrom) => isMattermostSenderAllowed({
|
|
1176
|
+
senderId,
|
|
1177
|
+
senderName,
|
|
1178
|
+
allowFrom,
|
|
1179
|
+
allowNameMatching
|
|
1180
|
+
})
|
|
1181
|
+
});
|
|
1182
|
+
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") return {
|
|
1183
|
+
ok: false,
|
|
1184
|
+
denyReason: "channels-disabled",
|
|
1185
|
+
commandAuthorized: false,
|
|
1186
|
+
channelInfo,
|
|
1187
|
+
kind,
|
|
1188
|
+
chatType,
|
|
1189
|
+
channelName,
|
|
1190
|
+
channelDisplay,
|
|
1191
|
+
roomLabel
|
|
1192
|
+
};
|
|
1193
|
+
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") return {
|
|
1194
|
+
ok: false,
|
|
1195
|
+
denyReason: "channel-no-allowlist",
|
|
1196
|
+
commandAuthorized: false,
|
|
1197
|
+
channelInfo,
|
|
1198
|
+
kind,
|
|
1199
|
+
chatType,
|
|
1200
|
+
channelName,
|
|
1201
|
+
channelDisplay,
|
|
1202
|
+
roomLabel
|
|
1203
|
+
};
|
|
1204
|
+
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") return {
|
|
1205
|
+
ok: false,
|
|
1206
|
+
denyReason: "unauthorized",
|
|
1207
|
+
commandAuthorized: false,
|
|
1208
|
+
channelInfo,
|
|
1209
|
+
kind,
|
|
1210
|
+
chatType,
|
|
1211
|
+
channelName,
|
|
1212
|
+
channelDisplay,
|
|
1213
|
+
roomLabel
|
|
1214
|
+
};
|
|
1215
|
+
if (commandGate.shouldBlock) return {
|
|
1216
|
+
ok: false,
|
|
1217
|
+
denyReason: "unauthorized",
|
|
1218
|
+
commandAuthorized: false,
|
|
1219
|
+
channelInfo,
|
|
1220
|
+
kind,
|
|
1221
|
+
chatType,
|
|
1222
|
+
channelName,
|
|
1223
|
+
channelDisplay,
|
|
1224
|
+
roomLabel
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
ok: true,
|
|
1229
|
+
commandAuthorized,
|
|
1230
|
+
channelInfo,
|
|
1231
|
+
kind,
|
|
1232
|
+
chatType,
|
|
1233
|
+
channelName,
|
|
1234
|
+
channelDisplay,
|
|
1235
|
+
roomLabel
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region extensions/mattermost/src/mattermost/monitor-helpers.ts
|
|
1240
|
+
const formatInboundFromLabel = formatInboundFromLabel$1;
|
|
1241
|
+
function resolveThreadSessionKeys(params) {
|
|
1242
|
+
return resolveThreadSessionKeys$1({
|
|
1243
|
+
...params,
|
|
1244
|
+
normalizeThreadId: (threadId) => threadId
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Strip bot mention from message text while preserving newlines and
|
|
1249
|
+
* block-level Markdown formatting (headings, lists, blockquotes).
|
|
1250
|
+
*/
|
|
1251
|
+
function normalizeMention(text, mention) {
|
|
1252
|
+
if (!mention) return text.trim();
|
|
1253
|
+
const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1254
|
+
const hasMentionRe = new RegExp(`@${escaped}\\b`, "i");
|
|
1255
|
+
const leadingMentionRe = new RegExp(`^([\\t ]*)@${escaped}\\b[\\t ]*`, "i");
|
|
1256
|
+
const trailingMentionRe = new RegExp(`[\\t ]*@${escaped}\\b[\\t ]*$`, "i");
|
|
1257
|
+
const normalizedLines = text.split("\n").map((line) => {
|
|
1258
|
+
const hadMention = hasMentionRe.test(line);
|
|
1259
|
+
const normalizedLine = line.replace(leadingMentionRe, "$1").replace(trailingMentionRe, "").replace(new RegExp(`@${escaped}\\b`, "gi"), "").replace(/(\S)[ \t]{2,}/g, "$1 ");
|
|
1260
|
+
return {
|
|
1261
|
+
text: normalizedLine,
|
|
1262
|
+
mentionOnlyBlank: hadMention && normalizedLine.trim() === ""
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
while (normalizedLines[0]?.mentionOnlyBlank) normalizedLines.shift();
|
|
1266
|
+
while (normalizedLines.at(-1)?.text.trim() === "") normalizedLines.pop();
|
|
1267
|
+
return normalizedLines.map((line) => line.text).join("\n");
|
|
1268
|
+
}
|
|
1269
|
+
//#endregion
|
|
1270
|
+
//#region extensions/mattermost/src/mattermost/monitor-onchar.ts
|
|
1271
|
+
const DEFAULT_ONCHAR_PREFIXES = [">", "!"];
|
|
1272
|
+
function resolveOncharPrefixes(prefixes) {
|
|
1273
|
+
const cleaned = prefixes?.map((entry) => entry.trim()).filter(Boolean) ?? DEFAULT_ONCHAR_PREFIXES;
|
|
1274
|
+
return cleaned.length > 0 ? cleaned : DEFAULT_ONCHAR_PREFIXES;
|
|
1275
|
+
}
|
|
1276
|
+
function stripOncharPrefix(text, prefixes) {
|
|
1277
|
+
const trimmed = text.trimStart();
|
|
1278
|
+
for (const prefix of prefixes) {
|
|
1279
|
+
if (!prefix) continue;
|
|
1280
|
+
if (trimmed.startsWith(prefix)) return {
|
|
1281
|
+
triggered: true,
|
|
1282
|
+
stripped: trimmed.slice(prefix.length).trimStart()
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
triggered: false,
|
|
1287
|
+
stripped: text
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
//#endregion
|
|
1291
|
+
//#region extensions/mattermost/src/mattermost/monitor-websocket.ts
|
|
1292
|
+
var WebSocketClosedBeforeOpenError = class extends Error {
|
|
1293
|
+
constructor(code, reason) {
|
|
1294
|
+
super(`websocket closed before open (code ${code})`);
|
|
1295
|
+
this.code = code;
|
|
1296
|
+
this.reason = reason;
|
|
1297
|
+
this.name = "WebSocketClosedBeforeOpenError";
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
const defaultMattermostWebSocketFactory = (url) => new WebSocket$1(url);
|
|
1301
|
+
function parsePostedPayload(payload) {
|
|
1302
|
+
if (payload.event !== "posted") return null;
|
|
1303
|
+
const postData = payload.data?.post;
|
|
1304
|
+
if (!postData) return null;
|
|
1305
|
+
let post = null;
|
|
1306
|
+
if (typeof postData === "string") try {
|
|
1307
|
+
post = JSON.parse(postData);
|
|
1308
|
+
} catch {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
else if (typeof postData === "object") post = postData;
|
|
1312
|
+
if (!post) return null;
|
|
1313
|
+
return {
|
|
1314
|
+
payload,
|
|
1315
|
+
post
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function createMattermostConnectOnce(opts) {
|
|
1319
|
+
const webSocketFactory = opts.webSocketFactory ?? defaultMattermostWebSocketFactory;
|
|
1320
|
+
return async () => {
|
|
1321
|
+
const ws = webSocketFactory(opts.wsUrl);
|
|
1322
|
+
const onAbort = () => ws.terminate();
|
|
1323
|
+
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1324
|
+
try {
|
|
1325
|
+
return await new Promise((resolve, reject) => {
|
|
1326
|
+
let opened = false;
|
|
1327
|
+
let settled = false;
|
|
1328
|
+
const resolveOnce = () => {
|
|
1329
|
+
if (settled) return;
|
|
1330
|
+
settled = true;
|
|
1331
|
+
resolve();
|
|
1332
|
+
};
|
|
1333
|
+
const rejectOnce = (error) => {
|
|
1334
|
+
if (settled) return;
|
|
1335
|
+
settled = true;
|
|
1336
|
+
reject(error);
|
|
1337
|
+
};
|
|
1338
|
+
ws.on("open", () => {
|
|
1339
|
+
opened = true;
|
|
1340
|
+
opts.statusSink?.({
|
|
1341
|
+
connected: true,
|
|
1342
|
+
lastConnectedAt: Date.now(),
|
|
1343
|
+
lastError: null
|
|
1344
|
+
});
|
|
1345
|
+
ws.send(JSON.stringify({
|
|
1346
|
+
seq: opts.nextSeq(),
|
|
1347
|
+
action: "authentication_challenge",
|
|
1348
|
+
data: { token: opts.botToken }
|
|
1349
|
+
}));
|
|
1350
|
+
});
|
|
1351
|
+
ws.on("message", async (data) => {
|
|
1352
|
+
const raw = rawDataToString(data);
|
|
1353
|
+
let payload;
|
|
1354
|
+
try {
|
|
1355
|
+
payload = JSON.parse(raw);
|
|
1356
|
+
} catch {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (payload.event === "reaction_added" || payload.event === "reaction_removed") {
|
|
1360
|
+
if (!opts.onReaction) return;
|
|
1361
|
+
try {
|
|
1362
|
+
await opts.onReaction(payload);
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
opts.runtime.error?.(`mattermost reaction handler failed: ${String(err)}`);
|
|
1365
|
+
}
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (payload.event !== "posted") return;
|
|
1369
|
+
const parsed = parsePostedPayload(payload);
|
|
1370
|
+
if (!parsed) return;
|
|
1371
|
+
try {
|
|
1372
|
+
await opts.onPosted(parsed.post, parsed.payload);
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
opts.runtime.error?.(`mattermost handler failed: ${String(err)}`);
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
ws.on("close", (code, reason) => {
|
|
1378
|
+
const message = reasonToString(reason);
|
|
1379
|
+
opts.statusSink?.({
|
|
1380
|
+
connected: false,
|
|
1381
|
+
lastDisconnect: {
|
|
1382
|
+
at: Date.now(),
|
|
1383
|
+
status: code,
|
|
1384
|
+
error: message || void 0
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
if (opened) {
|
|
1388
|
+
resolveOnce();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
rejectOnce(new WebSocketClosedBeforeOpenError(code, message || void 0));
|
|
1392
|
+
});
|
|
1393
|
+
ws.on("error", (err) => {
|
|
1394
|
+
opts.runtime.error?.(`mattermost websocket error: ${String(err)}`);
|
|
1395
|
+
opts.statusSink?.({ lastError: String(err) });
|
|
1396
|
+
try {
|
|
1397
|
+
ws.close();
|
|
1398
|
+
} catch {}
|
|
1399
|
+
});
|
|
1400
|
+
});
|
|
1401
|
+
} finally {
|
|
1402
|
+
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function reasonToString(reason) {
|
|
1407
|
+
if (!reason) return "";
|
|
1408
|
+
if (typeof reason === "string") return reason;
|
|
1409
|
+
return reason.length > 0 ? reason.toString("utf8") : "";
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region extensions/mattermost/src/mattermost/reconnect.ts
|
|
1413
|
+
/**
|
|
1414
|
+
* Reconnection loop with exponential backoff.
|
|
1415
|
+
*
|
|
1416
|
+
* Calls `connectFn` in a while loop. On normal resolve (connection closed),
|
|
1417
|
+
* the backoff resets. On thrown error (connection failed), the current delay is
|
|
1418
|
+
* used, then doubled for the next retry.
|
|
1419
|
+
* The loop exits when `abortSignal` fires.
|
|
1420
|
+
*/
|
|
1421
|
+
async function runWithReconnect(connectFn, opts = {}) {
|
|
1422
|
+
const { initialDelayMs = 2e3, maxDelayMs = 6e4 } = opts;
|
|
1423
|
+
const jitterRatio = Math.max(0, opts.jitterRatio ?? 0);
|
|
1424
|
+
const random = opts.random ?? Math.random;
|
|
1425
|
+
let retryDelay = initialDelayMs;
|
|
1426
|
+
let attempt = 0;
|
|
1427
|
+
while (!opts.abortSignal?.aborted) {
|
|
1428
|
+
let shouldIncreaseDelay = false;
|
|
1429
|
+
let outcome = "resolved";
|
|
1430
|
+
let error;
|
|
1431
|
+
try {
|
|
1432
|
+
await connectFn();
|
|
1433
|
+
retryDelay = initialDelayMs;
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
if (opts.abortSignal?.aborted) return;
|
|
1436
|
+
outcome = "rejected";
|
|
1437
|
+
error = err;
|
|
1438
|
+
opts.onError?.(err);
|
|
1439
|
+
shouldIncreaseDelay = true;
|
|
1440
|
+
}
|
|
1441
|
+
if (opts.abortSignal?.aborted) return;
|
|
1442
|
+
const delayMs = withJitter(retryDelay, jitterRatio, random);
|
|
1443
|
+
if (!(opts.shouldReconnect?.({
|
|
1444
|
+
attempt,
|
|
1445
|
+
delayMs,
|
|
1446
|
+
outcome,
|
|
1447
|
+
error
|
|
1448
|
+
}) ?? true)) return;
|
|
1449
|
+
opts.onReconnect?.(delayMs);
|
|
1450
|
+
await sleepAbortable(delayMs, opts.abortSignal);
|
|
1451
|
+
if (shouldIncreaseDelay) retryDelay = Math.min(retryDelay * 2, maxDelayMs);
|
|
1452
|
+
attempt++;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
function withJitter(baseMs, jitterRatio, random) {
|
|
1456
|
+
if (jitterRatio <= 0) return baseMs;
|
|
1457
|
+
const normalized = Math.max(0, Math.min(1, random()));
|
|
1458
|
+
const spread = baseMs * jitterRatio;
|
|
1459
|
+
return Math.max(1, Math.round(baseMs - spread + normalized * spread * 2));
|
|
1460
|
+
}
|
|
1461
|
+
function sleepAbortable(ms, signal) {
|
|
1462
|
+
return new Promise((resolve) => {
|
|
1463
|
+
if (signal?.aborted) {
|
|
1464
|
+
resolve();
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const onAbort = () => {
|
|
1468
|
+
clearTimeout(timer);
|
|
1469
|
+
resolve();
|
|
1470
|
+
};
|
|
1471
|
+
const timer = setTimeout(() => {
|
|
1472
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1473
|
+
resolve();
|
|
1474
|
+
}, ms);
|
|
1475
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
//#endregion
|
|
1479
|
+
//#region extensions/mattermost/src/mattermost/reply-delivery.ts
|
|
1480
|
+
async function deliverMattermostReplyPayload(params) {
|
|
1481
|
+
const mediaUrls = params.payload.mediaUrls ?? (params.payload.mediaUrl ? [params.payload.mediaUrl] : []);
|
|
1482
|
+
const text = params.core.channel.text.convertMarkdownTables(params.payload.text ?? "", params.tableMode);
|
|
1483
|
+
if (mediaUrls.length === 0) {
|
|
1484
|
+
const chunkMode = params.core.channel.text.resolveChunkMode(params.cfg, "mattermost", params.accountId);
|
|
1485
|
+
const chunks = params.core.channel.text.chunkMarkdownTextWithMode(text, params.textLimit, chunkMode);
|
|
1486
|
+
for (const chunk of chunks.length > 0 ? chunks : [text]) {
|
|
1487
|
+
if (!chunk) continue;
|
|
1488
|
+
await params.sendMessage(params.to, chunk, {
|
|
1489
|
+
accountId: params.accountId,
|
|
1490
|
+
replyToId: params.replyToId
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const mediaLocalRoots = getAgentScopedMediaLocalRoots(params.cfg, params.agentId);
|
|
1496
|
+
let first = true;
|
|
1497
|
+
for (const mediaUrl of mediaUrls) {
|
|
1498
|
+
const caption = first ? text : "";
|
|
1499
|
+
first = false;
|
|
1500
|
+
await params.sendMessage(params.to, caption, {
|
|
1501
|
+
accountId: params.accountId,
|
|
1502
|
+
mediaUrl,
|
|
1503
|
+
mediaLocalRoots,
|
|
1504
|
+
replyToId: params.replyToId
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
//#endregion
|
|
1509
|
+
//#region extensions/mattermost/src/mattermost/target-resolution.ts
|
|
1510
|
+
const mattermostOpaqueTargetCache = /* @__PURE__ */ new Map();
|
|
1511
|
+
function cacheKey$1(baseUrl, token, id) {
|
|
1512
|
+
return `${baseUrl}::${token}::${id}`;
|
|
1513
|
+
}
|
|
1514
|
+
/** Mattermost IDs are 26-character lowercase alphanumeric strings. */
|
|
1515
|
+
function isMattermostId(value) {
|
|
1516
|
+
return /^[a-z0-9]{26}$/.test(value);
|
|
1517
|
+
}
|
|
1518
|
+
function isExplicitMattermostTarget(raw) {
|
|
1519
|
+
const trimmed = raw.trim();
|
|
1520
|
+
if (!trimmed) return false;
|
|
1521
|
+
return /^(channel|user|mattermost):/i.test(trimmed) || trimmed.startsWith("@") || trimmed.startsWith("#");
|
|
1522
|
+
}
|
|
1523
|
+
function parseMattermostApiStatus(err) {
|
|
1524
|
+
if (!err || typeof err !== "object") return;
|
|
1525
|
+
const msg = "message" in err ? String(err.message ?? "") : "";
|
|
1526
|
+
const match = /Mattermost API (\d{3})\b/.exec(msg);
|
|
1527
|
+
if (!match) return;
|
|
1528
|
+
const code = Number(match[1]);
|
|
1529
|
+
return Number.isFinite(code) ? code : void 0;
|
|
1530
|
+
}
|
|
1531
|
+
async function resolveMattermostOpaqueTarget(params) {
|
|
1532
|
+
const input = params.input.trim();
|
|
1533
|
+
if (!input || isExplicitMattermostTarget(input) || !isMattermostId(input)) return null;
|
|
1534
|
+
const account = params.cfg && (!params.token || !params.baseUrl) ? resolveMattermostAccount({
|
|
1535
|
+
cfg: params.cfg,
|
|
1536
|
+
accountId: params.accountId
|
|
1537
|
+
}) : null;
|
|
1538
|
+
const token = params.token?.trim() || account?.botToken?.trim();
|
|
1539
|
+
const baseUrl = normalizeMattermostBaseUrl(params.baseUrl ?? account?.baseUrl);
|
|
1540
|
+
if (!token || !baseUrl) return null;
|
|
1541
|
+
const key = cacheKey$1(baseUrl, token, input);
|
|
1542
|
+
const cached = mattermostOpaqueTargetCache.get(key);
|
|
1543
|
+
if (cached === true) return {
|
|
1544
|
+
kind: "user",
|
|
1545
|
+
id: input,
|
|
1546
|
+
to: `user:${input}`
|
|
1547
|
+
};
|
|
1548
|
+
if (cached === false) return {
|
|
1549
|
+
kind: "channel",
|
|
1550
|
+
id: input,
|
|
1551
|
+
to: `channel:${input}`
|
|
1552
|
+
};
|
|
1553
|
+
const client = createMattermostClient({
|
|
1554
|
+
baseUrl,
|
|
1555
|
+
botToken: token
|
|
1556
|
+
});
|
|
1557
|
+
try {
|
|
1558
|
+
await fetchMattermostUser(client, input);
|
|
1559
|
+
mattermostOpaqueTargetCache.set(key, true);
|
|
1560
|
+
return {
|
|
1561
|
+
kind: "user",
|
|
1562
|
+
id: input,
|
|
1563
|
+
to: `user:${input}`
|
|
1564
|
+
};
|
|
1565
|
+
} catch (err) {
|
|
1566
|
+
if (parseMattermostApiStatus(err) === 404) mattermostOpaqueTargetCache.set(key, false);
|
|
1567
|
+
return {
|
|
1568
|
+
kind: "channel",
|
|
1569
|
+
id: input,
|
|
1570
|
+
to: `channel:${input}`
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
//#endregion
|
|
1575
|
+
//#region extensions/mattermost/src/mattermost/send.ts
|
|
1576
|
+
const botUserCache = /* @__PURE__ */ new Map();
|
|
1577
|
+
const userByNameCache = /* @__PURE__ */ new Map();
|
|
1578
|
+
const channelByNameCache = /* @__PURE__ */ new Map();
|
|
1579
|
+
const dmChannelCache = /* @__PURE__ */ new Map();
|
|
1580
|
+
const getCore = () => getMattermostRuntime();
|
|
1581
|
+
function cacheKey(baseUrl, token) {
|
|
1582
|
+
return `${baseUrl}::${token}`;
|
|
1583
|
+
}
|
|
1584
|
+
function normalizeMessage(text, mediaUrl) {
|
|
1585
|
+
return [text.trim(), mediaUrl?.trim()].filter(Boolean).join("\n");
|
|
1586
|
+
}
|
|
1587
|
+
function isHttpUrl(value) {
|
|
1588
|
+
return /^https?:\/\//i.test(value);
|
|
1589
|
+
}
|
|
1590
|
+
function parseMattermostTarget(raw) {
|
|
1591
|
+
const trimmed = raw.trim();
|
|
1592
|
+
if (!trimmed) throw new Error("Recipient is required for Mattermost sends");
|
|
1593
|
+
const lower = trimmed.toLowerCase();
|
|
1594
|
+
if (lower.startsWith("channel:")) {
|
|
1595
|
+
const id = trimmed.slice(8).trim();
|
|
1596
|
+
if (!id) throw new Error("Channel id is required for Mattermost sends");
|
|
1597
|
+
if (id.startsWith("#")) {
|
|
1598
|
+
const name = id.slice(1).trim();
|
|
1599
|
+
if (!name) throw new Error("Channel name is required for Mattermost sends");
|
|
1600
|
+
return {
|
|
1601
|
+
kind: "channel-name",
|
|
1602
|
+
name
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
if (!isMattermostId(id)) return {
|
|
1606
|
+
kind: "channel-name",
|
|
1607
|
+
name: id
|
|
1608
|
+
};
|
|
1609
|
+
return {
|
|
1610
|
+
kind: "channel",
|
|
1611
|
+
id
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
if (lower.startsWith("user:")) {
|
|
1615
|
+
const id = trimmed.slice(5).trim();
|
|
1616
|
+
if (!id) throw new Error("User id is required for Mattermost sends");
|
|
1617
|
+
return {
|
|
1618
|
+
kind: "user",
|
|
1619
|
+
id
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
if (lower.startsWith("mattermost:")) {
|
|
1623
|
+
const id = trimmed.slice(11).trim();
|
|
1624
|
+
if (!id) throw new Error("User id is required for Mattermost sends");
|
|
1625
|
+
return {
|
|
1626
|
+
kind: "user",
|
|
1627
|
+
id
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
if (trimmed.startsWith("@")) {
|
|
1631
|
+
const username = trimmed.slice(1).trim();
|
|
1632
|
+
if (!username) throw new Error("Username is required for Mattermost sends");
|
|
1633
|
+
return {
|
|
1634
|
+
kind: "user",
|
|
1635
|
+
username
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
if (trimmed.startsWith("#")) {
|
|
1639
|
+
const name = trimmed.slice(1).trim();
|
|
1640
|
+
if (!name) throw new Error("Channel name is required for Mattermost sends");
|
|
1641
|
+
return {
|
|
1642
|
+
kind: "channel-name",
|
|
1643
|
+
name
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
if (!isMattermostId(trimmed)) return {
|
|
1647
|
+
kind: "channel-name",
|
|
1648
|
+
name: trimmed
|
|
1649
|
+
};
|
|
1650
|
+
return {
|
|
1651
|
+
kind: "channel",
|
|
1652
|
+
id: trimmed
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
async function resolveBotUser(baseUrl, token) {
|
|
1656
|
+
const key = cacheKey(baseUrl, token);
|
|
1657
|
+
const cached = botUserCache.get(key);
|
|
1658
|
+
if (cached) return cached;
|
|
1659
|
+
const user = await fetchMattermostMe(createMattermostClient({
|
|
1660
|
+
baseUrl,
|
|
1661
|
+
botToken: token
|
|
1662
|
+
}));
|
|
1663
|
+
botUserCache.set(key, user);
|
|
1664
|
+
return user;
|
|
1665
|
+
}
|
|
1666
|
+
async function resolveUserIdByUsername(params) {
|
|
1667
|
+
const { baseUrl, token, username } = params;
|
|
1668
|
+
const key = `${cacheKey(baseUrl, token)}::${username.toLowerCase()}`;
|
|
1669
|
+
const cached = userByNameCache.get(key);
|
|
1670
|
+
if (cached?.id) return cached.id;
|
|
1671
|
+
const user = await fetchMattermostUserByUsername(createMattermostClient({
|
|
1672
|
+
baseUrl,
|
|
1673
|
+
botToken: token
|
|
1674
|
+
}), username);
|
|
1675
|
+
userByNameCache.set(key, user);
|
|
1676
|
+
return user.id;
|
|
1677
|
+
}
|
|
1678
|
+
async function resolveChannelIdByName(params) {
|
|
1679
|
+
const { baseUrl, token, name } = params;
|
|
1680
|
+
const key = `${cacheKey(baseUrl, token)}::channel::${name.toLowerCase()}`;
|
|
1681
|
+
const cached = channelByNameCache.get(key);
|
|
1682
|
+
if (cached) return cached;
|
|
1683
|
+
const client = createMattermostClient({
|
|
1684
|
+
baseUrl,
|
|
1685
|
+
botToken: token
|
|
1686
|
+
});
|
|
1687
|
+
const teams = await fetchMattermostUserTeams(client, (await fetchMattermostMe(client)).id);
|
|
1688
|
+
for (const team of teams) try {
|
|
1689
|
+
const channel = await fetchMattermostChannelByName(client, team.id, name);
|
|
1690
|
+
if (channel?.id) {
|
|
1691
|
+
channelByNameCache.set(key, channel.id);
|
|
1692
|
+
return channel.id;
|
|
1693
|
+
}
|
|
1694
|
+
} catch {}
|
|
1695
|
+
throw new Error(`Mattermost channel "#${name}" not found in any team the bot belongs to`);
|
|
1696
|
+
}
|
|
1697
|
+
async function resolveTargetChannelId(params) {
|
|
1698
|
+
if (params.target.kind === "channel") return params.target.id;
|
|
1699
|
+
if (params.target.kind === "channel-name") return await resolveChannelIdByName({
|
|
1700
|
+
baseUrl: params.baseUrl,
|
|
1701
|
+
token: params.token,
|
|
1702
|
+
name: params.target.name
|
|
1703
|
+
});
|
|
1704
|
+
const userId = params.target.id ? params.target.id : await resolveUserIdByUsername({
|
|
1705
|
+
baseUrl: params.baseUrl,
|
|
1706
|
+
token: params.token,
|
|
1707
|
+
username: params.target.username ?? ""
|
|
1708
|
+
});
|
|
1709
|
+
const dmKey = `${cacheKey(params.baseUrl, params.token)}::dm::${userId}`;
|
|
1710
|
+
const cachedDm = dmChannelCache.get(dmKey);
|
|
1711
|
+
if (cachedDm) return cachedDm;
|
|
1712
|
+
const botUser = await resolveBotUser(params.baseUrl, params.token);
|
|
1713
|
+
const channel = await createMattermostDirectChannel(createMattermostClient({
|
|
1714
|
+
baseUrl: params.baseUrl,
|
|
1715
|
+
botToken: params.token
|
|
1716
|
+
}), [botUser.id, userId]);
|
|
1717
|
+
dmChannelCache.set(dmKey, channel.id);
|
|
1718
|
+
return channel.id;
|
|
1719
|
+
}
|
|
1720
|
+
async function resolveMattermostSendContext(to, opts = {}) {
|
|
1721
|
+
const core = getCore();
|
|
1722
|
+
const cfg = opts.cfg ?? core.config.loadConfig();
|
|
1723
|
+
const account = resolveMattermostAccount({
|
|
1724
|
+
cfg,
|
|
1725
|
+
accountId: opts.accountId
|
|
1726
|
+
});
|
|
1727
|
+
const token = opts.botToken?.trim() || account.botToken?.trim();
|
|
1728
|
+
if (!token) throw new Error(`Mattermost bot token missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.botToken or MATTERMOST_BOT_TOKEN for default).`);
|
|
1729
|
+
const baseUrl = normalizeMattermostBaseUrl(opts.baseUrl ?? account.baseUrl);
|
|
1730
|
+
if (!baseUrl) throw new Error(`Mattermost baseUrl missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.baseUrl or MATTERMOST_URL for default).`);
|
|
1731
|
+
const trimmedTo = to?.trim() ?? "";
|
|
1732
|
+
const opaqueTarget = await resolveMattermostOpaqueTarget({
|
|
1733
|
+
input: trimmedTo,
|
|
1734
|
+
token,
|
|
1735
|
+
baseUrl
|
|
1736
|
+
});
|
|
1737
|
+
const channelId = await resolveTargetChannelId({
|
|
1738
|
+
target: opaqueTarget?.kind === "user" ? {
|
|
1739
|
+
kind: "user",
|
|
1740
|
+
id: opaqueTarget.id
|
|
1741
|
+
} : opaqueTarget?.kind === "channel" ? {
|
|
1742
|
+
kind: "channel",
|
|
1743
|
+
id: opaqueTarget.id
|
|
1744
|
+
} : parseMattermostTarget(trimmedTo),
|
|
1745
|
+
baseUrl,
|
|
1746
|
+
token
|
|
1747
|
+
});
|
|
1748
|
+
return {
|
|
1749
|
+
cfg,
|
|
1750
|
+
accountId: account.accountId,
|
|
1751
|
+
token,
|
|
1752
|
+
baseUrl,
|
|
1753
|
+
channelId
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
async function sendMessageMattermost(to, text, opts = {}) {
|
|
1757
|
+
const core = getCore();
|
|
1758
|
+
const logger = core.logging.getChildLogger({ module: "mattermost" });
|
|
1759
|
+
const { cfg, accountId, token, baseUrl, channelId } = await resolveMattermostSendContext(to, opts);
|
|
1760
|
+
const client = createMattermostClient({
|
|
1761
|
+
baseUrl,
|
|
1762
|
+
botToken: token
|
|
1763
|
+
});
|
|
1764
|
+
let props = opts.props;
|
|
1765
|
+
if (!props && Array.isArray(opts.buttons) && opts.buttons.length > 0) {
|
|
1766
|
+
setInteractionSecret(accountId, token);
|
|
1767
|
+
props = buildButtonProps({
|
|
1768
|
+
callbackUrl: resolveInteractionCallbackUrl(accountId, {
|
|
1769
|
+
gateway: cfg.gateway,
|
|
1770
|
+
interactions: resolveMattermostAccount({
|
|
1771
|
+
cfg,
|
|
1772
|
+
accountId
|
|
1773
|
+
}).config?.interactions
|
|
1774
|
+
}),
|
|
1775
|
+
accountId,
|
|
1776
|
+
channelId,
|
|
1777
|
+
buttons: opts.buttons,
|
|
1778
|
+
text: opts.attachmentText
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
let message = text?.trim() ?? "";
|
|
1782
|
+
let fileIds;
|
|
1783
|
+
let uploadError;
|
|
1784
|
+
const mediaUrl = opts.mediaUrl?.trim();
|
|
1785
|
+
if (mediaUrl) try {
|
|
1786
|
+
const media = await loadOutboundMediaFromUrl(mediaUrl, { mediaLocalRoots: opts.mediaLocalRoots });
|
|
1787
|
+
fileIds = [(await uploadMattermostFile(client, {
|
|
1788
|
+
channelId,
|
|
1789
|
+
buffer: media.buffer,
|
|
1790
|
+
fileName: media.fileName ?? "upload",
|
|
1791
|
+
contentType: media.contentType ?? void 0
|
|
1792
|
+
})).id];
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
uploadError = err instanceof Error ? err : new Error(String(err));
|
|
1795
|
+
if (core.logging.shouldLogVerbose()) logger.debug?.(`mattermost send: media upload failed, falling back to URL text: ${String(err)}`);
|
|
1796
|
+
message = normalizeMessage(message, isHttpUrl(mediaUrl) ? mediaUrl : "");
|
|
1797
|
+
}
|
|
1798
|
+
if (message) {
|
|
1799
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
1800
|
+
cfg,
|
|
1801
|
+
channel: "mattermost",
|
|
1802
|
+
accountId
|
|
1803
|
+
});
|
|
1804
|
+
message = core.channel.text.convertMarkdownTables(message, tableMode);
|
|
1805
|
+
}
|
|
1806
|
+
if (!message && (!fileIds || fileIds.length === 0)) {
|
|
1807
|
+
if (uploadError) throw new Error(`Mattermost media upload failed: ${uploadError.message}`);
|
|
1808
|
+
throw new Error("Mattermost message is empty");
|
|
1809
|
+
}
|
|
1810
|
+
const post = await createMattermostPost(client, {
|
|
1811
|
+
channelId,
|
|
1812
|
+
message,
|
|
1813
|
+
rootId: opts.replyToId,
|
|
1814
|
+
fileIds,
|
|
1815
|
+
props
|
|
1816
|
+
});
|
|
1817
|
+
core.channel.activity.record({
|
|
1818
|
+
channel: "mattermost",
|
|
1819
|
+
accountId,
|
|
1820
|
+
direction: "outbound"
|
|
1821
|
+
});
|
|
1822
|
+
return {
|
|
1823
|
+
messageId: post.id ?? "unknown",
|
|
1824
|
+
channelId
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region extensions/mattermost/src/mattermost/slash-commands.ts
|
|
1829
|
+
/**
|
|
1830
|
+
* Built-in MoldClaw commands to register as native slash commands.
|
|
1831
|
+
* These mirror the text-based commands already handled by the gateway.
|
|
1832
|
+
*/
|
|
1833
|
+
const DEFAULT_COMMAND_SPECS = [
|
|
1834
|
+
{
|
|
1835
|
+
trigger: "oc_status",
|
|
1836
|
+
originalName: "status",
|
|
1837
|
+
description: "Show session status (model, usage, uptime)",
|
|
1838
|
+
autoComplete: true
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
trigger: "oc_model",
|
|
1842
|
+
originalName: "model",
|
|
1843
|
+
description: "View or change the current model",
|
|
1844
|
+
autoComplete: true,
|
|
1845
|
+
autoCompleteHint: "[model-name]"
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
trigger: "oc_models",
|
|
1849
|
+
originalName: "models",
|
|
1850
|
+
description: "Browse available models",
|
|
1851
|
+
autoComplete: true,
|
|
1852
|
+
autoCompleteHint: "[provider]"
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
trigger: "oc_new",
|
|
1856
|
+
originalName: "new",
|
|
1857
|
+
description: "Start a new conversation session",
|
|
1858
|
+
autoComplete: true
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
trigger: "oc_help",
|
|
1862
|
+
originalName: "help",
|
|
1863
|
+
description: "Show available commands",
|
|
1864
|
+
autoComplete: true
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
trigger: "oc_think",
|
|
1868
|
+
originalName: "think",
|
|
1869
|
+
description: "Set thinking/reasoning level",
|
|
1870
|
+
autoComplete: true,
|
|
1871
|
+
autoCompleteHint: "[off|low|medium|high]"
|
|
1872
|
+
},
|
|
1873
|
+
{
|
|
1874
|
+
trigger: "oc_reasoning",
|
|
1875
|
+
originalName: "reasoning",
|
|
1876
|
+
description: "Toggle reasoning mode",
|
|
1877
|
+
autoComplete: true,
|
|
1878
|
+
autoCompleteHint: "[on|off]"
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
trigger: "oc_verbose",
|
|
1882
|
+
originalName: "verbose",
|
|
1883
|
+
description: "Toggle verbose mode",
|
|
1884
|
+
autoComplete: true,
|
|
1885
|
+
autoCompleteHint: "[on|off]"
|
|
1886
|
+
}
|
|
1887
|
+
];
|
|
1888
|
+
/**
|
|
1889
|
+
* List existing custom slash commands for a team.
|
|
1890
|
+
*/
|
|
1891
|
+
async function listMattermostCommands(client, teamId) {
|
|
1892
|
+
return await client.request(`/commands?team_id=${encodeURIComponent(teamId)}&custom_only=true`);
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Create a custom slash command on a Mattermost team.
|
|
1896
|
+
*/
|
|
1897
|
+
async function createMattermostCommand(client, params) {
|
|
1898
|
+
return await client.request("/commands", {
|
|
1899
|
+
method: "POST",
|
|
1900
|
+
body: JSON.stringify(params)
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Delete a custom slash command.
|
|
1905
|
+
*/
|
|
1906
|
+
async function deleteMattermostCommand(client, commandId) {
|
|
1907
|
+
await client.request(`/commands/${encodeURIComponent(commandId)}`, { method: "DELETE" });
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Update an existing custom slash command.
|
|
1911
|
+
*/
|
|
1912
|
+
async function updateMattermostCommand(client, params) {
|
|
1913
|
+
return await client.request(`/commands/${encodeURIComponent(params.id)}`, {
|
|
1914
|
+
method: "PUT",
|
|
1915
|
+
body: JSON.stringify(params)
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Register all MoldClaw slash commands for a given team.
|
|
1920
|
+
* Skips commands that are already registered with the same trigger + callback URL.
|
|
1921
|
+
* Returns the list of newly created command IDs.
|
|
1922
|
+
*/
|
|
1923
|
+
async function registerSlashCommands(params) {
|
|
1924
|
+
const { client, teamId, creatorUserId, callbackUrl, commands, log } = params;
|
|
1925
|
+
const normalizedCreatorUserId = creatorUserId.trim();
|
|
1926
|
+
if (!normalizedCreatorUserId) throw new Error("creatorUserId is required for slash command reconciliation");
|
|
1927
|
+
let existing = [];
|
|
1928
|
+
try {
|
|
1929
|
+
existing = await listMattermostCommands(client, teamId);
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
log?.(`mattermost: failed to list existing commands: ${String(err)}`);
|
|
1932
|
+
throw err;
|
|
1933
|
+
}
|
|
1934
|
+
const existingByTrigger = /* @__PURE__ */ new Map();
|
|
1935
|
+
for (const cmd of existing) {
|
|
1936
|
+
const list = existingByTrigger.get(cmd.trigger) ?? [];
|
|
1937
|
+
list.push(cmd);
|
|
1938
|
+
existingByTrigger.set(cmd.trigger, list);
|
|
1939
|
+
}
|
|
1940
|
+
const registered = [];
|
|
1941
|
+
for (const spec of commands) {
|
|
1942
|
+
const existingForTrigger = existingByTrigger.get(spec.trigger) ?? [];
|
|
1943
|
+
const ownedCommands = existingForTrigger.filter((cmd) => cmd.creator_id?.trim() === normalizedCreatorUserId);
|
|
1944
|
+
const foreignCommands = existingForTrigger.filter((cmd) => cmd.creator_id?.trim() !== normalizedCreatorUserId);
|
|
1945
|
+
if (ownedCommands.length === 0 && foreignCommands.length > 0) {
|
|
1946
|
+
log?.(`mattermost: trigger /${spec.trigger} already used by non-MoldClaw command(s); skipping to avoid mutating external integrations`);
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
if (ownedCommands.length > 1) log?.(`mattermost: multiple owned commands found for /${spec.trigger}; using the first and leaving extras untouched`);
|
|
1950
|
+
const existingCmd = ownedCommands[0];
|
|
1951
|
+
if (existingCmd && existingCmd.url === callbackUrl) {
|
|
1952
|
+
log?.(`mattermost: command /${spec.trigger} already registered (id=${existingCmd.id})`);
|
|
1953
|
+
registered.push({
|
|
1954
|
+
id: existingCmd.id,
|
|
1955
|
+
trigger: spec.trigger,
|
|
1956
|
+
teamId,
|
|
1957
|
+
token: existingCmd.token,
|
|
1958
|
+
managed: false
|
|
1959
|
+
});
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
if (existingCmd && existingCmd.url !== callbackUrl) {
|
|
1963
|
+
log?.(`mattermost: command /${spec.trigger} exists with different callback URL; updating (id=${existingCmd.id})`);
|
|
1964
|
+
try {
|
|
1965
|
+
const updated = await updateMattermostCommand(client, {
|
|
1966
|
+
id: existingCmd.id,
|
|
1967
|
+
team_id: teamId,
|
|
1968
|
+
trigger: spec.trigger,
|
|
1969
|
+
method: "P",
|
|
1970
|
+
url: callbackUrl,
|
|
1971
|
+
description: spec.description,
|
|
1972
|
+
auto_complete: spec.autoComplete,
|
|
1973
|
+
auto_complete_desc: spec.description,
|
|
1974
|
+
auto_complete_hint: spec.autoCompleteHint
|
|
1975
|
+
});
|
|
1976
|
+
registered.push({
|
|
1977
|
+
id: updated.id,
|
|
1978
|
+
trigger: spec.trigger,
|
|
1979
|
+
teamId,
|
|
1980
|
+
token: updated.token,
|
|
1981
|
+
managed: false
|
|
1982
|
+
});
|
|
1983
|
+
continue;
|
|
1984
|
+
} catch (err) {
|
|
1985
|
+
log?.(`mattermost: failed to update command /${spec.trigger} (id=${existingCmd.id}): ${String(err)}`);
|
|
1986
|
+
try {
|
|
1987
|
+
await deleteMattermostCommand(client, existingCmd.id);
|
|
1988
|
+
log?.(`mattermost: deleted stale command /${spec.trigger} (id=${existingCmd.id})`);
|
|
1989
|
+
} catch (deleteErr) {
|
|
1990
|
+
log?.(`mattermost: failed to delete stale command /${spec.trigger} (id=${existingCmd.id}): ${String(deleteErr)}`);
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
try {
|
|
1996
|
+
const created = await createMattermostCommand(client, {
|
|
1997
|
+
team_id: teamId,
|
|
1998
|
+
trigger: spec.trigger,
|
|
1999
|
+
method: "P",
|
|
2000
|
+
url: callbackUrl,
|
|
2001
|
+
description: spec.description,
|
|
2002
|
+
auto_complete: spec.autoComplete,
|
|
2003
|
+
auto_complete_desc: spec.description,
|
|
2004
|
+
auto_complete_hint: spec.autoCompleteHint
|
|
2005
|
+
});
|
|
2006
|
+
log?.(`mattermost: registered command /${spec.trigger} (id=${created.id})`);
|
|
2007
|
+
registered.push({
|
|
2008
|
+
id: created.id,
|
|
2009
|
+
trigger: spec.trigger,
|
|
2010
|
+
teamId,
|
|
2011
|
+
token: created.token,
|
|
2012
|
+
managed: true
|
|
2013
|
+
});
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
log?.(`mattermost: failed to register command /${spec.trigger}: ${String(err)}`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return registered;
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Clean up all registered slash commands.
|
|
2022
|
+
*/
|
|
2023
|
+
async function cleanupSlashCommands(params) {
|
|
2024
|
+
const { client, commands, log } = params;
|
|
2025
|
+
for (const cmd of commands) {
|
|
2026
|
+
if (!cmd.managed) continue;
|
|
2027
|
+
try {
|
|
2028
|
+
await deleteMattermostCommand(client, cmd.id);
|
|
2029
|
+
log?.(`mattermost: deleted command /${cmd.trigger} (id=${cmd.id})`);
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
log?.(`mattermost: failed to delete command /${cmd.trigger}: ${String(err)}`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Parse a Mattermost slash command callback payload from a URL-encoded or JSON body.
|
|
2037
|
+
*/
|
|
2038
|
+
function parseSlashCommandPayload(body, contentType) {
|
|
2039
|
+
if (!body) return null;
|
|
2040
|
+
try {
|
|
2041
|
+
if (contentType?.includes("application/json")) {
|
|
2042
|
+
const parsed = JSON.parse(body);
|
|
2043
|
+
const token = typeof parsed.token === "string" ? parsed.token : "";
|
|
2044
|
+
const teamId = typeof parsed.team_id === "string" ? parsed.team_id : "";
|
|
2045
|
+
const channelId = typeof parsed.channel_id === "string" ? parsed.channel_id : "";
|
|
2046
|
+
const userId = typeof parsed.user_id === "string" ? parsed.user_id : "";
|
|
2047
|
+
const command = typeof parsed.command === "string" ? parsed.command : "";
|
|
2048
|
+
if (!token || !teamId || !channelId || !userId || !command) return null;
|
|
2049
|
+
return {
|
|
2050
|
+
token,
|
|
2051
|
+
team_id: teamId,
|
|
2052
|
+
team_domain: typeof parsed.team_domain === "string" ? parsed.team_domain : void 0,
|
|
2053
|
+
channel_id: channelId,
|
|
2054
|
+
channel_name: typeof parsed.channel_name === "string" ? parsed.channel_name : void 0,
|
|
2055
|
+
user_id: userId,
|
|
2056
|
+
user_name: typeof parsed.user_name === "string" ? parsed.user_name : void 0,
|
|
2057
|
+
command,
|
|
2058
|
+
text: typeof parsed.text === "string" ? parsed.text : "",
|
|
2059
|
+
trigger_id: typeof parsed.trigger_id === "string" ? parsed.trigger_id : void 0,
|
|
2060
|
+
response_url: typeof parsed.response_url === "string" ? parsed.response_url : void 0
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
const params = new URLSearchParams(body);
|
|
2064
|
+
const token = params.get("token");
|
|
2065
|
+
const teamId = params.get("team_id");
|
|
2066
|
+
const channelId = params.get("channel_id");
|
|
2067
|
+
const userId = params.get("user_id");
|
|
2068
|
+
const command = params.get("command");
|
|
2069
|
+
if (!token || !teamId || !channelId || !userId || !command) return null;
|
|
2070
|
+
return {
|
|
2071
|
+
token,
|
|
2072
|
+
team_id: teamId,
|
|
2073
|
+
team_domain: params.get("team_domain") ?? void 0,
|
|
2074
|
+
channel_id: channelId,
|
|
2075
|
+
channel_name: params.get("channel_name") ?? void 0,
|
|
2076
|
+
user_id: userId,
|
|
2077
|
+
user_name: params.get("user_name") ?? void 0,
|
|
2078
|
+
command,
|
|
2079
|
+
text: params.get("text") ?? "",
|
|
2080
|
+
trigger_id: params.get("trigger_id") ?? void 0,
|
|
2081
|
+
response_url: params.get("response_url") ?? void 0
|
|
2082
|
+
};
|
|
2083
|
+
} catch {
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Map the trigger word back to the original MoldClaw command name.
|
|
2089
|
+
* e.g. "oc_status" -> "/status", "oc_model" -> "/model"
|
|
2090
|
+
*/
|
|
2091
|
+
function resolveCommandText(trigger, text, triggerMap) {
|
|
2092
|
+
const commandName = triggerMap?.get(trigger) ?? (trigger.startsWith("oc_") ? trigger.slice(3) : trigger);
|
|
2093
|
+
const args = text.trim();
|
|
2094
|
+
return args ? `/${commandName} ${args}` : `/${commandName}`;
|
|
2095
|
+
}
|
|
2096
|
+
const DEFAULT_CALLBACK_PATH = "/api/channels/mattermost/command";
|
|
2097
|
+
/**
|
|
2098
|
+
* Ensure the callback path starts with a leading `/` to prevent
|
|
2099
|
+
* malformed URLs like `http://host:portapi/...`.
|
|
2100
|
+
*/
|
|
2101
|
+
function normalizeCallbackPath(path) {
|
|
2102
|
+
const trimmed = path.trim();
|
|
2103
|
+
if (!trimmed) return DEFAULT_CALLBACK_PATH;
|
|
2104
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
2105
|
+
}
|
|
2106
|
+
function resolveSlashCommandConfig(raw) {
|
|
2107
|
+
return {
|
|
2108
|
+
native: raw?.native ?? "auto",
|
|
2109
|
+
nativeSkills: raw?.nativeSkills ?? "auto",
|
|
2110
|
+
callbackPath: normalizeCallbackPath(raw?.callbackPath ?? DEFAULT_CALLBACK_PATH),
|
|
2111
|
+
callbackUrl: raw?.callbackUrl?.trim() || void 0
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
function isSlashCommandsEnabled(config) {
|
|
2115
|
+
if (config.native === true) return true;
|
|
2116
|
+
if (config.native === false) return false;
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Build the callback URL that Mattermost will POST to when a command is invoked.
|
|
2121
|
+
*/
|
|
2122
|
+
function resolveCallbackUrl(params) {
|
|
2123
|
+
if (params.config.callbackUrl) return params.config.callbackUrl;
|
|
2124
|
+
const isWildcardBindHost = (rawHost) => {
|
|
2125
|
+
const trimmed = rawHost.trim();
|
|
2126
|
+
if (!trimmed) return false;
|
|
2127
|
+
const host = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
2128
|
+
return host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0" || host === "::0";
|
|
2129
|
+
};
|
|
2130
|
+
let host = params.gatewayHost && !isWildcardBindHost(params.gatewayHost) ? params.gatewayHost : "localhost";
|
|
2131
|
+
const path = normalizeCallbackPath(params.config.callbackPath);
|
|
2132
|
+
if (host.includes(":") && !(host.startsWith("[") && host.endsWith("]"))) host = `[${host}]`;
|
|
2133
|
+
return `http://${host}:${params.gatewayPort}${path}`;
|
|
2134
|
+
}
|
|
2135
|
+
//#endregion
|
|
2136
|
+
//#region extensions/mattermost/src/mattermost/slash-http.ts
|
|
2137
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
2138
|
+
const BODY_READ_TIMEOUT_MS = 5e3;
|
|
2139
|
+
/**
|
|
2140
|
+
* Read the full request body as a string.
|
|
2141
|
+
*/
|
|
2142
|
+
function readBody(req, maxBytes) {
|
|
2143
|
+
return readRequestBodyWithLimit(req, {
|
|
2144
|
+
maxBytes,
|
|
2145
|
+
timeoutMs: BODY_READ_TIMEOUT_MS
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
function sendJsonResponse(res, status, body) {
|
|
2149
|
+
res.statusCode = status;
|
|
2150
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2151
|
+
res.end(JSON.stringify(body));
|
|
2152
|
+
}
|
|
2153
|
+
async function authorizeSlashInvocation(params) {
|
|
2154
|
+
const { account, cfg, client, commandText, channelId, senderId, senderName, log } = params;
|
|
2155
|
+
const core = getMattermostRuntime();
|
|
2156
|
+
let channelInfo = null;
|
|
2157
|
+
try {
|
|
2158
|
+
channelInfo = await fetchMattermostChannel(client, channelId);
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
log?.(`mattermost: slash channel lookup failed for ${channelId}: ${String(err)}`);
|
|
2161
|
+
}
|
|
2162
|
+
if (!channelInfo) return {
|
|
2163
|
+
ok: false,
|
|
2164
|
+
denyResponse: {
|
|
2165
|
+
response_type: "ephemeral",
|
|
2166
|
+
text: "Temporary error: unable to determine channel type. Please try again."
|
|
2167
|
+
},
|
|
2168
|
+
commandAuthorized: false,
|
|
2169
|
+
channelInfo: null,
|
|
2170
|
+
kind: "channel",
|
|
2171
|
+
chatType: "channel",
|
|
2172
|
+
channelName: "",
|
|
2173
|
+
channelDisplay: "",
|
|
2174
|
+
roomLabel: `#${channelId}`
|
|
2175
|
+
};
|
|
2176
|
+
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
2177
|
+
cfg,
|
|
2178
|
+
surface: "mattermost"
|
|
2179
|
+
});
|
|
2180
|
+
const hasControlCommand = core.channel.text.hasControlCommand(commandText, cfg);
|
|
2181
|
+
const storeAllowFrom = normalizeMattermostAllowList(await core.channel.pairing.readAllowFromStore({
|
|
2182
|
+
channel: "mattermost",
|
|
2183
|
+
accountId: account.accountId
|
|
2184
|
+
}).catch(() => []));
|
|
2185
|
+
const decision = authorizeMattermostCommandInvocation({
|
|
2186
|
+
account,
|
|
2187
|
+
cfg,
|
|
2188
|
+
senderId,
|
|
2189
|
+
senderName,
|
|
2190
|
+
channelId,
|
|
2191
|
+
channelInfo,
|
|
2192
|
+
storeAllowFrom,
|
|
2193
|
+
allowTextCommands,
|
|
2194
|
+
hasControlCommand
|
|
2195
|
+
});
|
|
2196
|
+
if (!decision.ok) {
|
|
2197
|
+
if (decision.denyReason === "dm-pairing") {
|
|
2198
|
+
const { code } = await core.channel.pairing.upsertPairingRequest({
|
|
2199
|
+
channel: "mattermost",
|
|
2200
|
+
accountId: account.accountId,
|
|
2201
|
+
id: senderId,
|
|
2202
|
+
meta: { name: senderName }
|
|
2203
|
+
});
|
|
2204
|
+
return {
|
|
2205
|
+
...decision,
|
|
2206
|
+
denyResponse: {
|
|
2207
|
+
response_type: "ephemeral",
|
|
2208
|
+
text: core.channel.pairing.buildPairingReply({
|
|
2209
|
+
channel: "mattermost",
|
|
2210
|
+
idLine: `Your Mattermost user id: ${senderId}`,
|
|
2211
|
+
code
|
|
2212
|
+
})
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const denyText = decision.denyReason === "unknown-channel" ? "Temporary error: unable to determine channel type. Please try again." : decision.denyReason === "dm-disabled" ? "This bot is not accepting direct messages." : decision.denyReason === "channels-disabled" ? "Slash commands are disabled in channels." : decision.denyReason === "channel-no-allowlist" ? "Slash commands are not configured for this channel (no allowlist)." : "Unauthorized.";
|
|
2217
|
+
return {
|
|
2218
|
+
...decision,
|
|
2219
|
+
denyResponse: {
|
|
2220
|
+
response_type: "ephemeral",
|
|
2221
|
+
text: denyText
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
return {
|
|
2226
|
+
...decision,
|
|
2227
|
+
denyResponse: void 0
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Create the HTTP request handler for Mattermost slash command callbacks.
|
|
2232
|
+
*
|
|
2233
|
+
* This handler is registered as a plugin HTTP route and receives POSTs
|
|
2234
|
+
* from the Mattermost server when a user invokes a registered slash command.
|
|
2235
|
+
*/
|
|
2236
|
+
function createSlashCommandHttpHandler(params) {
|
|
2237
|
+
const { account, cfg, runtime, commandTokens, triggerMap, log } = params;
|
|
2238
|
+
return async (req, res) => {
|
|
2239
|
+
if (req.method !== "POST") {
|
|
2240
|
+
res.statusCode = 405;
|
|
2241
|
+
res.setHeader("Allow", "POST");
|
|
2242
|
+
res.end("Method Not Allowed");
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
let body;
|
|
2246
|
+
try {
|
|
2247
|
+
body = await readBody(req, MAX_BODY_BYTES);
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
if (isRequestBodyLimitError(error, "REQUEST_BODY_TIMEOUT")) {
|
|
2250
|
+
res.statusCode = 408;
|
|
2251
|
+
res.end("Request body timeout");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
res.statusCode = 413;
|
|
2255
|
+
res.end("Payload Too Large");
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
2259
|
+
const payload = parseSlashCommandPayload(body, contentType);
|
|
2260
|
+
if (!payload) {
|
|
2261
|
+
sendJsonResponse(res, 400, {
|
|
2262
|
+
response_type: "ephemeral",
|
|
2263
|
+
text: "Invalid slash command payload."
|
|
2264
|
+
});
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (commandTokens.size === 0 || !commandTokens.has(payload.token)) {
|
|
2268
|
+
sendJsonResponse(res, 401, {
|
|
2269
|
+
response_type: "ephemeral",
|
|
2270
|
+
text: "Unauthorized: invalid command token."
|
|
2271
|
+
});
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const trigger = payload.command.replace(/^\//, "").trim();
|
|
2275
|
+
const commandText = resolveCommandText(trigger, payload.text, triggerMap);
|
|
2276
|
+
const channelId = payload.channel_id;
|
|
2277
|
+
const senderId = payload.user_id;
|
|
2278
|
+
const senderName = payload.user_name ?? senderId;
|
|
2279
|
+
const client = createMattermostClient({
|
|
2280
|
+
baseUrl: account.baseUrl ?? "",
|
|
2281
|
+
botToken: account.botToken ?? ""
|
|
2282
|
+
});
|
|
2283
|
+
const auth = await authorizeSlashInvocation({
|
|
2284
|
+
account,
|
|
2285
|
+
cfg,
|
|
2286
|
+
client,
|
|
2287
|
+
commandText,
|
|
2288
|
+
channelId,
|
|
2289
|
+
senderId,
|
|
2290
|
+
senderName,
|
|
2291
|
+
log
|
|
2292
|
+
});
|
|
2293
|
+
if (!auth.ok) {
|
|
2294
|
+
sendJsonResponse(res, 200, auth.denyResponse ?? {
|
|
2295
|
+
response_type: "ephemeral",
|
|
2296
|
+
text: "Unauthorized."
|
|
2297
|
+
});
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
log?.(`mattermost: slash command /${trigger} from ${senderName} in ${channelId}`);
|
|
2301
|
+
sendJsonResponse(res, 200, {
|
|
2302
|
+
response_type: "ephemeral",
|
|
2303
|
+
text: "Processing..."
|
|
2304
|
+
});
|
|
2305
|
+
try {
|
|
2306
|
+
await handleSlashCommandAsync({
|
|
2307
|
+
account,
|
|
2308
|
+
cfg,
|
|
2309
|
+
runtime,
|
|
2310
|
+
client,
|
|
2311
|
+
commandText,
|
|
2312
|
+
channelId,
|
|
2313
|
+
senderId,
|
|
2314
|
+
senderName,
|
|
2315
|
+
teamId: payload.team_id,
|
|
2316
|
+
triggerId: payload.trigger_id,
|
|
2317
|
+
kind: auth.kind,
|
|
2318
|
+
chatType: auth.chatType,
|
|
2319
|
+
channelName: auth.channelName,
|
|
2320
|
+
channelDisplay: auth.channelDisplay,
|
|
2321
|
+
roomLabel: auth.roomLabel,
|
|
2322
|
+
commandAuthorized: auth.commandAuthorized,
|
|
2323
|
+
log
|
|
2324
|
+
});
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
log?.(`mattermost: slash command handler error: ${String(err)}`);
|
|
2327
|
+
try {
|
|
2328
|
+
await sendMessageMattermost(`channel:${channelId}`, "Sorry, something went wrong processing that command.", { accountId: account.accountId });
|
|
2329
|
+
} catch {}
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
async function handleSlashCommandAsync(params) {
|
|
2334
|
+
const { account, cfg, runtime, client, commandText, channelId, senderId, senderName, teamId, kind, chatType, channelName, channelDisplay, roomLabel, commandAuthorized, triggerId, log } = params;
|
|
2335
|
+
const core = getMattermostRuntime();
|
|
2336
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
2337
|
+
cfg,
|
|
2338
|
+
channel: "mattermost",
|
|
2339
|
+
accountId: account.accountId,
|
|
2340
|
+
teamId,
|
|
2341
|
+
peer: {
|
|
2342
|
+
kind,
|
|
2343
|
+
id: kind === "direct" ? senderId : channelId
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
const fromLabel = kind === "direct" ? `Mattermost DM from ${senderName}` : `Mattermost message in ${roomLabel} from ${senderName}`;
|
|
2347
|
+
const to = kind === "direct" ? `user:${senderId}` : `channel:${channelId}`;
|
|
2348
|
+
const pickerEntry = resolveMattermostModelPickerEntry(commandText);
|
|
2349
|
+
if (pickerEntry) {
|
|
2350
|
+
const data = await buildModelsProviderData(cfg, route.agentId);
|
|
2351
|
+
if (data.providers.length === 0) {
|
|
2352
|
+
await sendMessageMattermost(to, "No models available.", { accountId: account.accountId });
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
const currentModel = resolveMattermostModelPickerCurrentModel({
|
|
2356
|
+
cfg,
|
|
2357
|
+
route,
|
|
2358
|
+
data
|
|
2359
|
+
});
|
|
2360
|
+
const view = pickerEntry.kind === "summary" ? renderMattermostModelSummaryView({
|
|
2361
|
+
ownerUserId: senderId,
|
|
2362
|
+
currentModel
|
|
2363
|
+
}) : pickerEntry.kind === "providers" ? renderMattermostProviderPickerView({
|
|
2364
|
+
ownerUserId: senderId,
|
|
2365
|
+
data,
|
|
2366
|
+
currentModel
|
|
2367
|
+
}) : renderMattermostModelsPickerView({
|
|
2368
|
+
ownerUserId: senderId,
|
|
2369
|
+
data,
|
|
2370
|
+
provider: pickerEntry.provider,
|
|
2371
|
+
page: 1,
|
|
2372
|
+
currentModel
|
|
2373
|
+
});
|
|
2374
|
+
await sendMessageMattermost(to, view.text, {
|
|
2375
|
+
accountId: account.accountId,
|
|
2376
|
+
buttons: view.buttons
|
|
2377
|
+
});
|
|
2378
|
+
runtime.log?.(`delivered model picker to ${to}`);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
2382
|
+
Body: commandText,
|
|
2383
|
+
BodyForAgent: commandText,
|
|
2384
|
+
RawBody: commandText,
|
|
2385
|
+
CommandBody: commandText,
|
|
2386
|
+
From: kind === "direct" ? `mattermost:${senderId}` : kind === "group" ? `mattermost:group:${channelId}` : `mattermost:channel:${channelId}`,
|
|
2387
|
+
To: to,
|
|
2388
|
+
SessionKey: route.sessionKey,
|
|
2389
|
+
AccountId: route.accountId,
|
|
2390
|
+
ChatType: chatType,
|
|
2391
|
+
ConversationLabel: fromLabel,
|
|
2392
|
+
GroupSubject: kind !== "direct" ? channelDisplay || roomLabel : void 0,
|
|
2393
|
+
SenderName: senderName,
|
|
2394
|
+
SenderId: senderId,
|
|
2395
|
+
Provider: "mattermost",
|
|
2396
|
+
Surface: "mattermost",
|
|
2397
|
+
MessageSid: triggerId ?? `slash-${Date.now()}`,
|
|
2398
|
+
Timestamp: Date.now(),
|
|
2399
|
+
WasMentioned: true,
|
|
2400
|
+
CommandAuthorized: commandAuthorized,
|
|
2401
|
+
CommandSource: "native",
|
|
2402
|
+
OriginatingChannel: "mattermost",
|
|
2403
|
+
OriginatingTo: to
|
|
2404
|
+
});
|
|
2405
|
+
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
|
|
2406
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
2407
|
+
cfg,
|
|
2408
|
+
channel: "mattermost",
|
|
2409
|
+
accountId: account.accountId
|
|
2410
|
+
});
|
|
2411
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
2412
|
+
cfg,
|
|
2413
|
+
agentId: route.agentId,
|
|
2414
|
+
channel: "mattermost",
|
|
2415
|
+
accountId: account.accountId
|
|
2416
|
+
});
|
|
2417
|
+
const humanDelay = core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId);
|
|
2418
|
+
const typingCallbacks = createTypingCallbacks({
|
|
2419
|
+
start: () => sendMattermostTyping(client, { channelId }),
|
|
2420
|
+
onStartError: (err) => {
|
|
2421
|
+
logTypingFailure({
|
|
2422
|
+
log: (message) => log?.(message),
|
|
2423
|
+
channel: "mattermost",
|
|
2424
|
+
target: channelId,
|
|
2425
|
+
error: err
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
2430
|
+
...prefixOptions,
|
|
2431
|
+
humanDelay,
|
|
2432
|
+
deliver: async (payload) => {
|
|
2433
|
+
await deliverMattermostReplyPayload({
|
|
2434
|
+
core,
|
|
2435
|
+
cfg,
|
|
2436
|
+
payload,
|
|
2437
|
+
to,
|
|
2438
|
+
accountId: account.accountId,
|
|
2439
|
+
agentId: route.agentId,
|
|
2440
|
+
textLimit,
|
|
2441
|
+
tableMode,
|
|
2442
|
+
sendMessage: sendMessageMattermost
|
|
2443
|
+
});
|
|
2444
|
+
runtime.log?.(`delivered slash reply to ${to}`);
|
|
2445
|
+
},
|
|
2446
|
+
onError: (err, info) => {
|
|
2447
|
+
runtime.error?.(`mattermost slash ${info.kind} reply failed: ${String(err)}`);
|
|
2448
|
+
},
|
|
2449
|
+
onReplyStart: typingCallbacks.onReplyStart
|
|
2450
|
+
});
|
|
2451
|
+
await core.channel.reply.withReplyDispatcher({
|
|
2452
|
+
dispatcher,
|
|
2453
|
+
onSettled: () => {
|
|
2454
|
+
markDispatchIdle();
|
|
2455
|
+
},
|
|
2456
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
2457
|
+
ctx: ctxPayload,
|
|
2458
|
+
cfg,
|
|
2459
|
+
dispatcher,
|
|
2460
|
+
replyOptions: {
|
|
2461
|
+
...replyOptions,
|
|
2462
|
+
disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
|
|
2463
|
+
onModelSelected
|
|
2464
|
+
}
|
|
2465
|
+
})
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
//#endregion
|
|
2469
|
+
//#region extensions/mattermost/src/mattermost/slash-state.ts
|
|
2470
|
+
/** Map from accountId → per-account slash command state. */
|
|
2471
|
+
const accountStates = /* @__PURE__ */ new Map();
|
|
2472
|
+
function resolveSlashHandlerForToken(token) {
|
|
2473
|
+
const matches = [];
|
|
2474
|
+
for (const [accountId, state] of accountStates) if (state.commandTokens.has(token) && state.handler) matches.push({
|
|
2475
|
+
accountId,
|
|
2476
|
+
handler: state.handler
|
|
2477
|
+
});
|
|
2478
|
+
if (matches.length === 0) return { kind: "none" };
|
|
2479
|
+
if (matches.length === 1) return {
|
|
2480
|
+
kind: "single",
|
|
2481
|
+
handler: matches[0].handler,
|
|
2482
|
+
accountIds: [matches[0].accountId]
|
|
2483
|
+
};
|
|
2484
|
+
return {
|
|
2485
|
+
kind: "ambiguous",
|
|
2486
|
+
accountIds: matches.map((entry) => entry.accountId)
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Get the slash command state for a specific account, or null if not activated.
|
|
2491
|
+
*/
|
|
2492
|
+
function getSlashCommandState(accountId) {
|
|
2493
|
+
return accountStates.get(accountId) ?? null;
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Activate slash commands for a specific account.
|
|
2497
|
+
* Called from the monitor after bot connects.
|
|
2498
|
+
*/
|
|
2499
|
+
function activateSlashCommands(params) {
|
|
2500
|
+
const { account, commandTokens, registeredCommands, triggerMap, api, log } = params;
|
|
2501
|
+
const accountId = account.accountId;
|
|
2502
|
+
const tokenSet = new Set(commandTokens);
|
|
2503
|
+
const handler = createSlashCommandHttpHandler({
|
|
2504
|
+
account,
|
|
2505
|
+
cfg: api.cfg,
|
|
2506
|
+
runtime: api.runtime,
|
|
2507
|
+
commandTokens: tokenSet,
|
|
2508
|
+
triggerMap,
|
|
2509
|
+
log
|
|
2510
|
+
});
|
|
2511
|
+
accountStates.set(accountId, {
|
|
2512
|
+
commandTokens: tokenSet,
|
|
2513
|
+
registeredCommands,
|
|
2514
|
+
handler,
|
|
2515
|
+
account,
|
|
2516
|
+
triggerMap: triggerMap ?? /* @__PURE__ */ new Map()
|
|
2517
|
+
});
|
|
2518
|
+
log?.(`mattermost: slash commands activated for account ${accountId} (${registeredCommands.length} commands)`);
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Deactivate slash commands for a specific account (on shutdown/disconnect).
|
|
2522
|
+
*/
|
|
2523
|
+
function deactivateSlashCommands(accountId) {
|
|
2524
|
+
if (accountId) {
|
|
2525
|
+
const state = accountStates.get(accountId);
|
|
2526
|
+
if (state) {
|
|
2527
|
+
state.commandTokens.clear();
|
|
2528
|
+
state.registeredCommands = [];
|
|
2529
|
+
state.handler = null;
|
|
2530
|
+
accountStates.delete(accountId);
|
|
2531
|
+
}
|
|
2532
|
+
} else {
|
|
2533
|
+
for (const [, state] of accountStates) {
|
|
2534
|
+
state.commandTokens.clear();
|
|
2535
|
+
state.registeredCommands = [];
|
|
2536
|
+
state.handler = null;
|
|
2537
|
+
}
|
|
2538
|
+
accountStates.clear();
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Register the HTTP route for slash command callbacks.
|
|
2543
|
+
* Called during plugin registration.
|
|
2544
|
+
*
|
|
2545
|
+
* The single HTTP route dispatches to the correct per-account handler
|
|
2546
|
+
* by matching the inbound token against each account's registered tokens.
|
|
2547
|
+
*/
|
|
2548
|
+
function registerSlashCommandRoute(api) {
|
|
2549
|
+
const mmConfig = api.config.channels?.mattermost;
|
|
2550
|
+
const callbackPaths = /* @__PURE__ */ new Set();
|
|
2551
|
+
const addCallbackPaths = (raw) => {
|
|
2552
|
+
const resolved = resolveSlashCommandConfig(raw);
|
|
2553
|
+
callbackPaths.add(resolved.callbackPath);
|
|
2554
|
+
if (resolved.callbackUrl) try {
|
|
2555
|
+
const urlPath = new URL(resolved.callbackUrl).pathname;
|
|
2556
|
+
if (urlPath && urlPath !== resolved.callbackPath) callbackPaths.add(urlPath);
|
|
2557
|
+
} catch {}
|
|
2558
|
+
};
|
|
2559
|
+
const commandsRaw = mmConfig?.commands;
|
|
2560
|
+
addCallbackPaths(commandsRaw);
|
|
2561
|
+
const accountsRaw = mmConfig?.accounts ?? {};
|
|
2562
|
+
for (const accountId of Object.keys(accountsRaw)) {
|
|
2563
|
+
const accountCommandsRaw = accountsRaw[accountId]?.commands;
|
|
2564
|
+
addCallbackPaths(accountCommandsRaw);
|
|
2565
|
+
}
|
|
2566
|
+
const routeHandler = async (req, res) => {
|
|
2567
|
+
if (accountStates.size === 0) {
|
|
2568
|
+
res.statusCode = 503;
|
|
2569
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2570
|
+
res.end(JSON.stringify({
|
|
2571
|
+
response_type: "ephemeral",
|
|
2572
|
+
text: "Slash commands are not yet initialized. Please try again in a moment."
|
|
2573
|
+
}));
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
if (accountStates.size === 1) {
|
|
2577
|
+
const [, state] = [...accountStates.entries()][0];
|
|
2578
|
+
if (!state.handler) {
|
|
2579
|
+
res.statusCode = 503;
|
|
2580
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2581
|
+
res.end(JSON.stringify({
|
|
2582
|
+
response_type: "ephemeral",
|
|
2583
|
+
text: "Slash commands are not yet initialized. Please try again in a moment."
|
|
2584
|
+
}));
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
await state.handler(req, res);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
const chunks = [];
|
|
2591
|
+
const MAX_BODY = 64 * 1024;
|
|
2592
|
+
let size = 0;
|
|
2593
|
+
for await (const chunk of req) {
|
|
2594
|
+
size += chunk.length;
|
|
2595
|
+
if (size > MAX_BODY) {
|
|
2596
|
+
res.statusCode = 413;
|
|
2597
|
+
res.end("Payload Too Large");
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
chunks.push(chunk);
|
|
2601
|
+
}
|
|
2602
|
+
const bodyStr = Buffer.concat(chunks).toString("utf8");
|
|
2603
|
+
let token = null;
|
|
2604
|
+
const ct = req.headers["content-type"] ?? "";
|
|
2605
|
+
try {
|
|
2606
|
+
if (ct.includes("application/json")) token = JSON.parse(bodyStr).token ?? null;
|
|
2607
|
+
else token = new URLSearchParams(bodyStr).get("token");
|
|
2608
|
+
} catch {}
|
|
2609
|
+
const match = token ? resolveSlashHandlerForToken(token) : { kind: "none" };
|
|
2610
|
+
if (match.kind === "none") {
|
|
2611
|
+
res.statusCode = 401;
|
|
2612
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2613
|
+
res.end(JSON.stringify({
|
|
2614
|
+
response_type: "ephemeral",
|
|
2615
|
+
text: "Unauthorized: invalid command token."
|
|
2616
|
+
}));
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
if (match.kind === "ambiguous") {
|
|
2620
|
+
api.logger.warn?.(`mattermost: slash callback token matched multiple accounts (${match.accountIds?.join(", ")})`);
|
|
2621
|
+
res.statusCode = 409;
|
|
2622
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2623
|
+
res.end(JSON.stringify({
|
|
2624
|
+
response_type: "ephemeral",
|
|
2625
|
+
text: "Conflict: command token is not unique across accounts."
|
|
2626
|
+
}));
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const matchedHandler = match.handler;
|
|
2630
|
+
const { Readable } = await import("node:stream");
|
|
2631
|
+
const syntheticReq = new Readable({ read() {
|
|
2632
|
+
this.push(Buffer.from(bodyStr, "utf8"));
|
|
2633
|
+
this.push(null);
|
|
2634
|
+
} });
|
|
2635
|
+
syntheticReq.method = req.method;
|
|
2636
|
+
syntheticReq.url = req.url;
|
|
2637
|
+
syntheticReq.headers = req.headers;
|
|
2638
|
+
await matchedHandler(syntheticReq, res);
|
|
2639
|
+
};
|
|
2640
|
+
for (const callbackPath of callbackPaths) {
|
|
2641
|
+
api.registerHttpRoute({
|
|
2642
|
+
path: callbackPath,
|
|
2643
|
+
auth: "plugin",
|
|
2644
|
+
handler: routeHandler
|
|
2645
|
+
});
|
|
2646
|
+
api.logger.info?.(`mattermost: registered slash command callback at ${callbackPath}`);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
//#endregion
|
|
2650
|
+
//#region extensions/mattermost/src/mattermost/monitor.ts
|
|
2651
|
+
const RECENT_MATTERMOST_MESSAGE_TTL_MS = 5 * 6e4;
|
|
2652
|
+
const RECENT_MATTERMOST_MESSAGE_MAX = 2e3;
|
|
2653
|
+
const CHANNEL_CACHE_TTL_MS = 5 * 6e4;
|
|
2654
|
+
const USER_CACHE_TTL_MS = 10 * 6e4;
|
|
2655
|
+
function isLoopbackHost(hostname) {
|
|
2656
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
2657
|
+
}
|
|
2658
|
+
function normalizeInteractionSourceIps(values) {
|
|
2659
|
+
return (values ?? []).map((value) => value.trim()).filter(Boolean);
|
|
2660
|
+
}
|
|
2661
|
+
const recentInboundMessages = createDedupeCache({
|
|
2662
|
+
ttlMs: RECENT_MATTERMOST_MESSAGE_TTL_MS,
|
|
2663
|
+
maxSize: RECENT_MATTERMOST_MESSAGE_MAX
|
|
2664
|
+
});
|
|
2665
|
+
function resolveRuntime(opts) {
|
|
2666
|
+
return opts.runtime ?? {
|
|
2667
|
+
log: console.log,
|
|
2668
|
+
error: console.error,
|
|
2669
|
+
exit: (code) => {
|
|
2670
|
+
throw new Error(`exit ${code}`);
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
function isSystemPost(post) {
|
|
2675
|
+
const type = post.type?.trim();
|
|
2676
|
+
return Boolean(type);
|
|
2677
|
+
}
|
|
2678
|
+
function mapMattermostChannelTypeToChatType(channelType) {
|
|
2679
|
+
if (!channelType) return "channel";
|
|
2680
|
+
const normalized = channelType.trim().toUpperCase();
|
|
2681
|
+
if (normalized === "D") return "direct";
|
|
2682
|
+
if (normalized === "G") return "group";
|
|
2683
|
+
if (normalized === "P") return "group";
|
|
2684
|
+
return "channel";
|
|
2685
|
+
}
|
|
2686
|
+
function channelChatType(kind) {
|
|
2687
|
+
if (kind === "direct") return "direct";
|
|
2688
|
+
if (kind === "group") return "group";
|
|
2689
|
+
return "channel";
|
|
2690
|
+
}
|
|
2691
|
+
function evaluateMattermostMentionGate(params) {
|
|
2692
|
+
const shouldRequireMention = params.kind !== "direct" && params.resolveRequireMention({
|
|
2693
|
+
cfg: params.cfg,
|
|
2694
|
+
channel: "mattermost",
|
|
2695
|
+
accountId: params.accountId,
|
|
2696
|
+
groupId: params.channelId,
|
|
2697
|
+
requireMentionOverride: params.requireMentionOverride
|
|
2698
|
+
});
|
|
2699
|
+
const shouldBypassMention = params.isControlCommand && shouldRequireMention && !params.wasMentioned && params.commandAuthorized;
|
|
2700
|
+
const effectiveWasMentioned = params.wasMentioned || shouldBypassMention || params.oncharTriggered;
|
|
2701
|
+
if (params.oncharEnabled && !params.oncharTriggered && !params.wasMentioned && !params.isControlCommand) return {
|
|
2702
|
+
shouldRequireMention,
|
|
2703
|
+
shouldBypassMention,
|
|
2704
|
+
effectiveWasMentioned,
|
|
2705
|
+
dropReason: "onchar-not-triggered"
|
|
2706
|
+
};
|
|
2707
|
+
if (params.kind !== "direct" && shouldRequireMention && params.canDetectMention && !effectiveWasMentioned) return {
|
|
2708
|
+
shouldRequireMention,
|
|
2709
|
+
shouldBypassMention,
|
|
2710
|
+
effectiveWasMentioned,
|
|
2711
|
+
dropReason: "missing-mention"
|
|
2712
|
+
};
|
|
2713
|
+
return {
|
|
2714
|
+
shouldRequireMention,
|
|
2715
|
+
shouldBypassMention,
|
|
2716
|
+
effectiveWasMentioned,
|
|
2717
|
+
dropReason: null
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
function resolveMattermostReplyRootId(params) {
|
|
2721
|
+
const threadRootId = params.threadRootId?.trim();
|
|
2722
|
+
if (threadRootId) return threadRootId;
|
|
2723
|
+
return params.replyToId?.trim() || void 0;
|
|
2724
|
+
}
|
|
2725
|
+
function resolveMattermostEffectiveReplyToId(params) {
|
|
2726
|
+
const threadRootId = params.threadRootId?.trim();
|
|
2727
|
+
if (threadRootId) return threadRootId;
|
|
2728
|
+
if (params.kind === "direct") return;
|
|
2729
|
+
const postId = params.postId?.trim();
|
|
2730
|
+
if (!postId) return;
|
|
2731
|
+
return params.replyToMode === "all" || params.replyToMode === "first" ? postId : void 0;
|
|
2732
|
+
}
|
|
2733
|
+
function resolveMattermostThreadSessionContext(params) {
|
|
2734
|
+
const effectiveReplyToId = resolveMattermostEffectiveReplyToId({
|
|
2735
|
+
kind: params.kind,
|
|
2736
|
+
postId: params.postId,
|
|
2737
|
+
replyToMode: params.replyToMode,
|
|
2738
|
+
threadRootId: params.threadRootId
|
|
2739
|
+
});
|
|
2740
|
+
const threadKeys = resolveThreadSessionKeys({
|
|
2741
|
+
baseSessionKey: params.baseSessionKey,
|
|
2742
|
+
threadId: effectiveReplyToId,
|
|
2743
|
+
parentSessionKey: effectiveReplyToId ? params.baseSessionKey : void 0
|
|
2744
|
+
});
|
|
2745
|
+
return {
|
|
2746
|
+
effectiveReplyToId,
|
|
2747
|
+
sessionKey: threadKeys.sessionKey,
|
|
2748
|
+
parentSessionKey: threadKeys.parentSessionKey
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
function buildMattermostAttachmentPlaceholder(mediaList) {
|
|
2752
|
+
if (mediaList.length === 0) return "";
|
|
2753
|
+
if (mediaList.length === 1) return `<media:${mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind}>`;
|
|
2754
|
+
const allImages = mediaList.every((media) => media.kind === "image");
|
|
2755
|
+
const label = allImages ? "image" : "file";
|
|
2756
|
+
const suffix = mediaList.length === 1 ? label : `${label}s`;
|
|
2757
|
+
return `${allImages ? "<media:image>" : "<media:document>"} (${mediaList.length} ${suffix})`;
|
|
2758
|
+
}
|
|
2759
|
+
function buildMattermostWsUrl(baseUrl) {
|
|
2760
|
+
const normalized = normalizeMattermostBaseUrl(baseUrl);
|
|
2761
|
+
if (!normalized) throw new Error("Mattermost baseUrl is required");
|
|
2762
|
+
return `${normalized.replace(/^http/i, "ws")}/api/v4/websocket`;
|
|
2763
|
+
}
|
|
2764
|
+
async function monitorMattermostProvider(opts = {}) {
|
|
2765
|
+
const core = getMattermostRuntime();
|
|
2766
|
+
const runtime = resolveRuntime(opts);
|
|
2767
|
+
const cfg = opts.config ?? core.config.loadConfig();
|
|
2768
|
+
const account = resolveMattermostAccount({
|
|
2769
|
+
cfg,
|
|
2770
|
+
accountId: opts.accountId
|
|
2771
|
+
});
|
|
2772
|
+
const pairing = createScopedPairingAccess({
|
|
2773
|
+
core,
|
|
2774
|
+
channel: "mattermost",
|
|
2775
|
+
accountId: account.accountId
|
|
2776
|
+
});
|
|
2777
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
2778
|
+
const botToken = opts.botToken?.trim() || account.botToken?.trim();
|
|
2779
|
+
if (!botToken) throw new Error(`Mattermost bot token missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.botToken or MATTERMOST_BOT_TOKEN for default).`);
|
|
2780
|
+
const baseUrl = normalizeMattermostBaseUrl(opts.baseUrl ?? account.baseUrl);
|
|
2781
|
+
if (!baseUrl) throw new Error(`Mattermost baseUrl missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.baseUrl or MATTERMOST_URL for default).`);
|
|
2782
|
+
const client = createMattermostClient({
|
|
2783
|
+
baseUrl,
|
|
2784
|
+
botToken
|
|
2785
|
+
});
|
|
2786
|
+
const botUser = await fetchMattermostMe(client);
|
|
2787
|
+
const botUserId = botUser.id;
|
|
2788
|
+
const botUsername = botUser.username?.trim() || void 0;
|
|
2789
|
+
runtime.log?.(`mattermost connected as ${botUsername ? `@${botUsername}` : botUserId}`);
|
|
2790
|
+
const commandsRaw = account.config.commands;
|
|
2791
|
+
const slashConfig = resolveSlashCommandConfig(commandsRaw);
|
|
2792
|
+
const slashEnabled = isSlashCommandsEnabled(slashConfig);
|
|
2793
|
+
if (slashEnabled) try {
|
|
2794
|
+
const teams = await fetchMattermostUserTeams(client, botUserId);
|
|
2795
|
+
const envPortRaw = process.env.MOLDCLAW_GATEWAY_PORT?.trim();
|
|
2796
|
+
const slashCallbackUrl = resolveCallbackUrl({
|
|
2797
|
+
config: slashConfig,
|
|
2798
|
+
gatewayPort: parseStrictPositiveInteger(envPortRaw) ?? cfg.gateway?.port ?? 18789,
|
|
2799
|
+
gatewayHost: cfg.gateway?.customBindHost ?? void 0
|
|
2800
|
+
});
|
|
2801
|
+
try {
|
|
2802
|
+
const mmHost = new URL(baseUrl).hostname;
|
|
2803
|
+
const callbackHost = new URL(slashCallbackUrl).hostname;
|
|
2804
|
+
if (isLoopbackHost(callbackHost) && !isLoopbackHost(mmHost)) runtime.error?.(`mattermost: slash commands callbackUrl resolved to ${slashCallbackUrl} (loopback) while baseUrl is ${baseUrl}. This MAY be unreachable depending on your deployment. If native slash commands don't work, set channels.mattermost.commands.callbackUrl to a URL reachable from the Mattermost server (e.g. your public reverse proxy URL).`);
|
|
2805
|
+
} catch {}
|
|
2806
|
+
const commandsToRegister = [...DEFAULT_COMMAND_SPECS];
|
|
2807
|
+
if (slashConfig.nativeSkills === true) try {
|
|
2808
|
+
const skillCommands = listSkillCommandsForAgents({ cfg });
|
|
2809
|
+
for (const spec of skillCommands) {
|
|
2810
|
+
const name = typeof spec.name === "string" ? spec.name.trim() : "";
|
|
2811
|
+
if (!name) continue;
|
|
2812
|
+
const trigger = name.startsWith("oc_") ? name : `oc_${name}`;
|
|
2813
|
+
commandsToRegister.push({
|
|
2814
|
+
trigger,
|
|
2815
|
+
description: spec.description || `Run skill ${name}`,
|
|
2816
|
+
autoComplete: true,
|
|
2817
|
+
autoCompleteHint: "[args]",
|
|
2818
|
+
originalName: name
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
runtime.error?.(`mattermost: failed to list skill commands: ${String(err)}`);
|
|
2823
|
+
}
|
|
2824
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2825
|
+
const dedupedCommands = commandsToRegister.filter((cmd) => {
|
|
2826
|
+
const key = cmd.trigger.trim();
|
|
2827
|
+
if (!key) return false;
|
|
2828
|
+
if (seen.has(key)) return false;
|
|
2829
|
+
seen.add(key);
|
|
2830
|
+
return true;
|
|
2831
|
+
});
|
|
2832
|
+
const allRegistered = [];
|
|
2833
|
+
let teamRegistrationFailures = 0;
|
|
2834
|
+
for (const team of teams) try {
|
|
2835
|
+
const registered = await registerSlashCommands({
|
|
2836
|
+
client,
|
|
2837
|
+
teamId: team.id,
|
|
2838
|
+
creatorUserId: botUserId,
|
|
2839
|
+
callbackUrl: slashCallbackUrl,
|
|
2840
|
+
commands: dedupedCommands,
|
|
2841
|
+
log: (msg) => runtime.log?.(msg)
|
|
2842
|
+
});
|
|
2843
|
+
allRegistered.push(...registered);
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
teamRegistrationFailures += 1;
|
|
2846
|
+
runtime.error?.(`mattermost: failed to register slash commands for team ${team.id}: ${String(err)}`);
|
|
2847
|
+
}
|
|
2848
|
+
if (allRegistered.length === 0) runtime.error?.("mattermost: native slash commands enabled but no commands could be registered; keeping slash callbacks inactive");
|
|
2849
|
+
else {
|
|
2850
|
+
if (teamRegistrationFailures > 0) runtime.error?.(`mattermost: slash command registration completed with ${teamRegistrationFailures} team error(s)`);
|
|
2851
|
+
const triggerMap = /* @__PURE__ */ new Map();
|
|
2852
|
+
for (const cmd of dedupedCommands) if (cmd.originalName) triggerMap.set(cmd.trigger, cmd.originalName);
|
|
2853
|
+
activateSlashCommands({
|
|
2854
|
+
account,
|
|
2855
|
+
commandTokens: allRegistered.map((cmd) => cmd.token).filter(Boolean),
|
|
2856
|
+
registeredCommands: allRegistered,
|
|
2857
|
+
triggerMap,
|
|
2858
|
+
api: {
|
|
2859
|
+
cfg,
|
|
2860
|
+
runtime
|
|
2861
|
+
},
|
|
2862
|
+
log: (msg) => runtime.log?.(msg)
|
|
2863
|
+
});
|
|
2864
|
+
runtime.log?.(`mattermost: slash commands registered (${allRegistered.length} commands across ${teams.length} teams, callback=${slashCallbackUrl})`);
|
|
2865
|
+
}
|
|
2866
|
+
} catch (err) {
|
|
2867
|
+
runtime.error?.(`mattermost: failed to register slash commands: ${String(err)}`);
|
|
2868
|
+
}
|
|
2869
|
+
setInteractionSecret(account.accountId, botToken);
|
|
2870
|
+
const interactionPath = resolveInteractionCallbackPath(account.accountId);
|
|
2871
|
+
const callbackUrl = computeInteractionCallbackUrl(account.accountId, {
|
|
2872
|
+
gateway: cfg.gateway,
|
|
2873
|
+
interactions: account.config.interactions
|
|
2874
|
+
});
|
|
2875
|
+
setInteractionCallbackUrl(account.accountId, callbackUrl);
|
|
2876
|
+
const allowedInteractionSourceIps = normalizeInteractionSourceIps(account.config.interactions?.allowedSourceIps);
|
|
2877
|
+
try {
|
|
2878
|
+
const mmHost = new URL(baseUrl).hostname;
|
|
2879
|
+
const callbackHost = new URL(callbackUrl).hostname;
|
|
2880
|
+
if (isLoopbackHost(callbackHost) && !isLoopbackHost(mmHost)) runtime.error?.(`mattermost: interactions callbackUrl resolved to ${callbackUrl} (loopback) while baseUrl is ${baseUrl}. This MAY be unreachable depending on your deployment. If button clicks don't work, set channels.mattermost.interactions.callbackBaseUrl to a URL reachable from the Mattermost server (e.g. your public reverse proxy URL).`);
|
|
2881
|
+
if (!isLoopbackHost(callbackHost) && allowedInteractionSourceIps.length === 0) runtime.error?.(`mattermost: interactions callbackUrl resolved to ${callbackUrl} without channels.mattermost.interactions.allowedSourceIps. For safety, non-loopback callback sources will be rejected until you allowlist the Mattermost server or trusted ingress IPs.`);
|
|
2882
|
+
} catch {}
|
|
2883
|
+
const effectiveInteractionSourceIps = allowedInteractionSourceIps.length > 0 ? allowedInteractionSourceIps : ["127.0.0.1", "::1"];
|
|
2884
|
+
const unregisterInteractions = registerPluginHttpRoute({
|
|
2885
|
+
path: interactionPath,
|
|
2886
|
+
fallbackPath: "/mattermost/interactions/default",
|
|
2887
|
+
auth: "plugin",
|
|
2888
|
+
handler: createMattermostInteractionHandler({
|
|
2889
|
+
client,
|
|
2890
|
+
botUserId,
|
|
2891
|
+
accountId: account.accountId,
|
|
2892
|
+
allowedSourceIps: effectiveInteractionSourceIps,
|
|
2893
|
+
trustedProxies: cfg.gateway?.trustedProxies,
|
|
2894
|
+
allowRealIpFallback: cfg.gateway?.allowRealIpFallback === true,
|
|
2895
|
+
handleInteraction: handleModelPickerInteraction,
|
|
2896
|
+
authorizeButtonClick: async ({ payload, post }) => {
|
|
2897
|
+
const channelInfo = await resolveChannelInfo(payload.channel_id);
|
|
2898
|
+
const isDirect = channelInfo?.type?.trim().toUpperCase() === "D";
|
|
2899
|
+
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
2900
|
+
cfg,
|
|
2901
|
+
surface: "mattermost"
|
|
2902
|
+
});
|
|
2903
|
+
const decision = authorizeMattermostCommandInvocation({
|
|
2904
|
+
account,
|
|
2905
|
+
cfg,
|
|
2906
|
+
senderId: payload.user_id,
|
|
2907
|
+
senderName: payload.user_name ?? "",
|
|
2908
|
+
channelId: payload.channel_id,
|
|
2909
|
+
channelInfo,
|
|
2910
|
+
storeAllowFrom: isDirect ? await readStoreAllowFromForDmPolicy({
|
|
2911
|
+
provider: "mattermost",
|
|
2912
|
+
accountId: account.accountId,
|
|
2913
|
+
dmPolicy: account.config.dmPolicy ?? "pairing",
|
|
2914
|
+
readStore: pairing.readStoreForDmPolicy
|
|
2915
|
+
}) : void 0,
|
|
2916
|
+
allowTextCommands,
|
|
2917
|
+
hasControlCommand: false
|
|
2918
|
+
});
|
|
2919
|
+
if (decision.ok) return { ok: true };
|
|
2920
|
+
return {
|
|
2921
|
+
ok: false,
|
|
2922
|
+
response: {
|
|
2923
|
+
update: {
|
|
2924
|
+
message: post.message ?? "",
|
|
2925
|
+
props: post.props
|
|
2926
|
+
},
|
|
2927
|
+
ephemeral_text: `MoldClaw ignored this action for ${decision.roomLabel}.`
|
|
2928
|
+
}
|
|
2929
|
+
};
|
|
2930
|
+
},
|
|
2931
|
+
resolveSessionKey: async ({ channelId, userId, post }) => {
|
|
2932
|
+
const channelInfo = await resolveChannelInfo(channelId);
|
|
2933
|
+
const kind = mapMattermostChannelTypeToChatType(channelInfo?.type);
|
|
2934
|
+
const teamId = channelInfo?.team_id ?? void 0;
|
|
2935
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
2936
|
+
cfg,
|
|
2937
|
+
channel: "mattermost",
|
|
2938
|
+
accountId: account.accountId,
|
|
2939
|
+
teamId,
|
|
2940
|
+
peer: {
|
|
2941
|
+
kind,
|
|
2942
|
+
id: kind === "direct" ? userId : channelId
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
const replyToMode = resolveMattermostReplyToMode(account, kind);
|
|
2946
|
+
return resolveMattermostThreadSessionContext({
|
|
2947
|
+
baseSessionKey: route.sessionKey,
|
|
2948
|
+
kind,
|
|
2949
|
+
postId: post.id || void 0,
|
|
2950
|
+
replyToMode,
|
|
2951
|
+
threadRootId: post.root_id
|
|
2952
|
+
}).sessionKey;
|
|
2953
|
+
},
|
|
2954
|
+
dispatchButtonClick: async (opts) => {
|
|
2955
|
+
const channelInfo = await resolveChannelInfo(opts.channelId);
|
|
2956
|
+
const kind = mapMattermostChannelTypeToChatType(channelInfo?.type);
|
|
2957
|
+
const chatType = channelChatType(kind);
|
|
2958
|
+
const teamId = channelInfo?.team_id ?? void 0;
|
|
2959
|
+
const channelName = channelInfo?.name ?? void 0;
|
|
2960
|
+
const channelDisplay = channelInfo?.display_name ?? channelName ?? opts.channelId;
|
|
2961
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
2962
|
+
cfg,
|
|
2963
|
+
channel: "mattermost",
|
|
2964
|
+
accountId: account.accountId,
|
|
2965
|
+
teamId,
|
|
2966
|
+
peer: {
|
|
2967
|
+
kind,
|
|
2968
|
+
id: kind === "direct" ? opts.userId : opts.channelId
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
const replyToMode = resolveMattermostReplyToMode(account, kind);
|
|
2972
|
+
const threadContext = resolveMattermostThreadSessionContext({
|
|
2973
|
+
baseSessionKey: route.sessionKey,
|
|
2974
|
+
kind,
|
|
2975
|
+
postId: opts.post.id || opts.postId,
|
|
2976
|
+
replyToMode,
|
|
2977
|
+
threadRootId: opts.post.root_id
|
|
2978
|
+
});
|
|
2979
|
+
const to = kind === "direct" ? `user:${opts.userId}` : `channel:${opts.channelId}`;
|
|
2980
|
+
const bodyText = `[Button click: user @${opts.userName} selected "${opts.actionName}"]`;
|
|
2981
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
2982
|
+
Body: bodyText,
|
|
2983
|
+
BodyForAgent: bodyText,
|
|
2984
|
+
RawBody: bodyText,
|
|
2985
|
+
CommandBody: bodyText,
|
|
2986
|
+
From: kind === "direct" ? `mattermost:${opts.userId}` : kind === "group" ? `mattermost:group:${opts.channelId}` : `mattermost:channel:${opts.channelId}`,
|
|
2987
|
+
To: to,
|
|
2988
|
+
SessionKey: threadContext.sessionKey,
|
|
2989
|
+
ParentSessionKey: threadContext.parentSessionKey,
|
|
2990
|
+
AccountId: route.accountId,
|
|
2991
|
+
ChatType: chatType,
|
|
2992
|
+
ConversationLabel: `mattermost:${opts.userName}`,
|
|
2993
|
+
GroupSubject: kind !== "direct" ? channelDisplay : void 0,
|
|
2994
|
+
GroupChannel: channelName ? `#${channelName}` : void 0,
|
|
2995
|
+
GroupSpace: teamId,
|
|
2996
|
+
SenderName: opts.userName,
|
|
2997
|
+
SenderId: opts.userId,
|
|
2998
|
+
Provider: "mattermost",
|
|
2999
|
+
Surface: "mattermost",
|
|
3000
|
+
MessageSid: `interaction:${opts.postId}:${opts.actionId}`,
|
|
3001
|
+
ReplyToId: threadContext.effectiveReplyToId,
|
|
3002
|
+
MessageThreadId: threadContext.effectiveReplyToId,
|
|
3003
|
+
WasMentioned: true,
|
|
3004
|
+
CommandAuthorized: false,
|
|
3005
|
+
OriginatingChannel: "mattermost",
|
|
3006
|
+
OriginatingTo: to
|
|
3007
|
+
});
|
|
3008
|
+
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
|
|
3009
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
3010
|
+
cfg,
|
|
3011
|
+
channel: "mattermost",
|
|
3012
|
+
accountId: account.accountId
|
|
3013
|
+
});
|
|
3014
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
3015
|
+
cfg,
|
|
3016
|
+
agentId: route.agentId,
|
|
3017
|
+
channel: "mattermost",
|
|
3018
|
+
accountId: account.accountId
|
|
3019
|
+
});
|
|
3020
|
+
const typingCallbacks = createTypingCallbacks({
|
|
3021
|
+
start: () => sendTypingIndicator(opts.channelId, threadContext.effectiveReplyToId),
|
|
3022
|
+
onStartError: (err) => {
|
|
3023
|
+
logTypingFailure({
|
|
3024
|
+
log: (message) => logger.debug?.(message),
|
|
3025
|
+
channel: "mattermost",
|
|
3026
|
+
target: opts.channelId,
|
|
3027
|
+
error: err
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
3032
|
+
...prefixOptions,
|
|
3033
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
3034
|
+
deliver: async (payload) => {
|
|
3035
|
+
await deliverMattermostReplyPayload({
|
|
3036
|
+
core,
|
|
3037
|
+
cfg,
|
|
3038
|
+
payload,
|
|
3039
|
+
to,
|
|
3040
|
+
accountId: account.accountId,
|
|
3041
|
+
agentId: route.agentId,
|
|
3042
|
+
replyToId: resolveMattermostReplyRootId({
|
|
3043
|
+
threadRootId: threadContext.effectiveReplyToId,
|
|
3044
|
+
replyToId: payload.replyToId
|
|
3045
|
+
}),
|
|
3046
|
+
textLimit,
|
|
3047
|
+
tableMode,
|
|
3048
|
+
sendMessage: sendMessageMattermost
|
|
3049
|
+
});
|
|
3050
|
+
runtime.log?.(`delivered button-click reply to ${to}`);
|
|
3051
|
+
},
|
|
3052
|
+
onError: (err, info) => {
|
|
3053
|
+
runtime.error?.(`mattermost button-click ${info.kind} reply failed: ${String(err)}`);
|
|
3054
|
+
},
|
|
3055
|
+
onReplyStart: typingCallbacks.onReplyStart
|
|
3056
|
+
});
|
|
3057
|
+
await core.channel.reply.dispatchReplyFromConfig({
|
|
3058
|
+
ctx: ctxPayload,
|
|
3059
|
+
cfg,
|
|
3060
|
+
dispatcher,
|
|
3061
|
+
replyOptions: {
|
|
3062
|
+
...replyOptions,
|
|
3063
|
+
disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
|
|
3064
|
+
onModelSelected
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
markDispatchIdle();
|
|
3068
|
+
},
|
|
3069
|
+
log: (msg) => runtime.log?.(msg)
|
|
3070
|
+
}),
|
|
3071
|
+
pluginId: "mattermost",
|
|
3072
|
+
source: "mattermost-interactions",
|
|
3073
|
+
accountId: account.accountId,
|
|
3074
|
+
log: (msg) => runtime.log?.(msg)
|
|
3075
|
+
});
|
|
3076
|
+
const channelCache = /* @__PURE__ */ new Map();
|
|
3077
|
+
const userCache = /* @__PURE__ */ new Map();
|
|
3078
|
+
const logger = core.logging.getChildLogger({ module: "mattermost" });
|
|
3079
|
+
const logVerboseMessage = (message) => {
|
|
3080
|
+
if (!core.logging.shouldLogVerbose()) return;
|
|
3081
|
+
logger.debug?.(message);
|
|
3082
|
+
};
|
|
3083
|
+
const mediaMaxBytes = resolveChannelMediaMaxBytes({
|
|
3084
|
+
cfg,
|
|
3085
|
+
resolveChannelLimitMb: () => void 0,
|
|
3086
|
+
accountId: account.accountId
|
|
3087
|
+
}) ?? 8 * 1024 * 1024;
|
|
3088
|
+
const historyLimit = Math.max(0, cfg.messages?.groupChat?.historyLimit ?? 50);
|
|
3089
|
+
const channelHistories = /* @__PURE__ */ new Map();
|
|
3090
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
3091
|
+
const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
|
|
3092
|
+
providerConfigPresent: cfg.channels?.mattermost !== void 0,
|
|
3093
|
+
groupPolicy: account.config.groupPolicy,
|
|
3094
|
+
defaultGroupPolicy
|
|
3095
|
+
});
|
|
3096
|
+
warnMissingProviderGroupPolicyFallbackOnce({
|
|
3097
|
+
providerMissingFallbackApplied,
|
|
3098
|
+
providerKey: "mattermost",
|
|
3099
|
+
accountId: account.accountId,
|
|
3100
|
+
log: (message) => logVerboseMessage(message)
|
|
3101
|
+
});
|
|
3102
|
+
const resolveMattermostMedia = async (fileIds) => {
|
|
3103
|
+
const ids = (fileIds ?? []).map((id) => id?.trim()).filter(Boolean);
|
|
3104
|
+
if (ids.length === 0) return [];
|
|
3105
|
+
const out = [];
|
|
3106
|
+
for (const fileId of ids) try {
|
|
3107
|
+
const fetched = await core.channel.media.fetchRemoteMedia({
|
|
3108
|
+
url: `${client.apiBaseUrl}/files/${fileId}`,
|
|
3109
|
+
requestInit: { headers: { Authorization: `Bearer ${client.token}` } },
|
|
3110
|
+
filePathHint: fileId,
|
|
3111
|
+
maxBytes: mediaMaxBytes,
|
|
3112
|
+
ssrfPolicy: { allowedHostnames: [new URL(client.baseUrl).hostname] }
|
|
3113
|
+
});
|
|
3114
|
+
const saved = await core.channel.media.saveMediaBuffer(fetched.buffer, fetched.contentType ?? void 0, "inbound", mediaMaxBytes);
|
|
3115
|
+
const contentType = saved.contentType ?? fetched.contentType ?? void 0;
|
|
3116
|
+
out.push({
|
|
3117
|
+
path: saved.path,
|
|
3118
|
+
contentType,
|
|
3119
|
+
kind: core.media.mediaKindFromMime(contentType) ?? "unknown"
|
|
3120
|
+
});
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
logger.debug?.(`mattermost: failed to download file ${fileId}: ${String(err)}`);
|
|
3123
|
+
}
|
|
3124
|
+
return out;
|
|
3125
|
+
};
|
|
3126
|
+
const sendTypingIndicator = async (channelId, parentId) => {
|
|
3127
|
+
await sendMattermostTyping(client, {
|
|
3128
|
+
channelId,
|
|
3129
|
+
parentId
|
|
3130
|
+
});
|
|
3131
|
+
};
|
|
3132
|
+
const resolveChannelInfo = async (channelId) => {
|
|
3133
|
+
const cached = channelCache.get(channelId);
|
|
3134
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
3135
|
+
try {
|
|
3136
|
+
const info = await fetchMattermostChannel(client, channelId);
|
|
3137
|
+
channelCache.set(channelId, {
|
|
3138
|
+
value: info,
|
|
3139
|
+
expiresAt: Date.now() + CHANNEL_CACHE_TTL_MS
|
|
3140
|
+
});
|
|
3141
|
+
return info;
|
|
3142
|
+
} catch (err) {
|
|
3143
|
+
logger.debug?.(`mattermost: channel lookup failed: ${String(err)}`);
|
|
3144
|
+
channelCache.set(channelId, {
|
|
3145
|
+
value: null,
|
|
3146
|
+
expiresAt: Date.now() + CHANNEL_CACHE_TTL_MS
|
|
3147
|
+
});
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
};
|
|
3151
|
+
const resolveUserInfo = async (userId) => {
|
|
3152
|
+
const cached = userCache.get(userId);
|
|
3153
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
3154
|
+
try {
|
|
3155
|
+
const info = await fetchMattermostUser(client, userId);
|
|
3156
|
+
userCache.set(userId, {
|
|
3157
|
+
value: info,
|
|
3158
|
+
expiresAt: Date.now() + USER_CACHE_TTL_MS
|
|
3159
|
+
});
|
|
3160
|
+
return info;
|
|
3161
|
+
} catch (err) {
|
|
3162
|
+
logger.debug?.(`mattermost: user lookup failed: ${String(err)}`);
|
|
3163
|
+
userCache.set(userId, {
|
|
3164
|
+
value: null,
|
|
3165
|
+
expiresAt: Date.now() + USER_CACHE_TTL_MS
|
|
3166
|
+
});
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
};
|
|
3170
|
+
const buildModelPickerProps = (channelId, buttons) => buildButtonProps({
|
|
3171
|
+
callbackUrl,
|
|
3172
|
+
accountId: account.accountId,
|
|
3173
|
+
channelId,
|
|
3174
|
+
buttons
|
|
3175
|
+
});
|
|
3176
|
+
const updateModelPickerPost = async (params) => {
|
|
3177
|
+
const props = buildModelPickerProps(params.channelId, params.buttons ?? []) ?? { attachments: [] };
|
|
3178
|
+
await updateMattermostPost(client, params.postId, {
|
|
3179
|
+
message: params.message,
|
|
3180
|
+
props
|
|
3181
|
+
});
|
|
3182
|
+
return {};
|
|
3183
|
+
};
|
|
3184
|
+
const runModelPickerCommand = async (params) => {
|
|
3185
|
+
const to = params.kind === "direct" ? `user:${params.senderId}` : `channel:${params.channelId}`;
|
|
3186
|
+
const fromLabel = params.kind === "direct" ? `Mattermost DM from ${params.senderName}` : `Mattermost message in ${params.roomLabel} from ${params.senderName}`;
|
|
3187
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
3188
|
+
Body: params.commandText,
|
|
3189
|
+
BodyForAgent: params.commandText,
|
|
3190
|
+
RawBody: params.commandText,
|
|
3191
|
+
CommandBody: params.commandText,
|
|
3192
|
+
From: params.kind === "direct" ? `mattermost:${params.senderId}` : params.kind === "group" ? `mattermost:group:${params.channelId}` : `mattermost:channel:${params.channelId}`,
|
|
3193
|
+
To: to,
|
|
3194
|
+
SessionKey: params.sessionKey,
|
|
3195
|
+
ParentSessionKey: params.parentSessionKey,
|
|
3196
|
+
AccountId: params.route.accountId,
|
|
3197
|
+
ChatType: params.chatType,
|
|
3198
|
+
ConversationLabel: fromLabel,
|
|
3199
|
+
GroupSubject: params.kind !== "direct" ? params.channelDisplay || params.roomLabel : void 0,
|
|
3200
|
+
GroupChannel: params.channelName ? `#${params.channelName}` : void 0,
|
|
3201
|
+
GroupSpace: params.teamId,
|
|
3202
|
+
SenderName: params.senderName,
|
|
3203
|
+
SenderId: params.senderId,
|
|
3204
|
+
Provider: "mattermost",
|
|
3205
|
+
Surface: "mattermost",
|
|
3206
|
+
MessageSid: `interaction:${params.postId}:${Date.now()}`,
|
|
3207
|
+
ReplyToId: params.effectiveReplyToId,
|
|
3208
|
+
MessageThreadId: params.effectiveReplyToId,
|
|
3209
|
+
Timestamp: Date.now(),
|
|
3210
|
+
WasMentioned: true,
|
|
3211
|
+
CommandAuthorized: params.commandAuthorized,
|
|
3212
|
+
CommandSource: "native",
|
|
3213
|
+
OriginatingChannel: "mattermost",
|
|
3214
|
+
OriginatingTo: to
|
|
3215
|
+
});
|
|
3216
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
3217
|
+
cfg,
|
|
3218
|
+
channel: "mattermost",
|
|
3219
|
+
accountId: account.accountId
|
|
3220
|
+
});
|
|
3221
|
+
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
|
|
3222
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
3223
|
+
cfg,
|
|
3224
|
+
agentId: params.route.agentId,
|
|
3225
|
+
channel: "mattermost",
|
|
3226
|
+
accountId: account.accountId
|
|
3227
|
+
});
|
|
3228
|
+
const shouldDeliverReplies = params.deliverReplies === true;
|
|
3229
|
+
const capturedTexts = [];
|
|
3230
|
+
const typingCallbacks = shouldDeliverReplies ? createTypingCallbacks({
|
|
3231
|
+
start: () => sendTypingIndicator(params.channelId, params.effectiveReplyToId),
|
|
3232
|
+
onStartError: (err) => {
|
|
3233
|
+
logTypingFailure({
|
|
3234
|
+
log: (message) => logger.debug?.(message),
|
|
3235
|
+
channel: "mattermost",
|
|
3236
|
+
target: params.channelId,
|
|
3237
|
+
error: err
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
}) : void 0;
|
|
3241
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
3242
|
+
...prefixOptions,
|
|
3243
|
+
deliver: async (payload) => {
|
|
3244
|
+
const trimmedPayload = {
|
|
3245
|
+
...payload,
|
|
3246
|
+
text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode).trim()
|
|
3247
|
+
};
|
|
3248
|
+
if (!shouldDeliverReplies) {
|
|
3249
|
+
if (trimmedPayload.text) capturedTexts.push(trimmedPayload.text);
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
await deliverMattermostReplyPayload({
|
|
3253
|
+
core,
|
|
3254
|
+
cfg,
|
|
3255
|
+
payload: trimmedPayload,
|
|
3256
|
+
to,
|
|
3257
|
+
accountId: account.accountId,
|
|
3258
|
+
agentId: params.route.agentId,
|
|
3259
|
+
replyToId: resolveMattermostReplyRootId({
|
|
3260
|
+
threadRootId: params.effectiveReplyToId,
|
|
3261
|
+
replyToId: trimmedPayload.replyToId
|
|
3262
|
+
}),
|
|
3263
|
+
textLimit,
|
|
3264
|
+
tableMode: "off",
|
|
3265
|
+
sendMessage: sendMessageMattermost
|
|
3266
|
+
});
|
|
3267
|
+
},
|
|
3268
|
+
onError: (err, info) => {
|
|
3269
|
+
runtime.error?.(`mattermost model picker ${info.kind} reply failed: ${String(err)}`);
|
|
3270
|
+
},
|
|
3271
|
+
onReplyStart: typingCallbacks?.onReplyStart
|
|
3272
|
+
});
|
|
3273
|
+
await core.channel.reply.withReplyDispatcher({
|
|
3274
|
+
dispatcher,
|
|
3275
|
+
onSettled: () => {
|
|
3276
|
+
markDispatchIdle();
|
|
3277
|
+
},
|
|
3278
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
3279
|
+
ctx: ctxPayload,
|
|
3280
|
+
cfg,
|
|
3281
|
+
dispatcher,
|
|
3282
|
+
replyOptions: {
|
|
3283
|
+
...replyOptions,
|
|
3284
|
+
disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
|
|
3285
|
+
onModelSelected
|
|
3286
|
+
}
|
|
3287
|
+
})
|
|
3288
|
+
});
|
|
3289
|
+
return capturedTexts.join("\n\n").trim();
|
|
3290
|
+
};
|
|
3291
|
+
async function handleModelPickerInteraction(params) {
|
|
3292
|
+
const pickerState = parseMattermostModelPickerContext(params.context);
|
|
3293
|
+
if (!pickerState) return null;
|
|
3294
|
+
if (pickerState.ownerUserId !== params.payload.user_id) return { ephemeral_text: "Only the person who opened this picker can use it." };
|
|
3295
|
+
const channelInfo = await resolveChannelInfo(params.payload.channel_id);
|
|
3296
|
+
const pickerCommandText = pickerState.action === "select" ? `/model ${pickerState.provider}/${pickerState.model}` : pickerState.action === "list" ? `/models ${pickerState.provider}` : "/models";
|
|
3297
|
+
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
3298
|
+
cfg,
|
|
3299
|
+
surface: "mattermost"
|
|
3300
|
+
});
|
|
3301
|
+
const hasControlCommand = core.channel.text.hasControlCommand(pickerCommandText, cfg);
|
|
3302
|
+
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
3303
|
+
const storeAllowFrom = normalizeMattermostAllowList(await readStoreAllowFromForDmPolicy({
|
|
3304
|
+
provider: "mattermost",
|
|
3305
|
+
accountId: account.accountId,
|
|
3306
|
+
dmPolicy,
|
|
3307
|
+
readStore: pairing.readStoreForDmPolicy
|
|
3308
|
+
}));
|
|
3309
|
+
const auth = authorizeMattermostCommandInvocation({
|
|
3310
|
+
account,
|
|
3311
|
+
cfg,
|
|
3312
|
+
senderId: params.payload.user_id,
|
|
3313
|
+
senderName: params.userName,
|
|
3314
|
+
channelId: params.payload.channel_id,
|
|
3315
|
+
channelInfo,
|
|
3316
|
+
storeAllowFrom,
|
|
3317
|
+
allowTextCommands,
|
|
3318
|
+
hasControlCommand
|
|
3319
|
+
});
|
|
3320
|
+
if (!auth.ok) {
|
|
3321
|
+
if (auth.denyReason === "dm-pairing") {
|
|
3322
|
+
const { code } = await pairing.upsertPairingRequest({
|
|
3323
|
+
id: params.payload.user_id,
|
|
3324
|
+
meta: { name: params.userName }
|
|
3325
|
+
});
|
|
3326
|
+
return { ephemeral_text: core.channel.pairing.buildPairingReply({
|
|
3327
|
+
channel: "mattermost",
|
|
3328
|
+
idLine: `Your Mattermost user id: ${params.payload.user_id}`,
|
|
3329
|
+
code
|
|
3330
|
+
}) };
|
|
3331
|
+
}
|
|
3332
|
+
return { ephemeral_text: auth.denyReason === "unknown-channel" ? "Temporary error: unable to determine channel type. Please try again." : auth.denyReason === "dm-disabled" ? "This bot is not accepting direct messages." : auth.denyReason === "channels-disabled" ? "Model picker actions are disabled in channels." : auth.denyReason === "channel-no-allowlist" ? "Model picker actions are not configured for this channel." : "Unauthorized." };
|
|
3333
|
+
}
|
|
3334
|
+
const kind = auth.kind;
|
|
3335
|
+
const chatType = auth.chatType;
|
|
3336
|
+
const teamId = auth.channelInfo.team_id ?? params.payload.team_id ?? void 0;
|
|
3337
|
+
const channelName = auth.channelName || void 0;
|
|
3338
|
+
const channelDisplay = auth.channelDisplay || auth.channelName || params.payload.channel_id;
|
|
3339
|
+
const roomLabel = auth.roomLabel;
|
|
3340
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
3341
|
+
cfg,
|
|
3342
|
+
channel: "mattermost",
|
|
3343
|
+
accountId: account.accountId,
|
|
3344
|
+
teamId,
|
|
3345
|
+
peer: {
|
|
3346
|
+
kind,
|
|
3347
|
+
id: kind === "direct" ? params.payload.user_id : params.payload.channel_id
|
|
3348
|
+
}
|
|
3349
|
+
});
|
|
3350
|
+
const replyToMode = resolveMattermostReplyToMode(account, kind);
|
|
3351
|
+
const threadContext = resolveMattermostThreadSessionContext({
|
|
3352
|
+
baseSessionKey: route.sessionKey,
|
|
3353
|
+
kind,
|
|
3354
|
+
postId: params.post.id || params.payload.post_id,
|
|
3355
|
+
replyToMode,
|
|
3356
|
+
threadRootId: params.post.root_id
|
|
3357
|
+
});
|
|
3358
|
+
const modelSessionRoute = {
|
|
3359
|
+
agentId: route.agentId,
|
|
3360
|
+
sessionKey: threadContext.sessionKey
|
|
3361
|
+
};
|
|
3362
|
+
const data = await buildModelsProviderData(cfg, route.agentId);
|
|
3363
|
+
if (data.providers.length === 0) return await updateModelPickerPost({
|
|
3364
|
+
channelId: params.payload.channel_id,
|
|
3365
|
+
postId: params.payload.post_id,
|
|
3366
|
+
message: "No models available."
|
|
3367
|
+
});
|
|
3368
|
+
if (pickerState.action === "providers" || pickerState.action === "back") {
|
|
3369
|
+
const currentModel = resolveMattermostModelPickerCurrentModel({
|
|
3370
|
+
cfg,
|
|
3371
|
+
route: modelSessionRoute,
|
|
3372
|
+
data
|
|
3373
|
+
});
|
|
3374
|
+
const view = renderMattermostProviderPickerView({
|
|
3375
|
+
ownerUserId: pickerState.ownerUserId,
|
|
3376
|
+
data,
|
|
3377
|
+
currentModel
|
|
3378
|
+
});
|
|
3379
|
+
return await updateModelPickerPost({
|
|
3380
|
+
channelId: params.payload.channel_id,
|
|
3381
|
+
postId: params.payload.post_id,
|
|
3382
|
+
message: view.text,
|
|
3383
|
+
buttons: view.buttons
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
if (pickerState.action === "list") {
|
|
3387
|
+
const currentModel = resolveMattermostModelPickerCurrentModel({
|
|
3388
|
+
cfg,
|
|
3389
|
+
route: modelSessionRoute,
|
|
3390
|
+
data
|
|
3391
|
+
});
|
|
3392
|
+
const view = renderMattermostModelsPickerView({
|
|
3393
|
+
ownerUserId: pickerState.ownerUserId,
|
|
3394
|
+
data,
|
|
3395
|
+
provider: pickerState.provider,
|
|
3396
|
+
page: pickerState.page,
|
|
3397
|
+
currentModel
|
|
3398
|
+
});
|
|
3399
|
+
return await updateModelPickerPost({
|
|
3400
|
+
channelId: params.payload.channel_id,
|
|
3401
|
+
postId: params.payload.post_id,
|
|
3402
|
+
message: view.text,
|
|
3403
|
+
buttons: view.buttons
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
const targetModelRef = `${pickerState.provider}/${pickerState.model}`;
|
|
3407
|
+
if (!buildMattermostAllowedModelRefs(data).has(targetModelRef)) return { ephemeral_text: `That model is no longer available: ${targetModelRef}` };
|
|
3408
|
+
(async () => {
|
|
3409
|
+
try {
|
|
3410
|
+
await runModelPickerCommand({
|
|
3411
|
+
commandText: `/model ${targetModelRef}`,
|
|
3412
|
+
commandAuthorized: auth.commandAuthorized,
|
|
3413
|
+
route,
|
|
3414
|
+
sessionKey: threadContext.sessionKey,
|
|
3415
|
+
parentSessionKey: threadContext.parentSessionKey,
|
|
3416
|
+
channelId: params.payload.channel_id,
|
|
3417
|
+
senderId: params.payload.user_id,
|
|
3418
|
+
senderName: params.userName,
|
|
3419
|
+
kind,
|
|
3420
|
+
chatType,
|
|
3421
|
+
channelName,
|
|
3422
|
+
channelDisplay,
|
|
3423
|
+
roomLabel,
|
|
3424
|
+
teamId,
|
|
3425
|
+
postId: params.payload.post_id,
|
|
3426
|
+
effectiveReplyToId: threadContext.effectiveReplyToId,
|
|
3427
|
+
deliverReplies: true
|
|
3428
|
+
});
|
|
3429
|
+
const updatedModel = resolveMattermostModelPickerCurrentModel({
|
|
3430
|
+
cfg,
|
|
3431
|
+
route: modelSessionRoute,
|
|
3432
|
+
data,
|
|
3433
|
+
skipCache: true
|
|
3434
|
+
});
|
|
3435
|
+
const view = renderMattermostModelsPickerView({
|
|
3436
|
+
ownerUserId: pickerState.ownerUserId,
|
|
3437
|
+
data,
|
|
3438
|
+
provider: pickerState.provider,
|
|
3439
|
+
page: pickerState.page,
|
|
3440
|
+
currentModel: updatedModel
|
|
3441
|
+
});
|
|
3442
|
+
await updateModelPickerPost({
|
|
3443
|
+
channelId: params.payload.channel_id,
|
|
3444
|
+
postId: params.payload.post_id,
|
|
3445
|
+
message: view.text,
|
|
3446
|
+
buttons: view.buttons
|
|
3447
|
+
});
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
runtime.error?.(`mattermost model picker select failed: ${String(err)}`);
|
|
3450
|
+
}
|
|
3451
|
+
})();
|
|
3452
|
+
return {};
|
|
3453
|
+
}
|
|
3454
|
+
const handlePost = async (post, payload, messageIds) => {
|
|
3455
|
+
const channelId = post.channel_id ?? payload.data?.channel_id ?? payload.broadcast?.channel_id;
|
|
3456
|
+
if (!channelId) {
|
|
3457
|
+
logVerboseMessage("mattermost: drop post (missing channel id)");
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
const allMessageIds = messageIds?.length ? messageIds : post.id ? [post.id] : [];
|
|
3461
|
+
if (allMessageIds.length === 0) {
|
|
3462
|
+
logVerboseMessage("mattermost: drop post (missing message id)");
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
const dedupeEntries = allMessageIds.map((id) => recentInboundMessages.check(`${account.accountId}:${id}`));
|
|
3466
|
+
if (dedupeEntries.length > 0 && dedupeEntries.every(Boolean)) {
|
|
3467
|
+
logVerboseMessage(`mattermost: drop post (dedupe account=${account.accountId} ids=${allMessageIds.length})`);
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
const senderId = post.user_id ?? payload.broadcast?.user_id;
|
|
3471
|
+
if (!senderId) {
|
|
3472
|
+
logVerboseMessage("mattermost: drop post (missing sender id)");
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
if (senderId === botUserId) {
|
|
3476
|
+
logVerboseMessage(`mattermost: drop post (self sender=${senderId})`);
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
if (isSystemPost(post)) {
|
|
3480
|
+
logVerboseMessage(`mattermost: drop post (system post type=${post.type ?? "unknown"})`);
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
const channelInfo = await resolveChannelInfo(channelId);
|
|
3484
|
+
const kind = mapMattermostChannelTypeToChatType(payload.data?.channel_type ?? channelInfo?.type ?? void 0);
|
|
3485
|
+
const chatType = channelChatType(kind);
|
|
3486
|
+
const senderName = payload.data?.sender_name?.trim() || (await resolveUserInfo(senderId))?.username?.trim() || senderId;
|
|
3487
|
+
const rawText = post.message?.trim() || "";
|
|
3488
|
+
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
3489
|
+
const normalizedAllowFrom = normalizeMattermostAllowList(account.config.allowFrom ?? []);
|
|
3490
|
+
const normalizedGroupAllowFrom = normalizeMattermostAllowList(account.config.groupAllowFrom ?? []);
|
|
3491
|
+
const storeAllowFrom = normalizeMattermostAllowList(await readStoreAllowFromForDmPolicy({
|
|
3492
|
+
provider: "mattermost",
|
|
3493
|
+
accountId: account.accountId,
|
|
3494
|
+
dmPolicy,
|
|
3495
|
+
readStore: pairing.readStoreForDmPolicy
|
|
3496
|
+
}));
|
|
3497
|
+
const accessDecision = resolveDmGroupAccessWithLists({
|
|
3498
|
+
isGroup: kind !== "direct",
|
|
3499
|
+
dmPolicy,
|
|
3500
|
+
groupPolicy,
|
|
3501
|
+
allowFrom: normalizedAllowFrom,
|
|
3502
|
+
groupAllowFrom: normalizedGroupAllowFrom,
|
|
3503
|
+
storeAllowFrom,
|
|
3504
|
+
isSenderAllowed: (allowFrom) => isMattermostSenderAllowed({
|
|
3505
|
+
senderId,
|
|
3506
|
+
senderName,
|
|
3507
|
+
allowFrom,
|
|
3508
|
+
allowNameMatching
|
|
3509
|
+
})
|
|
3510
|
+
});
|
|
3511
|
+
const effectiveAllowFrom = accessDecision.effectiveAllowFrom;
|
|
3512
|
+
const effectiveGroupAllowFrom = accessDecision.effectiveGroupAllowFrom;
|
|
3513
|
+
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
3514
|
+
cfg,
|
|
3515
|
+
surface: "mattermost"
|
|
3516
|
+
});
|
|
3517
|
+
const hasControlCommand = core.channel.text.hasControlCommand(rawText, cfg);
|
|
3518
|
+
const isControlCommand = allowTextCommands && hasControlCommand;
|
|
3519
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
3520
|
+
const commandDmAllowFrom = kind === "direct" ? effectiveAllowFrom : normalizedAllowFrom;
|
|
3521
|
+
const senderAllowedForCommands = isMattermostSenderAllowed({
|
|
3522
|
+
senderId,
|
|
3523
|
+
senderName,
|
|
3524
|
+
allowFrom: commandDmAllowFrom,
|
|
3525
|
+
allowNameMatching
|
|
3526
|
+
});
|
|
3527
|
+
const groupAllowedForCommands = isMattermostSenderAllowed({
|
|
3528
|
+
senderId,
|
|
3529
|
+
senderName,
|
|
3530
|
+
allowFrom: effectiveGroupAllowFrom,
|
|
3531
|
+
allowNameMatching
|
|
3532
|
+
});
|
|
3533
|
+
const commandGate = resolveControlCommandGate({
|
|
3534
|
+
useAccessGroups,
|
|
3535
|
+
authorizers: [{
|
|
3536
|
+
configured: commandDmAllowFrom.length > 0,
|
|
3537
|
+
allowed: senderAllowedForCommands
|
|
3538
|
+
}, {
|
|
3539
|
+
configured: effectiveGroupAllowFrom.length > 0,
|
|
3540
|
+
allowed: groupAllowedForCommands
|
|
3541
|
+
}],
|
|
3542
|
+
allowTextCommands,
|
|
3543
|
+
hasControlCommand
|
|
3544
|
+
});
|
|
3545
|
+
const commandAuthorized = commandGate.commandAuthorized;
|
|
3546
|
+
if (accessDecision.decision !== "allow") {
|
|
3547
|
+
if (kind === "direct") {
|
|
3548
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED) {
|
|
3549
|
+
logVerboseMessage(`mattermost: drop dm (dmPolicy=disabled sender=${senderId})`);
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
if (accessDecision.decision === "pairing") {
|
|
3553
|
+
const { code, created } = await pairing.upsertPairingRequest({
|
|
3554
|
+
id: senderId,
|
|
3555
|
+
meta: { name: senderName }
|
|
3556
|
+
});
|
|
3557
|
+
logVerboseMessage(`mattermost: pairing request sender=${senderId} created=${created}`);
|
|
3558
|
+
if (created) try {
|
|
3559
|
+
await sendMessageMattermost(`user:${senderId}`, core.channel.pairing.buildPairingReply({
|
|
3560
|
+
channel: "mattermost",
|
|
3561
|
+
idLine: `Your Mattermost user id: ${senderId}`,
|
|
3562
|
+
code
|
|
3563
|
+
}), { accountId: account.accountId });
|
|
3564
|
+
opts.statusSink?.({ lastOutboundAt: Date.now() });
|
|
3565
|
+
} catch (err) {
|
|
3566
|
+
logVerboseMessage(`mattermost: pairing reply failed for ${senderId}: ${String(err)}`);
|
|
3567
|
+
}
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
logVerboseMessage(`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`);
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_DISABLED) {
|
|
3574
|
+
logVerboseMessage("mattermost: drop group message (groupPolicy=disabled)");
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST) {
|
|
3578
|
+
logVerboseMessage("mattermost: drop group message (no group allowlist)");
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED) {
|
|
3582
|
+
logVerboseMessage(`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`);
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
logVerboseMessage(`mattermost: drop group message (groupPolicy=${groupPolicy} reason=${accessDecision.reason})`);
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
if (kind !== "direct" && commandGate.shouldBlock) {
|
|
3589
|
+
logInboundDrop({
|
|
3590
|
+
log: logVerboseMessage,
|
|
3591
|
+
channel: "mattermost",
|
|
3592
|
+
reason: "control command (unauthorized)",
|
|
3593
|
+
target: senderId
|
|
3594
|
+
});
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
const teamId = payload.data?.team_id ?? channelInfo?.team_id ?? void 0;
|
|
3598
|
+
const channelName = payload.data?.channel_name ?? channelInfo?.name ?? "";
|
|
3599
|
+
const channelDisplay = payload.data?.channel_display_name ?? channelInfo?.display_name ?? channelName;
|
|
3600
|
+
const roomLabel = channelName ? `#${channelName}` : channelDisplay || `#${channelId}`;
|
|
3601
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
3602
|
+
cfg,
|
|
3603
|
+
channel: "mattermost",
|
|
3604
|
+
accountId: account.accountId,
|
|
3605
|
+
teamId,
|
|
3606
|
+
peer: {
|
|
3607
|
+
kind,
|
|
3608
|
+
id: kind === "direct" ? senderId : channelId
|
|
3609
|
+
}
|
|
3610
|
+
});
|
|
3611
|
+
const baseSessionKey = route.sessionKey;
|
|
3612
|
+
const threadRootId = post.root_id?.trim() || void 0;
|
|
3613
|
+
const replyToMode = resolveMattermostReplyToMode(account, kind);
|
|
3614
|
+
const { effectiveReplyToId, sessionKey, parentSessionKey } = resolveMattermostThreadSessionContext({
|
|
3615
|
+
baseSessionKey,
|
|
3616
|
+
kind,
|
|
3617
|
+
postId: post.id,
|
|
3618
|
+
replyToMode,
|
|
3619
|
+
threadRootId
|
|
3620
|
+
});
|
|
3621
|
+
const historyKey = kind === "direct" ? null : sessionKey;
|
|
3622
|
+
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId);
|
|
3623
|
+
const wasMentioned = kind !== "direct" && ((botUsername ? rawText.toLowerCase().includes(`@${botUsername.toLowerCase()}`) : false) || core.channel.mentions.matchesMentionPatterns(rawText, mentionRegexes));
|
|
3624
|
+
const pendingBody = rawText || (post.file_ids?.length ? `[Mattermost ${post.file_ids.length === 1 ? "file" : "files"}]` : "");
|
|
3625
|
+
const pendingSender = senderName;
|
|
3626
|
+
const recordPendingHistory = () => {
|
|
3627
|
+
const trimmed = pendingBody.trim();
|
|
3628
|
+
recordPendingHistoryEntryIfEnabled({
|
|
3629
|
+
historyMap: channelHistories,
|
|
3630
|
+
limit: historyLimit,
|
|
3631
|
+
historyKey: historyKey ?? "",
|
|
3632
|
+
entry: historyKey && trimmed ? {
|
|
3633
|
+
sender: pendingSender,
|
|
3634
|
+
body: trimmed,
|
|
3635
|
+
timestamp: typeof post.create_at === "number" ? post.create_at : void 0,
|
|
3636
|
+
messageId: post.id ?? void 0
|
|
3637
|
+
} : null
|
|
3638
|
+
});
|
|
3639
|
+
};
|
|
3640
|
+
const oncharEnabled = account.chatmode === "onchar" && kind !== "direct";
|
|
3641
|
+
const oncharPrefixes = oncharEnabled ? resolveOncharPrefixes(account.oncharPrefixes) : [];
|
|
3642
|
+
const oncharResult = oncharEnabled ? stripOncharPrefix(rawText, oncharPrefixes) : {
|
|
3643
|
+
triggered: false,
|
|
3644
|
+
stripped: rawText
|
|
3645
|
+
};
|
|
3646
|
+
const oncharTriggered = oncharResult.triggered;
|
|
3647
|
+
const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
|
|
3648
|
+
const mentionDecision = evaluateMattermostMentionGate({
|
|
3649
|
+
kind,
|
|
3650
|
+
cfg,
|
|
3651
|
+
accountId: account.accountId,
|
|
3652
|
+
channelId,
|
|
3653
|
+
threadRootId,
|
|
3654
|
+
requireMentionOverride: account.requireMention,
|
|
3655
|
+
resolveRequireMention: core.channel.groups.resolveRequireMention,
|
|
3656
|
+
wasMentioned,
|
|
3657
|
+
isControlCommand,
|
|
3658
|
+
commandAuthorized,
|
|
3659
|
+
oncharEnabled,
|
|
3660
|
+
oncharTriggered,
|
|
3661
|
+
canDetectMention
|
|
3662
|
+
});
|
|
3663
|
+
const { shouldRequireMention, shouldBypassMention } = mentionDecision;
|
|
3664
|
+
if (mentionDecision.dropReason === "onchar-not-triggered") {
|
|
3665
|
+
logVerboseMessage(`mattermost: drop group message (onchar not triggered channel=${channelId} sender=${senderId})`);
|
|
3666
|
+
recordPendingHistory();
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
if (mentionDecision.dropReason === "missing-mention") {
|
|
3670
|
+
logVerboseMessage(`mattermost: drop group message (missing mention channel=${channelId} sender=${senderId} requireMention=${shouldRequireMention} bypass=${shouldBypassMention} canDetectMention=${canDetectMention})`);
|
|
3671
|
+
recordPendingHistory();
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
const mediaList = await resolveMattermostMedia(post.file_ids);
|
|
3675
|
+
const mediaPlaceholder = buildMattermostAttachmentPlaceholder(mediaList);
|
|
3676
|
+
const bodyText = normalizeMention([oncharTriggered ? oncharResult.stripped : rawText, mediaPlaceholder].filter(Boolean).join("\n").trim(), botUsername);
|
|
3677
|
+
if (!bodyText) {
|
|
3678
|
+
logVerboseMessage(`mattermost: drop group message (empty body after normalization channel=${channelId} sender=${senderId})`);
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
core.channel.activity.record({
|
|
3682
|
+
channel: "mattermost",
|
|
3683
|
+
accountId: account.accountId,
|
|
3684
|
+
direction: "inbound"
|
|
3685
|
+
});
|
|
3686
|
+
const fromLabel = formatInboundFromLabel({
|
|
3687
|
+
isGroup: kind !== "direct",
|
|
3688
|
+
groupLabel: channelDisplay || roomLabel,
|
|
3689
|
+
groupId: channelId,
|
|
3690
|
+
groupFallback: roomLabel || "Channel",
|
|
3691
|
+
directLabel: senderName,
|
|
3692
|
+
directId: senderId
|
|
3693
|
+
});
|
|
3694
|
+
const preview = bodyText.replace(/\s+/g, " ").slice(0, 160);
|
|
3695
|
+
const inboundLabel = kind === "direct" ? `Mattermost DM from ${senderName}` : `Mattermost message in ${roomLabel} from ${senderName}`;
|
|
3696
|
+
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
|
3697
|
+
sessionKey,
|
|
3698
|
+
contextKey: `mattermost:message:${channelId}:${post.id ?? "unknown"}`
|
|
3699
|
+
});
|
|
3700
|
+
const textWithId = `${bodyText}\n[mattermost message id: ${post.id ?? "unknown"} channel: ${channelId}]`;
|
|
3701
|
+
let combinedBody = core.channel.reply.formatInboundEnvelope({
|
|
3702
|
+
channel: "Mattermost",
|
|
3703
|
+
from: fromLabel,
|
|
3704
|
+
timestamp: typeof post.create_at === "number" ? post.create_at : void 0,
|
|
3705
|
+
body: textWithId,
|
|
3706
|
+
chatType,
|
|
3707
|
+
sender: {
|
|
3708
|
+
name: senderName,
|
|
3709
|
+
id: senderId
|
|
3710
|
+
}
|
|
3711
|
+
});
|
|
3712
|
+
if (historyKey) combinedBody = buildPendingHistoryContextFromMap({
|
|
3713
|
+
historyMap: channelHistories,
|
|
3714
|
+
historyKey,
|
|
3715
|
+
limit: historyLimit,
|
|
3716
|
+
currentMessage: combinedBody,
|
|
3717
|
+
formatEntry: (entry) => core.channel.reply.formatInboundEnvelope({
|
|
3718
|
+
channel: "Mattermost",
|
|
3719
|
+
from: fromLabel,
|
|
3720
|
+
timestamp: entry.timestamp,
|
|
3721
|
+
body: `${entry.body}${entry.messageId ? ` [id:${entry.messageId} channel:${channelId}]` : ""}`,
|
|
3722
|
+
chatType,
|
|
3723
|
+
senderLabel: entry.sender
|
|
3724
|
+
})
|
|
3725
|
+
});
|
|
3726
|
+
const to = kind === "direct" ? `user:${senderId}` : `channel:${channelId}`;
|
|
3727
|
+
const mediaPayload = buildAgentMediaPayload(mediaList);
|
|
3728
|
+
const commandBody = rawText.trim();
|
|
3729
|
+
const inboundHistory = historyKey && historyLimit > 0 ? (channelHistories.get(historyKey) ?? []).map((entry) => ({
|
|
3730
|
+
sender: entry.sender,
|
|
3731
|
+
body: entry.body,
|
|
3732
|
+
timestamp: entry.timestamp
|
|
3733
|
+
})) : void 0;
|
|
3734
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
3735
|
+
Body: combinedBody,
|
|
3736
|
+
BodyForAgent: bodyText,
|
|
3737
|
+
InboundHistory: inboundHistory,
|
|
3738
|
+
RawBody: bodyText,
|
|
3739
|
+
CommandBody: commandBody,
|
|
3740
|
+
BodyForCommands: commandBody,
|
|
3741
|
+
From: kind === "direct" ? `mattermost:${senderId}` : kind === "group" ? `mattermost:group:${channelId}` : `mattermost:channel:${channelId}`,
|
|
3742
|
+
To: to,
|
|
3743
|
+
SessionKey: sessionKey,
|
|
3744
|
+
ParentSessionKey: parentSessionKey,
|
|
3745
|
+
AccountId: route.accountId,
|
|
3746
|
+
ChatType: chatType,
|
|
3747
|
+
ConversationLabel: fromLabel,
|
|
3748
|
+
GroupSubject: kind !== "direct" ? channelDisplay || roomLabel : void 0,
|
|
3749
|
+
GroupChannel: channelName ? `#${channelName}` : void 0,
|
|
3750
|
+
GroupSpace: teamId,
|
|
3751
|
+
SenderName: senderName,
|
|
3752
|
+
SenderId: senderId,
|
|
3753
|
+
Provider: "mattermost",
|
|
3754
|
+
Surface: "mattermost",
|
|
3755
|
+
MessageSid: post.id ?? void 0,
|
|
3756
|
+
MessageSids: allMessageIds.length > 1 ? allMessageIds : void 0,
|
|
3757
|
+
MessageSidFirst: allMessageIds.length > 1 ? allMessageIds[0] : void 0,
|
|
3758
|
+
MessageSidLast: allMessageIds.length > 1 ? allMessageIds[allMessageIds.length - 1] : void 0,
|
|
3759
|
+
ReplyToId: effectiveReplyToId,
|
|
3760
|
+
MessageThreadId: effectiveReplyToId,
|
|
3761
|
+
Timestamp: typeof post.create_at === "number" ? post.create_at : void 0,
|
|
3762
|
+
WasMentioned: kind !== "direct" ? mentionDecision.effectiveWasMentioned : void 0,
|
|
3763
|
+
CommandAuthorized: commandAuthorized,
|
|
3764
|
+
OriginatingChannel: "mattermost",
|
|
3765
|
+
OriginatingTo: to,
|
|
3766
|
+
...mediaPayload
|
|
3767
|
+
});
|
|
3768
|
+
if (kind === "direct") {
|
|
3769
|
+
const sessionCfg = cfg.session;
|
|
3770
|
+
const storePath = core.channel.session.resolveStorePath(sessionCfg?.store, { agentId: route.agentId });
|
|
3771
|
+
await core.channel.session.updateLastRoute({
|
|
3772
|
+
storePath,
|
|
3773
|
+
sessionKey: route.mainSessionKey,
|
|
3774
|
+
deliveryContext: {
|
|
3775
|
+
channel: "mattermost",
|
|
3776
|
+
to,
|
|
3777
|
+
accountId: route.accountId
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
const previewLine = bodyText.slice(0, 200).replace(/\n/g, "\\n");
|
|
3782
|
+
logVerboseMessage(`mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`);
|
|
3783
|
+
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
|
|
3784
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
3785
|
+
cfg,
|
|
3786
|
+
channel: "mattermost",
|
|
3787
|
+
accountId: account.accountId
|
|
3788
|
+
});
|
|
3789
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
3790
|
+
cfg,
|
|
3791
|
+
agentId: route.agentId,
|
|
3792
|
+
channel: "mattermost",
|
|
3793
|
+
accountId: account.accountId
|
|
3794
|
+
});
|
|
3795
|
+
const typingCallbacks = createTypingCallbacks({
|
|
3796
|
+
start: () => sendTypingIndicator(channelId, effectiveReplyToId),
|
|
3797
|
+
onStartError: (err) => {
|
|
3798
|
+
logTypingFailure({
|
|
3799
|
+
log: (message) => logger.debug?.(message),
|
|
3800
|
+
channel: "mattermost",
|
|
3801
|
+
target: channelId,
|
|
3802
|
+
error: err
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
3807
|
+
...prefixOptions,
|
|
3808
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
3809
|
+
typingCallbacks,
|
|
3810
|
+
deliver: async (payload) => {
|
|
3811
|
+
await deliverMattermostReplyPayload({
|
|
3812
|
+
core,
|
|
3813
|
+
cfg,
|
|
3814
|
+
payload,
|
|
3815
|
+
to,
|
|
3816
|
+
accountId: account.accountId,
|
|
3817
|
+
agentId: route.agentId,
|
|
3818
|
+
replyToId: resolveMattermostReplyRootId({
|
|
3819
|
+
threadRootId: effectiveReplyToId,
|
|
3820
|
+
replyToId: payload.replyToId
|
|
3821
|
+
}),
|
|
3822
|
+
textLimit,
|
|
3823
|
+
tableMode,
|
|
3824
|
+
sendMessage: sendMessageMattermost
|
|
3825
|
+
});
|
|
3826
|
+
runtime.log?.(`delivered reply to ${to}`);
|
|
3827
|
+
},
|
|
3828
|
+
onError: (err, info) => {
|
|
3829
|
+
runtime.error?.(`mattermost ${info.kind} reply failed: ${String(err)}`);
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
await core.channel.reply.withReplyDispatcher({
|
|
3833
|
+
dispatcher,
|
|
3834
|
+
onSettled: () => {
|
|
3835
|
+
markDispatchIdle();
|
|
3836
|
+
},
|
|
3837
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
3838
|
+
ctx: ctxPayload,
|
|
3839
|
+
cfg,
|
|
3840
|
+
dispatcher,
|
|
3841
|
+
replyOptions: {
|
|
3842
|
+
...replyOptions,
|
|
3843
|
+
disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
|
|
3844
|
+
onModelSelected
|
|
3845
|
+
}
|
|
3846
|
+
})
|
|
3847
|
+
});
|
|
3848
|
+
if (historyKey) clearHistoryEntriesIfEnabled({
|
|
3849
|
+
historyMap: channelHistories,
|
|
3850
|
+
historyKey,
|
|
3851
|
+
limit: historyLimit
|
|
3852
|
+
});
|
|
3853
|
+
};
|
|
3854
|
+
const handleReactionEvent = async (payload) => {
|
|
3855
|
+
const reactionData = payload.data?.reaction;
|
|
3856
|
+
if (!reactionData) return;
|
|
3857
|
+
let reaction = null;
|
|
3858
|
+
if (typeof reactionData === "string") try {
|
|
3859
|
+
reaction = JSON.parse(reactionData);
|
|
3860
|
+
} catch {
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
else if (typeof reactionData === "object") reaction = reactionData;
|
|
3864
|
+
if (!reaction) return;
|
|
3865
|
+
const userId = reaction.user_id?.trim();
|
|
3866
|
+
const postId = reaction.post_id?.trim();
|
|
3867
|
+
const emojiName = reaction.emoji_name?.trim();
|
|
3868
|
+
if (!userId || !postId || !emojiName) return;
|
|
3869
|
+
if (userId === botUserId) return;
|
|
3870
|
+
const action = payload.event === "reaction_removed" ? "removed" : "added";
|
|
3871
|
+
const senderName = (await resolveUserInfo(userId))?.username?.trim() || userId;
|
|
3872
|
+
const channelId = payload.broadcast?.channel_id;
|
|
3873
|
+
if (!channelId) {
|
|
3874
|
+
logVerboseMessage(`mattermost: drop reaction (no channel_id in broadcast, cannot enforce policy)`);
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
const channelInfo = await resolveChannelInfo(channelId);
|
|
3878
|
+
if (!channelInfo?.type) {
|
|
3879
|
+
logVerboseMessage(`mattermost: drop reaction (cannot resolve channel type for ${channelId})`);
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
const kind = mapMattermostChannelTypeToChatType(channelInfo.type);
|
|
3883
|
+
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
3884
|
+
const storeAllowFrom = normalizeMattermostAllowList(await readStoreAllowFromForDmPolicy({
|
|
3885
|
+
provider: "mattermost",
|
|
3886
|
+
accountId: account.accountId,
|
|
3887
|
+
dmPolicy,
|
|
3888
|
+
readStore: pairing.readStoreForDmPolicy
|
|
3889
|
+
}));
|
|
3890
|
+
const reactionAccess = resolveDmGroupAccessWithLists({
|
|
3891
|
+
isGroup: kind !== "direct",
|
|
3892
|
+
dmPolicy,
|
|
3893
|
+
groupPolicy,
|
|
3894
|
+
allowFrom: normalizeMattermostAllowList(account.config.allowFrom ?? []),
|
|
3895
|
+
groupAllowFrom: normalizeMattermostAllowList(account.config.groupAllowFrom ?? []),
|
|
3896
|
+
storeAllowFrom,
|
|
3897
|
+
isSenderAllowed: (allowFrom) => isMattermostSenderAllowed({
|
|
3898
|
+
senderId: userId,
|
|
3899
|
+
senderName,
|
|
3900
|
+
allowFrom,
|
|
3901
|
+
allowNameMatching
|
|
3902
|
+
})
|
|
3903
|
+
});
|
|
3904
|
+
if (reactionAccess.decision !== "allow") {
|
|
3905
|
+
if (kind === "direct") logVerboseMessage(`mattermost: drop reaction (dmPolicy=${dmPolicy} sender=${userId} reason=${reactionAccess.reason})`);
|
|
3906
|
+
else logVerboseMessage(`mattermost: drop reaction (groupPolicy=${groupPolicy} sender=${userId} reason=${reactionAccess.reason} channel=${channelId})`);
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3909
|
+
const teamId = channelInfo?.team_id ?? void 0;
|
|
3910
|
+
const sessionKey = core.channel.routing.resolveAgentRoute({
|
|
3911
|
+
cfg,
|
|
3912
|
+
channel: "mattermost",
|
|
3913
|
+
accountId: account.accountId,
|
|
3914
|
+
teamId,
|
|
3915
|
+
peer: {
|
|
3916
|
+
kind,
|
|
3917
|
+
id: kind === "direct" ? userId : channelId
|
|
3918
|
+
}
|
|
3919
|
+
}).sessionKey;
|
|
3920
|
+
const eventText = `Mattermost reaction ${action}: :${emojiName}: by @${senderName} on post ${postId} in channel ${channelId}`;
|
|
3921
|
+
core.system.enqueueSystemEvent(eventText, {
|
|
3922
|
+
sessionKey,
|
|
3923
|
+
contextKey: `mattermost:reaction:${postId}:${emojiName}:${userId}:${action}`
|
|
3924
|
+
});
|
|
3925
|
+
logVerboseMessage(`mattermost reaction: ${action} :${emojiName}: by ${senderName} on ${postId}`);
|
|
3926
|
+
};
|
|
3927
|
+
const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
|
|
3928
|
+
cfg,
|
|
3929
|
+
channel: "mattermost"
|
|
3930
|
+
});
|
|
3931
|
+
const debouncer = core.channel.debounce.createInboundDebouncer({
|
|
3932
|
+
debounceMs: inboundDebounceMs,
|
|
3933
|
+
buildKey: (entry) => {
|
|
3934
|
+
const channelId = entry.post.channel_id ?? entry.payload.data?.channel_id ?? entry.payload.broadcast?.channel_id;
|
|
3935
|
+
if (!channelId) return null;
|
|
3936
|
+
const threadId = entry.post.root_id?.trim();
|
|
3937
|
+
const threadKey = threadId ? `thread:${threadId}` : "channel";
|
|
3938
|
+
return `mattermost:${account.accountId}:${channelId}:${threadKey}`;
|
|
3939
|
+
},
|
|
3940
|
+
shouldDebounce: (entry) => {
|
|
3941
|
+
if (entry.post.file_ids && entry.post.file_ids.length > 0) return false;
|
|
3942
|
+
const text = entry.post.message?.trim() ?? "";
|
|
3943
|
+
if (!text) return false;
|
|
3944
|
+
return !core.channel.text.hasControlCommand(text, cfg);
|
|
3945
|
+
},
|
|
3946
|
+
onFlush: async (entries) => {
|
|
3947
|
+
const last = entries.at(-1);
|
|
3948
|
+
if (!last) return;
|
|
3949
|
+
if (entries.length === 1) {
|
|
3950
|
+
await handlePost(last.post, last.payload);
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3953
|
+
const combinedText = entries.map((entry) => entry.post.message?.trim() ?? "").filter(Boolean).join("\n");
|
|
3954
|
+
const mergedPost = {
|
|
3955
|
+
...last.post,
|
|
3956
|
+
message: combinedText,
|
|
3957
|
+
file_ids: []
|
|
3958
|
+
};
|
|
3959
|
+
const ids = entries.map((entry) => entry.post.id).filter(Boolean);
|
|
3960
|
+
await handlePost(mergedPost, last.payload, ids.length > 0 ? ids : void 0);
|
|
3961
|
+
},
|
|
3962
|
+
onError: (err) => {
|
|
3963
|
+
runtime.error?.(`mattermost debounce flush failed: ${String(err)}`);
|
|
3964
|
+
}
|
|
3965
|
+
});
|
|
3966
|
+
const wsUrl = buildMattermostWsUrl(baseUrl);
|
|
3967
|
+
let seq = 1;
|
|
3968
|
+
const connectOnce = createMattermostConnectOnce({
|
|
3969
|
+
wsUrl,
|
|
3970
|
+
botToken,
|
|
3971
|
+
abortSignal: opts.abortSignal,
|
|
3972
|
+
statusSink: opts.statusSink,
|
|
3973
|
+
runtime,
|
|
3974
|
+
webSocketFactory: opts.webSocketFactory,
|
|
3975
|
+
nextSeq: () => seq++,
|
|
3976
|
+
onPosted: async (post, payload) => {
|
|
3977
|
+
await debouncer.enqueue({
|
|
3978
|
+
post,
|
|
3979
|
+
payload
|
|
3980
|
+
});
|
|
3981
|
+
},
|
|
3982
|
+
onReaction: async (payload) => {
|
|
3983
|
+
await handleReactionEvent(payload);
|
|
3984
|
+
}
|
|
3985
|
+
});
|
|
3986
|
+
let slashShutdownCleanup = null;
|
|
3987
|
+
if (slashEnabled) {
|
|
3988
|
+
const runAbortCleanup = () => {
|
|
3989
|
+
if (slashShutdownCleanup) return;
|
|
3990
|
+
const commands = getSlashCommandState(account.accountId)?.registeredCommands ?? [];
|
|
3991
|
+
deactivateSlashCommands(account.accountId);
|
|
3992
|
+
slashShutdownCleanup = cleanupSlashCommands({
|
|
3993
|
+
client,
|
|
3994
|
+
commands,
|
|
3995
|
+
log: (msg) => runtime.log?.(msg)
|
|
3996
|
+
}).catch((err) => {
|
|
3997
|
+
runtime.error?.(`mattermost: slash cleanup failed: ${String(err)}`);
|
|
3998
|
+
});
|
|
3999
|
+
};
|
|
4000
|
+
if (opts.abortSignal?.aborted) runAbortCleanup();
|
|
4001
|
+
else opts.abortSignal?.addEventListener("abort", runAbortCleanup, { once: true });
|
|
4002
|
+
}
|
|
4003
|
+
try {
|
|
4004
|
+
await runWithReconnect(connectOnce, {
|
|
4005
|
+
abortSignal: opts.abortSignal,
|
|
4006
|
+
jitterRatio: .2,
|
|
4007
|
+
onError: (err) => {
|
|
4008
|
+
runtime.error?.(`mattermost connection failed: ${String(err)}`);
|
|
4009
|
+
opts.statusSink?.({
|
|
4010
|
+
lastError: String(err),
|
|
4011
|
+
connected: false
|
|
4012
|
+
});
|
|
4013
|
+
},
|
|
4014
|
+
onReconnect: (delayMs) => {
|
|
4015
|
+
runtime.log?.(`mattermost reconnecting in ${Math.round(delayMs / 1e3)}s`);
|
|
4016
|
+
}
|
|
4017
|
+
});
|
|
4018
|
+
} finally {
|
|
4019
|
+
unregisterInteractions?.();
|
|
4020
|
+
}
|
|
4021
|
+
if (slashShutdownCleanup) await slashShutdownCleanup;
|
|
4022
|
+
}
|
|
4023
|
+
//#endregion
|
|
4024
|
+
//#region extensions/mattermost/src/mattermost/probe.ts
|
|
4025
|
+
async function probeMattermost(baseUrl, botToken, timeoutMs = 2500) {
|
|
4026
|
+
const normalized = normalizeMattermostBaseUrl(baseUrl);
|
|
4027
|
+
if (!normalized) return {
|
|
4028
|
+
ok: false,
|
|
4029
|
+
error: "baseUrl missing"
|
|
4030
|
+
};
|
|
4031
|
+
const url = `${normalized}/api/v4/users/me`;
|
|
4032
|
+
const start = Date.now();
|
|
4033
|
+
const controller = timeoutMs > 0 ? new AbortController() : void 0;
|
|
4034
|
+
let timer = null;
|
|
4035
|
+
if (controller) timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4036
|
+
try {
|
|
4037
|
+
const res = await fetch(url, {
|
|
4038
|
+
headers: { Authorization: `Bearer ${botToken}` },
|
|
4039
|
+
signal: controller?.signal
|
|
4040
|
+
});
|
|
4041
|
+
const elapsedMs = Date.now() - start;
|
|
4042
|
+
if (!res.ok) {
|
|
4043
|
+
const detail = await readMattermostError(res);
|
|
4044
|
+
return {
|
|
4045
|
+
ok: false,
|
|
4046
|
+
status: res.status,
|
|
4047
|
+
error: detail || res.statusText,
|
|
4048
|
+
elapsedMs
|
|
4049
|
+
};
|
|
4050
|
+
}
|
|
4051
|
+
const bot = await res.json();
|
|
4052
|
+
return {
|
|
4053
|
+
ok: true,
|
|
4054
|
+
status: res.status,
|
|
4055
|
+
elapsedMs,
|
|
4056
|
+
bot
|
|
4057
|
+
};
|
|
4058
|
+
} catch (err) {
|
|
4059
|
+
return {
|
|
4060
|
+
ok: false,
|
|
4061
|
+
status: null,
|
|
4062
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4063
|
+
elapsedMs: Date.now() - start
|
|
4064
|
+
};
|
|
4065
|
+
} finally {
|
|
4066
|
+
if (timer) clearTimeout(timer);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
//#endregion
|
|
4070
|
+
//#region extensions/mattermost/src/mattermost/reactions.ts
|
|
4071
|
+
const BOT_USER_CACHE_TTL_MS = 10 * 6e4;
|
|
4072
|
+
const botUserIdCache = /* @__PURE__ */ new Map();
|
|
4073
|
+
async function resolveBotUserId(client, cacheKey) {
|
|
4074
|
+
const cached = botUserIdCache.get(cacheKey);
|
|
4075
|
+
if (cached && cached.expiresAt > Date.now()) return cached.userId;
|
|
4076
|
+
const userId = (await fetchMattermostMe(client))?.id?.trim();
|
|
4077
|
+
if (!userId) return null;
|
|
4078
|
+
botUserIdCache.set(cacheKey, {
|
|
4079
|
+
userId,
|
|
4080
|
+
expiresAt: Date.now() + BOT_USER_CACHE_TTL_MS
|
|
4081
|
+
});
|
|
4082
|
+
return userId;
|
|
4083
|
+
}
|
|
4084
|
+
async function addMattermostReaction(params) {
|
|
4085
|
+
return runMattermostReaction(params, {
|
|
4086
|
+
action: "add",
|
|
4087
|
+
mutation: createReaction
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
async function removeMattermostReaction(params) {
|
|
4091
|
+
return runMattermostReaction(params, {
|
|
4092
|
+
action: "remove",
|
|
4093
|
+
mutation: deleteReaction
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
async function runMattermostReaction(params, options) {
|
|
4097
|
+
const resolved = resolveMattermostAccount({
|
|
4098
|
+
cfg: params.cfg,
|
|
4099
|
+
accountId: params.accountId
|
|
4100
|
+
});
|
|
4101
|
+
const baseUrl = resolved.baseUrl?.trim();
|
|
4102
|
+
const botToken = resolved.botToken?.trim();
|
|
4103
|
+
if (!baseUrl || !botToken) return {
|
|
4104
|
+
ok: false,
|
|
4105
|
+
error: "Mattermost botToken/baseUrl missing."
|
|
4106
|
+
};
|
|
4107
|
+
const client = createMattermostClient({
|
|
4108
|
+
baseUrl,
|
|
4109
|
+
botToken,
|
|
4110
|
+
fetchImpl: params.fetchImpl
|
|
4111
|
+
});
|
|
4112
|
+
const userId = await resolveBotUserId(client, `${baseUrl}:${botToken}`);
|
|
4113
|
+
if (!userId) return {
|
|
4114
|
+
ok: false,
|
|
4115
|
+
error: "Mattermost reactions failed: could not resolve bot user id."
|
|
4116
|
+
};
|
|
4117
|
+
try {
|
|
4118
|
+
await options.mutation(client, {
|
|
4119
|
+
userId,
|
|
4120
|
+
postId: params.postId,
|
|
4121
|
+
emojiName: params.emojiName
|
|
4122
|
+
});
|
|
4123
|
+
} catch (err) {
|
|
4124
|
+
return {
|
|
4125
|
+
ok: false,
|
|
4126
|
+
error: `Mattermost ${options.action} reaction failed: ${String(err)}`
|
|
4127
|
+
};
|
|
4128
|
+
}
|
|
4129
|
+
return { ok: true };
|
|
4130
|
+
}
|
|
4131
|
+
async function createReaction(client, params) {
|
|
4132
|
+
await client.request("/reactions", {
|
|
4133
|
+
method: "POST",
|
|
4134
|
+
body: JSON.stringify({
|
|
4135
|
+
user_id: params.userId,
|
|
4136
|
+
post_id: params.postId,
|
|
4137
|
+
emoji_name: params.emojiName
|
|
4138
|
+
})
|
|
4139
|
+
});
|
|
4140
|
+
}
|
|
4141
|
+
async function deleteReaction(client, params) {
|
|
4142
|
+
const emoji = encodeURIComponent(params.emojiName);
|
|
4143
|
+
await client.request(`/users/${params.userId}/posts/${params.postId}/reactions/${emoji}`, { method: "DELETE" });
|
|
4144
|
+
}
|
|
4145
|
+
//#endregion
|
|
4146
|
+
//#region extensions/mattermost/src/normalize.ts
|
|
4147
|
+
function normalizeMattermostMessagingTarget(raw) {
|
|
4148
|
+
const trimmed = raw.trim();
|
|
4149
|
+
if (!trimmed) return;
|
|
4150
|
+
const lower = trimmed.toLowerCase();
|
|
4151
|
+
if (lower.startsWith("channel:")) {
|
|
4152
|
+
const id = trimmed.slice(8).trim();
|
|
4153
|
+
return id ? `channel:${id}` : void 0;
|
|
4154
|
+
}
|
|
4155
|
+
if (lower.startsWith("group:")) {
|
|
4156
|
+
const id = trimmed.slice(6).trim();
|
|
4157
|
+
return id ? `channel:${id}` : void 0;
|
|
4158
|
+
}
|
|
4159
|
+
if (lower.startsWith("user:")) {
|
|
4160
|
+
const id = trimmed.slice(5).trim();
|
|
4161
|
+
return id ? `user:${id}` : void 0;
|
|
4162
|
+
}
|
|
4163
|
+
if (lower.startsWith("mattermost:")) {
|
|
4164
|
+
const id = trimmed.slice(11).trim();
|
|
4165
|
+
return id ? `user:${id}` : void 0;
|
|
4166
|
+
}
|
|
4167
|
+
if (trimmed.startsWith("@")) {
|
|
4168
|
+
const id = trimmed.slice(1).trim();
|
|
4169
|
+
return id ? `@${id}` : void 0;
|
|
4170
|
+
}
|
|
4171
|
+
if (trimmed.startsWith("#")) return;
|
|
4172
|
+
}
|
|
4173
|
+
function looksLikeMattermostTargetId(raw, normalized) {
|
|
4174
|
+
const trimmed = raw.trim();
|
|
4175
|
+
if (!trimmed) return false;
|
|
4176
|
+
if (/^(user|channel|group|mattermost):/i.test(trimmed)) return true;
|
|
4177
|
+
if (trimmed.startsWith("@")) return true;
|
|
4178
|
+
return /^[a-z0-9]{26}$/i.test(trimmed) || /^[a-z0-9]{26}__[a-z0-9]{26}$/i.test(trimmed);
|
|
4179
|
+
}
|
|
4180
|
+
//#endregion
|
|
4181
|
+
//#region extensions/mattermost/src/setup-core.ts
|
|
4182
|
+
const channel$1 = "mattermost";
|
|
4183
|
+
function isMattermostConfigured(account) {
|
|
4184
|
+
return (Boolean(account.botToken?.trim()) || hasConfiguredSecretInput(account.config.botToken)) && Boolean(account.baseUrl);
|
|
4185
|
+
}
|
|
4186
|
+
function resolveMattermostAccountWithSecrets(cfg, accountId) {
|
|
4187
|
+
return resolveMattermostAccount({
|
|
4188
|
+
cfg,
|
|
4189
|
+
accountId,
|
|
4190
|
+
allowUnresolvedSecretRef: true
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
const mattermostSetupAdapter = {
|
|
4194
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
4195
|
+
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
4196
|
+
cfg,
|
|
4197
|
+
channelKey: channel$1,
|
|
4198
|
+
accountId,
|
|
4199
|
+
name
|
|
4200
|
+
}),
|
|
4201
|
+
validateInput: ({ accountId, input }) => {
|
|
4202
|
+
const token = input.botToken ?? input.token;
|
|
4203
|
+
const baseUrl = normalizeMattermostBaseUrl(input.httpUrl);
|
|
4204
|
+
if (input.useEnv && accountId !== "default") return "Mattermost env vars can only be used for the default account.";
|
|
4205
|
+
if (!input.useEnv && (!token || !baseUrl)) return "Mattermost requires --bot-token and --http-url (or --use-env).";
|
|
4206
|
+
if (input.httpUrl && !baseUrl) return "Mattermost --http-url must include a valid base URL.";
|
|
4207
|
+
return null;
|
|
4208
|
+
},
|
|
4209
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
4210
|
+
const token = input.botToken ?? input.token;
|
|
4211
|
+
const baseUrl = normalizeMattermostBaseUrl(input.httpUrl);
|
|
4212
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
4213
|
+
cfg,
|
|
4214
|
+
channelKey: channel$1,
|
|
4215
|
+
accountId,
|
|
4216
|
+
name: input.name
|
|
4217
|
+
});
|
|
4218
|
+
return applySetupAccountConfigPatch({
|
|
4219
|
+
cfg: accountId !== "default" ? migrateBaseNameToDefaultAccount({
|
|
4220
|
+
cfg: namedConfig,
|
|
4221
|
+
channelKey: channel$1
|
|
4222
|
+
}) : namedConfig,
|
|
4223
|
+
channelKey: channel$1,
|
|
4224
|
+
accountId,
|
|
4225
|
+
patch: input.useEnv ? {} : {
|
|
4226
|
+
...token ? { botToken: token } : {},
|
|
4227
|
+
...baseUrl ? { baseUrl } : {}
|
|
4228
|
+
}
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
};
|
|
4232
|
+
//#endregion
|
|
4233
|
+
//#region extensions/mattermost/src/setup-surface.ts
|
|
4234
|
+
const channel = "mattermost";
|
|
4235
|
+
const mattermostSetupWizard = {
|
|
4236
|
+
channel,
|
|
4237
|
+
status: {
|
|
4238
|
+
configuredLabel: "configured",
|
|
4239
|
+
unconfiguredLabel: "needs token + url",
|
|
4240
|
+
configuredHint: "configured",
|
|
4241
|
+
unconfiguredHint: "needs setup",
|
|
4242
|
+
configuredScore: 2,
|
|
4243
|
+
unconfiguredScore: 1,
|
|
4244
|
+
resolveConfigured: ({ cfg }) => listMattermostAccountIds(cfg).some((accountId) => isMattermostConfigured(resolveMattermostAccountWithSecrets(cfg, accountId)))
|
|
4245
|
+
},
|
|
4246
|
+
introNote: {
|
|
4247
|
+
title: "Mattermost bot token",
|
|
4248
|
+
lines: [
|
|
4249
|
+
"1) Mattermost System Console -> Integrations -> Bot Accounts",
|
|
4250
|
+
"2) Create a bot + copy its token",
|
|
4251
|
+
"3) Use your server base URL (e.g., https://chat.example.com)",
|
|
4252
|
+
"Tip: the bot must be a member of any channel you want it to monitor.",
|
|
4253
|
+
`Docs: ${formatDocsLink("/mattermost", "mattermost")}`
|
|
4254
|
+
],
|
|
4255
|
+
shouldShow: ({ cfg, accountId }) => !isMattermostConfigured(resolveMattermostAccountWithSecrets(cfg, accountId))
|
|
4256
|
+
},
|
|
4257
|
+
envShortcut: {
|
|
4258
|
+
prompt: "MATTERMOST_BOT_TOKEN + MATTERMOST_URL detected. Use env vars?",
|
|
4259
|
+
preferredEnvVar: "MATTERMOST_BOT_TOKEN",
|
|
4260
|
+
isAvailable: ({ cfg, accountId }) => {
|
|
4261
|
+
if (accountId !== "default") return false;
|
|
4262
|
+
const resolvedAccount = resolveMattermostAccountWithSecrets(cfg, accountId);
|
|
4263
|
+
const hasConfigValues = hasConfiguredSecretInput(resolvedAccount.config.botToken) || Boolean(resolvedAccount.config.baseUrl?.trim());
|
|
4264
|
+
return Boolean(process.env.MATTERMOST_BOT_TOKEN?.trim() && process.env.MATTERMOST_URL?.trim() && !hasConfigValues);
|
|
4265
|
+
},
|
|
4266
|
+
apply: ({ cfg, accountId }) => applySetupAccountConfigPatch({
|
|
4267
|
+
cfg,
|
|
4268
|
+
channelKey: channel,
|
|
4269
|
+
accountId,
|
|
4270
|
+
patch: {}
|
|
4271
|
+
})
|
|
4272
|
+
},
|
|
4273
|
+
credentials: [{
|
|
4274
|
+
inputKey: "botToken",
|
|
4275
|
+
providerHint: channel,
|
|
4276
|
+
credentialLabel: "bot token",
|
|
4277
|
+
preferredEnvVar: "MATTERMOST_BOT_TOKEN",
|
|
4278
|
+
envPrompt: "MATTERMOST_BOT_TOKEN + MATTERMOST_URL detected. Use env vars?",
|
|
4279
|
+
keepPrompt: "Mattermost bot token already configured. Keep it?",
|
|
4280
|
+
inputPrompt: "Enter Mattermost bot token",
|
|
4281
|
+
inspect: ({ cfg, accountId }) => {
|
|
4282
|
+
const resolvedAccount = resolveMattermostAccountWithSecrets(cfg, accountId);
|
|
4283
|
+
return {
|
|
4284
|
+
accountConfigured: isMattermostConfigured(resolvedAccount),
|
|
4285
|
+
hasConfiguredValue: hasConfiguredSecretInput(resolvedAccount.config.botToken)
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
}],
|
|
4289
|
+
textInputs: [{
|
|
4290
|
+
inputKey: "httpUrl",
|
|
4291
|
+
message: "Enter Mattermost base URL",
|
|
4292
|
+
confirmCurrentValue: false,
|
|
4293
|
+
currentValue: ({ cfg, accountId }) => resolveMattermostAccountWithSecrets(cfg, accountId).baseUrl ?? process.env.MATTERMOST_URL?.trim(),
|
|
4294
|
+
initialValue: ({ cfg, accountId }) => resolveMattermostAccountWithSecrets(cfg, accountId).baseUrl ?? process.env.MATTERMOST_URL?.trim(),
|
|
4295
|
+
shouldPrompt: ({ cfg, accountId, credentialValues, currentValue }) => {
|
|
4296
|
+
const resolvedAccount = resolveMattermostAccountWithSecrets(cfg, accountId);
|
|
4297
|
+
const tokenConfigured = Boolean(resolvedAccount.botToken?.trim()) || hasConfiguredSecretInput(resolvedAccount.config.botToken);
|
|
4298
|
+
return Boolean(credentialValues.botToken) || !tokenConfigured || !currentValue;
|
|
4299
|
+
},
|
|
4300
|
+
validate: ({ value }) => normalizeMattermostBaseUrl(value) ? void 0 : "Mattermost base URL must include a valid base URL.",
|
|
4301
|
+
normalizeValue: ({ value }) => normalizeMattermostBaseUrl(value) ?? value.trim()
|
|
4302
|
+
}],
|
|
4303
|
+
disable: (cfg) => ({
|
|
4304
|
+
...cfg,
|
|
4305
|
+
channels: {
|
|
4306
|
+
...cfg.channels,
|
|
4307
|
+
mattermost: {
|
|
4308
|
+
...cfg.channels?.mattermost,
|
|
4309
|
+
enabled: false
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
})
|
|
4313
|
+
};
|
|
4314
|
+
//#endregion
|
|
4315
|
+
//#region extensions/mattermost/src/channel.ts
|
|
4316
|
+
const mattermostMessageActions = {
|
|
4317
|
+
listActions: ({ cfg }) => {
|
|
4318
|
+
const enabledAccounts = listMattermostAccountIds(cfg).map((accountId) => resolveMattermostAccount({
|
|
4319
|
+
cfg,
|
|
4320
|
+
accountId
|
|
4321
|
+
})).filter((account) => account.enabled).filter((account) => Boolean(account.botToken?.trim() && account.baseUrl?.trim()));
|
|
4322
|
+
const actions = [];
|
|
4323
|
+
if (enabledAccounts.length > 0) actions.push("send");
|
|
4324
|
+
const baseReactions = (cfg.channels?.mattermost?.actions)?.reactions;
|
|
4325
|
+
if (enabledAccounts.some((account) => {
|
|
4326
|
+
return (account.config.actions?.reactions ?? baseReactions ?? true) !== false;
|
|
4327
|
+
})) actions.push("react");
|
|
4328
|
+
return actions;
|
|
4329
|
+
},
|
|
4330
|
+
supportsAction: ({ action }) => {
|
|
4331
|
+
return action === "send" || action === "react";
|
|
4332
|
+
},
|
|
4333
|
+
getCapabilities: ({ cfg }) => {
|
|
4334
|
+
return listMattermostAccountIds(cfg).map((id) => resolveMattermostAccount({
|
|
4335
|
+
cfg,
|
|
4336
|
+
accountId: id
|
|
4337
|
+
})).filter((a) => a.enabled && a.botToken?.trim() && a.baseUrl?.trim()).length > 0 ? ["buttons"] : [];
|
|
4338
|
+
},
|
|
4339
|
+
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
4340
|
+
if (action === "react") {
|
|
4341
|
+
const mmBase = cfg?.channels?.mattermost;
|
|
4342
|
+
const accounts = mmBase?.accounts;
|
|
4343
|
+
const resolvedAccountId = accountId ?? resolveDefaultMattermostAccountId(cfg);
|
|
4344
|
+
const acctActions = (accounts?.[resolvedAccountId])?.actions;
|
|
4345
|
+
const baseActions = mmBase?.actions;
|
|
4346
|
+
if (!(acctActions?.reactions ?? baseActions?.reactions ?? true)) throw new Error("Mattermost reactions are disabled in config");
|
|
4347
|
+
const postId = (typeof params?.messageId === "string" ? params.messageId : typeof params?.postId === "string" ? params.postId : "").trim();
|
|
4348
|
+
if (!postId) throw new Error("Mattermost react requires messageId (post id)");
|
|
4349
|
+
const emojiName = (typeof params?.emoji === "string" ? params.emoji : "").trim().replace(/^:+|:+$/g, "");
|
|
4350
|
+
if (!emojiName) throw new Error("Mattermost react requires emoji");
|
|
4351
|
+
if (params?.remove === true) {
|
|
4352
|
+
const result = await removeMattermostReaction({
|
|
4353
|
+
cfg,
|
|
4354
|
+
postId,
|
|
4355
|
+
emojiName,
|
|
4356
|
+
accountId: resolvedAccountId
|
|
4357
|
+
});
|
|
4358
|
+
if (!result.ok) throw new Error(result.error);
|
|
4359
|
+
return {
|
|
4360
|
+
content: [{
|
|
4361
|
+
type: "text",
|
|
4362
|
+
text: `Removed reaction :${emojiName}: from ${postId}`
|
|
4363
|
+
}],
|
|
4364
|
+
details: {}
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
const result = await addMattermostReaction({
|
|
4368
|
+
cfg,
|
|
4369
|
+
postId,
|
|
4370
|
+
emojiName,
|
|
4371
|
+
accountId: resolvedAccountId
|
|
4372
|
+
});
|
|
4373
|
+
if (!result.ok) throw new Error(result.error);
|
|
4374
|
+
return {
|
|
4375
|
+
content: [{
|
|
4376
|
+
type: "text",
|
|
4377
|
+
text: `Reacted with :${emojiName}: on ${postId}`
|
|
4378
|
+
}],
|
|
4379
|
+
details: {}
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
if (action !== "send") throw new Error(`Unsupported Mattermost action: ${action}`);
|
|
4383
|
+
const to = typeof params.to === "string" ? params.to.trim() : typeof params.target === "string" ? params.target.trim() : "";
|
|
4384
|
+
if (!to) throw new Error("Mattermost send requires a target (to).");
|
|
4385
|
+
const message = typeof params.message === "string" ? params.message : "";
|
|
4386
|
+
const replyToId = readMattermostReplyToId(params);
|
|
4387
|
+
const resolvedAccountId = accountId || void 0;
|
|
4388
|
+
const mediaUrl = typeof params.media === "string" ? params.media.trim() || void 0 : void 0;
|
|
4389
|
+
const result = await sendMessageMattermost(to, message, {
|
|
4390
|
+
accountId: resolvedAccountId,
|
|
4391
|
+
replyToId,
|
|
4392
|
+
buttons: Array.isArray(params.buttons) ? params.buttons : void 0,
|
|
4393
|
+
attachmentText: typeof params.attachmentText === "string" ? params.attachmentText : void 0,
|
|
4394
|
+
mediaUrl
|
|
4395
|
+
});
|
|
4396
|
+
return {
|
|
4397
|
+
content: [{
|
|
4398
|
+
type: "text",
|
|
4399
|
+
text: JSON.stringify({
|
|
4400
|
+
ok: true,
|
|
4401
|
+
channel: "mattermost",
|
|
4402
|
+
messageId: result.messageId,
|
|
4403
|
+
channelId: result.channelId
|
|
4404
|
+
})
|
|
4405
|
+
}],
|
|
4406
|
+
details: {}
|
|
4407
|
+
};
|
|
4408
|
+
}
|
|
4409
|
+
};
|
|
4410
|
+
const meta = {
|
|
4411
|
+
id: "mattermost",
|
|
4412
|
+
label: "Mattermost",
|
|
4413
|
+
selectionLabel: "Mattermost (plugin)",
|
|
4414
|
+
detailLabel: "Mattermost Bot",
|
|
4415
|
+
docsPath: "/channels/mattermost",
|
|
4416
|
+
docsLabel: "mattermost",
|
|
4417
|
+
blurb: "self-hosted Slack-style chat; install the plugin to enable.",
|
|
4418
|
+
systemImage: "bubble.left.and.bubble.right",
|
|
4419
|
+
order: 65,
|
|
4420
|
+
quickstartAllowFrom: true
|
|
4421
|
+
};
|
|
4422
|
+
function readMattermostReplyToId(params) {
|
|
4423
|
+
const readNormalizedValue = (value) => {
|
|
4424
|
+
if (typeof value !== "string") return;
|
|
4425
|
+
return value.trim() || void 0;
|
|
4426
|
+
};
|
|
4427
|
+
return readNormalizedValue(params.replyToId) ?? readNormalizedValue(params.replyTo);
|
|
4428
|
+
}
|
|
4429
|
+
function normalizeAllowEntry(entry) {
|
|
4430
|
+
return entry.trim().replace(/^(mattermost|user):/i, "").replace(/^@/, "").toLowerCase();
|
|
4431
|
+
}
|
|
4432
|
+
function formatAllowEntry(entry) {
|
|
4433
|
+
const trimmed = entry.trim();
|
|
4434
|
+
if (!trimmed) return "";
|
|
4435
|
+
if (trimmed.startsWith("@")) {
|
|
4436
|
+
const username = trimmed.slice(1).trim();
|
|
4437
|
+
return username ? `@${username.toLowerCase()}` : "";
|
|
4438
|
+
}
|
|
4439
|
+
return trimmed.replace(/^(mattermost|user):/i, "").toLowerCase();
|
|
4440
|
+
}
|
|
4441
|
+
const mattermostConfigAccessors = createScopedAccountConfigAccessors({
|
|
4442
|
+
resolveAccount: ({ cfg, accountId }) => resolveMattermostAccount({
|
|
4443
|
+
cfg,
|
|
4444
|
+
accountId
|
|
4445
|
+
}),
|
|
4446
|
+
resolveAllowFrom: (account) => account.config.allowFrom,
|
|
4447
|
+
formatAllowFrom: (allowFrom) => formatNormalizedAllowFromEntries({
|
|
4448
|
+
allowFrom,
|
|
4449
|
+
normalizeEntry: formatAllowEntry
|
|
4450
|
+
})
|
|
4451
|
+
});
|
|
4452
|
+
const mattermostPlugin = {
|
|
4453
|
+
id: "mattermost",
|
|
4454
|
+
meta: { ...meta },
|
|
4455
|
+
setup: mattermostSetupAdapter,
|
|
4456
|
+
setupWizard: mattermostSetupWizard,
|
|
4457
|
+
pairing: {
|
|
4458
|
+
idLabel: "mattermostUserId",
|
|
4459
|
+
normalizeAllowEntry: (entry) => normalizeAllowEntry(entry),
|
|
4460
|
+
notifyApproval: async ({ id }) => {
|
|
4461
|
+
console.log(`[mattermost] User ${id} approved for pairing`);
|
|
4462
|
+
}
|
|
4463
|
+
},
|
|
4464
|
+
capabilities: {
|
|
4465
|
+
chatTypes: [
|
|
4466
|
+
"direct",
|
|
4467
|
+
"channel",
|
|
4468
|
+
"group",
|
|
4469
|
+
"thread"
|
|
4470
|
+
],
|
|
4471
|
+
reactions: true,
|
|
4472
|
+
threads: true,
|
|
4473
|
+
media: true,
|
|
4474
|
+
nativeCommands: true
|
|
4475
|
+
},
|
|
4476
|
+
streaming: { blockStreamingCoalesceDefaults: {
|
|
4477
|
+
minChars: 1500,
|
|
4478
|
+
idleMs: 1e3
|
|
4479
|
+
} },
|
|
4480
|
+
threading: { resolveReplyToMode: ({ cfg, accountId, chatType }) => {
|
|
4481
|
+
return resolveMattermostReplyToMode(resolveMattermostAccount({
|
|
4482
|
+
cfg,
|
|
4483
|
+
accountId: accountId ?? "default"
|
|
4484
|
+
}), chatType === "direct" || chatType === "group" || chatType === "channel" ? chatType : "channel");
|
|
4485
|
+
} },
|
|
4486
|
+
reload: { configPrefixes: ["channels.mattermost"] },
|
|
4487
|
+
configSchema: buildChannelConfigSchema(MattermostConfigSchema),
|
|
4488
|
+
config: {
|
|
4489
|
+
listAccountIds: (cfg) => listMattermostAccountIds(cfg),
|
|
4490
|
+
resolveAccount: (cfg, accountId) => resolveMattermostAccount({
|
|
4491
|
+
cfg,
|
|
4492
|
+
accountId
|
|
4493
|
+
}),
|
|
4494
|
+
defaultAccountId: (cfg) => resolveDefaultMattermostAccountId(cfg),
|
|
4495
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
|
|
4496
|
+
cfg,
|
|
4497
|
+
sectionKey: "mattermost",
|
|
4498
|
+
accountId,
|
|
4499
|
+
enabled,
|
|
4500
|
+
allowTopLevel: true
|
|
4501
|
+
}),
|
|
4502
|
+
deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
|
|
4503
|
+
cfg,
|
|
4504
|
+
sectionKey: "mattermost",
|
|
4505
|
+
accountId,
|
|
4506
|
+
clearBaseFields: [
|
|
4507
|
+
"botToken",
|
|
4508
|
+
"baseUrl",
|
|
4509
|
+
"name"
|
|
4510
|
+
]
|
|
4511
|
+
}),
|
|
4512
|
+
isConfigured: (account) => Boolean(account.botToken && account.baseUrl),
|
|
4513
|
+
describeAccount: (account) => ({
|
|
4514
|
+
accountId: account.accountId,
|
|
4515
|
+
name: account.name,
|
|
4516
|
+
enabled: account.enabled,
|
|
4517
|
+
configured: Boolean(account.botToken && account.baseUrl),
|
|
4518
|
+
botTokenSource: account.botTokenSource,
|
|
4519
|
+
baseUrl: account.baseUrl
|
|
4520
|
+
}),
|
|
4521
|
+
...mattermostConfigAccessors
|
|
4522
|
+
},
|
|
4523
|
+
security: {
|
|
4524
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
4525
|
+
return buildAccountScopedDmSecurityPolicy({
|
|
4526
|
+
cfg,
|
|
4527
|
+
channelKey: "mattermost",
|
|
4528
|
+
accountId,
|
|
4529
|
+
fallbackAccountId: account.accountId ?? "default",
|
|
4530
|
+
policy: account.config.dmPolicy,
|
|
4531
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
4532
|
+
policyPathSuffix: "dmPolicy",
|
|
4533
|
+
normalizeEntry: (raw) => normalizeAllowEntry(raw)
|
|
4534
|
+
});
|
|
4535
|
+
},
|
|
4536
|
+
collectWarnings: ({ account, cfg }) => {
|
|
4537
|
+
return collectAllowlistProviderRestrictSendersWarnings({
|
|
4538
|
+
cfg,
|
|
4539
|
+
providerConfigPresent: cfg.channels?.mattermost !== void 0,
|
|
4540
|
+
configuredGroupPolicy: account.config.groupPolicy,
|
|
4541
|
+
surface: "Mattermost channels",
|
|
4542
|
+
openScope: "any member",
|
|
4543
|
+
groupPolicyPath: "channels.mattermost.groupPolicy",
|
|
4544
|
+
groupAllowFromPath: "channels.mattermost.groupAllowFrom"
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
},
|
|
4548
|
+
groups: { resolveRequireMention: resolveMattermostGroupRequireMention },
|
|
4549
|
+
actions: mattermostMessageActions,
|
|
4550
|
+
directory: {
|
|
4551
|
+
listGroups: async (params) => listMattermostDirectoryGroups(params),
|
|
4552
|
+
listGroupsLive: async (params) => listMattermostDirectoryGroups(params),
|
|
4553
|
+
listPeers: async (params) => listMattermostDirectoryPeers(params),
|
|
4554
|
+
listPeersLive: async (params) => listMattermostDirectoryPeers(params)
|
|
4555
|
+
},
|
|
4556
|
+
messaging: {
|
|
4557
|
+
normalizeTarget: normalizeMattermostMessagingTarget,
|
|
4558
|
+
targetResolver: {
|
|
4559
|
+
looksLikeId: looksLikeMattermostTargetId,
|
|
4560
|
+
hint: "<channelId|user:ID|channel:ID>",
|
|
4561
|
+
resolveTarget: async ({ cfg, accountId, input }) => {
|
|
4562
|
+
const resolved = await resolveMattermostOpaqueTarget({
|
|
4563
|
+
input,
|
|
4564
|
+
cfg,
|
|
4565
|
+
accountId
|
|
4566
|
+
});
|
|
4567
|
+
if (!resolved) return null;
|
|
4568
|
+
return {
|
|
4569
|
+
to: resolved.to,
|
|
4570
|
+
kind: resolved.kind,
|
|
4571
|
+
source: "directory"
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
},
|
|
4576
|
+
outbound: {
|
|
4577
|
+
deliveryMode: "direct",
|
|
4578
|
+
chunker: (text, limit) => getMattermostRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
4579
|
+
chunkerMode: "markdown",
|
|
4580
|
+
textChunkLimit: 4e3,
|
|
4581
|
+
resolveTarget: ({ to }) => {
|
|
4582
|
+
const trimmed = to?.trim();
|
|
4583
|
+
if (!trimmed) return {
|
|
4584
|
+
ok: false,
|
|
4585
|
+
error: /* @__PURE__ */ new Error("Delivering to Mattermost requires --to <channelId|@username|user:ID|channel:ID>")
|
|
4586
|
+
};
|
|
4587
|
+
return {
|
|
4588
|
+
ok: true,
|
|
4589
|
+
to: trimmed
|
|
4590
|
+
};
|
|
4591
|
+
},
|
|
4592
|
+
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
4593
|
+
return {
|
|
4594
|
+
channel: "mattermost",
|
|
4595
|
+
...await sendMessageMattermost(to, text, {
|
|
4596
|
+
cfg,
|
|
4597
|
+
accountId: accountId ?? void 0,
|
|
4598
|
+
replyToId: replyToId ?? (threadId != null ? String(threadId) : void 0)
|
|
4599
|
+
})
|
|
4600
|
+
};
|
|
4601
|
+
},
|
|
4602
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, replyToId, threadId }) => {
|
|
4603
|
+
return {
|
|
4604
|
+
channel: "mattermost",
|
|
4605
|
+
...await sendMessageMattermost(to, text, {
|
|
4606
|
+
cfg,
|
|
4607
|
+
accountId: accountId ?? void 0,
|
|
4608
|
+
mediaUrl,
|
|
4609
|
+
mediaLocalRoots,
|
|
4610
|
+
replyToId: replyToId ?? (threadId != null ? String(threadId) : void 0)
|
|
4611
|
+
})
|
|
4612
|
+
};
|
|
4613
|
+
}
|
|
4614
|
+
},
|
|
4615
|
+
status: {
|
|
4616
|
+
defaultRuntime: {
|
|
4617
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
4618
|
+
running: false,
|
|
4619
|
+
connected: false,
|
|
4620
|
+
lastConnectedAt: null,
|
|
4621
|
+
lastDisconnect: null,
|
|
4622
|
+
lastStartAt: null,
|
|
4623
|
+
lastStopAt: null,
|
|
4624
|
+
lastError: null
|
|
4625
|
+
},
|
|
4626
|
+
buildChannelSummary: ({ snapshot }) => buildPassiveProbedChannelStatusSummary(snapshot, {
|
|
4627
|
+
botTokenSource: snapshot.botTokenSource ?? "none",
|
|
4628
|
+
connected: snapshot.connected ?? false,
|
|
4629
|
+
baseUrl: snapshot.baseUrl ?? null
|
|
4630
|
+
}),
|
|
4631
|
+
probeAccount: async ({ account, timeoutMs }) => {
|
|
4632
|
+
const token = account.botToken?.trim();
|
|
4633
|
+
const baseUrl = account.baseUrl?.trim();
|
|
4634
|
+
if (!token || !baseUrl) return {
|
|
4635
|
+
ok: false,
|
|
4636
|
+
error: "bot token or baseUrl missing"
|
|
4637
|
+
};
|
|
4638
|
+
return await probeMattermost(baseUrl, token, timeoutMs);
|
|
4639
|
+
},
|
|
4640
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
|
4641
|
+
return {
|
|
4642
|
+
...buildComputedAccountStatusSnapshot({
|
|
4643
|
+
accountId: account.accountId,
|
|
4644
|
+
name: account.name,
|
|
4645
|
+
enabled: account.enabled,
|
|
4646
|
+
configured: Boolean(account.botToken && account.baseUrl),
|
|
4647
|
+
runtime,
|
|
4648
|
+
probe
|
|
4649
|
+
}),
|
|
4650
|
+
botTokenSource: account.botTokenSource,
|
|
4651
|
+
baseUrl: account.baseUrl,
|
|
4652
|
+
connected: runtime?.connected ?? false,
|
|
4653
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
4654
|
+
lastDisconnect: runtime?.lastDisconnect ?? null
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
},
|
|
4658
|
+
gateway: { startAccount: async (ctx) => {
|
|
4659
|
+
const account = ctx.account;
|
|
4660
|
+
const statusSink = createAccountStatusSink({
|
|
4661
|
+
accountId: ctx.accountId,
|
|
4662
|
+
setStatus: ctx.setStatus
|
|
4663
|
+
});
|
|
4664
|
+
statusSink({
|
|
4665
|
+
baseUrl: account.baseUrl,
|
|
4666
|
+
botTokenSource: account.botTokenSource
|
|
4667
|
+
});
|
|
4668
|
+
ctx.log?.info(`[${account.accountId}] starting channel`);
|
|
4669
|
+
return monitorMattermostProvider({
|
|
4670
|
+
botToken: account.botToken ?? void 0,
|
|
4671
|
+
baseUrl: account.baseUrl ?? void 0,
|
|
4672
|
+
accountId: account.accountId,
|
|
4673
|
+
config: ctx.cfg,
|
|
4674
|
+
runtime: ctx.runtime,
|
|
4675
|
+
abortSignal: ctx.abortSignal,
|
|
4676
|
+
statusSink
|
|
4677
|
+
});
|
|
4678
|
+
} }
|
|
4679
|
+
};
|
|
4680
|
+
//#endregion
|
|
4681
|
+
export { registerSlashCommandRoute as n, setMattermostRuntime as r, mattermostPlugin as t };
|