feat: add slack multi-account routing
This commit is contained in:
@@ -74,6 +74,7 @@
|
|||||||
- CLI: add `clawdbot agents` (list/add/delete) with wizarded workspace/setup, provider login, and full prune on delete.
|
- CLI: add `clawdbot agents` (list/add/delete) with wizarded workspace/setup, provider login, and full prune on delete.
|
||||||
- CLI: add non-interactive flags for `agents add`, support `agents list --bindings`, and keep JSON output clean for scripting.
|
- CLI: add non-interactive flags for `agents add`, support `agents list --bindings`, and keep JSON output clean for scripting.
|
||||||
- Discord/Slack: fork thread sessions (agent-scoped) and inject thread starters for context. Thanks @thewilloftheshadow for PR #400.
|
- Discord/Slack: fork thread sessions (agent-scoped) and inject thread starters for context. Thanks @thewilloftheshadow for PR #400.
|
||||||
|
- Slack: route tool actions per account and suppress duplicate follow-ups per provider/target/account. Thanks @adam91holt for PR #457.
|
||||||
- Agent: treat compaction retry AbortError as a fallback trigger without swallowing non-abort errors. Thanks @erikpr1994 for PR #341.
|
- Agent: treat compaction retry AbortError as a fallback trigger without swallowing non-abort errors. Thanks @erikpr1994 for PR #341.
|
||||||
- Agent: add opt-in session pruning for tool results to reduce context bloat. Thanks @maxsumrall for PR #381.
|
- Agent: add opt-in session pruning for tool results to reduce context bloat. Thanks @maxsumrall for PR #381.
|
||||||
- Agent: protect bootstrap prefix from context pruning. Thanks @maxsumrall for PR #381.
|
- Agent: protect bootstrap prefix from context pruning. Thanks @maxsumrall for PR #381.
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -451,9 +451,9 @@ Thanks to all clawtributors:
|
|||||||
<a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a>
|
<a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a>
|
||||||
<a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a>
|
<a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a>
|
||||||
<a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a>
|
<a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a>
|
||||||
<a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a>
|
<a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a>
|
||||||
<a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a>
|
<a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a>
|
||||||
<a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a>
|
<a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
|
||||||
<a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a>
|
<a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a>
|
||||||
<a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/search?q=Tobias%20Bischoff"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
<a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/search?q=Tobias%20Bischoff"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -733,15 +733,17 @@ Slack runs in Socket Mode and requires both a bot token and app token:
|
|||||||
groupChannels: ["G123"]
|
groupChannels: ["G123"]
|
||||||
},
|
},
|
||||||
channels: {
|
channels: {
|
||||||
C123: { allow: true, requireMention: true },
|
C123: { allow: true, requireMention: true, allowBots: false },
|
||||||
"#general": {
|
"#general": {
|
||||||
allow: true,
|
allow: true,
|
||||||
requireMention: true,
|
requireMention: true,
|
||||||
|
allowBots: false,
|
||||||
users: ["U123"],
|
users: ["U123"],
|
||||||
skills: ["docs"],
|
skills: ["docs"],
|
||||||
systemPrompt: "Short answers only."
|
systemPrompt: "Short answers only."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
allowBots: false,
|
||||||
reactionNotifications: "own", // off | own | all | allowlist
|
reactionNotifications: "own", // off | own | all | allowlist
|
||||||
reactionAllowlist: ["U123"],
|
reactionAllowlist: ["U123"],
|
||||||
replyToMode: "off", // off | first | all
|
replyToMode: "off", // off | first | all
|
||||||
@@ -768,6 +770,8 @@ Multi-account support lives under `slack.accounts` (see the multi-account sectio
|
|||||||
|
|
||||||
Clawdbot starts Slack when the provider is enabled and both tokens are set (via config or `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`). Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands.
|
Clawdbot starts Slack when the provider is enabled and both tokens are set (via config or `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`). Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands.
|
||||||
|
|
||||||
|
Bot-authored messages are ignored by default. Enable with `slack.allowBots` or `slack.channels.<id>.allowBots`.
|
||||||
|
|
||||||
Reaction notification modes:
|
Reaction notification modes:
|
||||||
- `off`: no reaction events.
|
- `off`: no reaction events.
|
||||||
- `own`: reactions on the bot's own messages (default).
|
- `own`: reactions on the bot's own messages (default).
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ Controlled by `slack.replyToMode`:
|
|||||||
Channel options (`slack.channels.<id>` or `slack.channels.<name>`):
|
Channel options (`slack.channels.<id>` or `slack.channels.<name>`):
|
||||||
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
||||||
- `requireMention`: mention gating for the channel.
|
- `requireMention`: mention gating for the channel.
|
||||||
|
- `allowBots`: allow bot-authored messages in this channel (default: false).
|
||||||
- `users`: optional per-channel user allowlist.
|
- `users`: optional per-channel user allowlist.
|
||||||
- `skills`: skill filter (omit = all skills, empty = none).
|
- `skills`: skill filter (omit = all skills, empty = none).
|
||||||
- `systemPrompt`: extra system prompt for the channel (combined with topic/purpose).
|
- `systemPrompt`: extra system prompt for the channel (combined with topic/purpose).
|
||||||
@@ -251,5 +252,6 @@ Slack tool actions can be gated with `slack.actions.*`:
|
|||||||
## Notes
|
## Notes
|
||||||
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`); `routing.groupChat.mentionPatterns` also count as mentions.
|
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`); `routing.groupChat.mentionPatterns` also count as mentions.
|
||||||
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
|
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
|
||||||
|
- Bot-authored messages are ignored by default; enable via `slack.allowBots` or `slack.channels.<id>.allowBots`.
|
||||||
- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions).
|
- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions).
|
||||||
- Attachments are downloaded to the media store when permitted and under the size limit.
|
- Attachments are downloaded to the media store when permitted and under the size limit.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function createClawdbotTools(options?: {
|
|||||||
browserControlUrl?: string;
|
browserControlUrl?: string;
|
||||||
agentSessionKey?: string;
|
agentSessionKey?: string;
|
||||||
agentProvider?: string;
|
agentProvider?: string;
|
||||||
|
agentAccountId?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
sandboxed?: boolean;
|
sandboxed?: boolean;
|
||||||
config?: ClawdbotConfig;
|
config?: ClawdbotConfig;
|
||||||
@@ -34,7 +35,10 @@ export function createClawdbotTools(options?: {
|
|||||||
createNodesTool(),
|
createNodesTool(),
|
||||||
createCronTool(),
|
createCronTool(),
|
||||||
createDiscordTool(),
|
createDiscordTool(),
|
||||||
createSlackTool(),
|
createSlackTool({
|
||||||
|
agentAccountId: options?.agentAccountId,
|
||||||
|
config: options?.config,
|
||||||
|
}),
|
||||||
createTelegramTool(),
|
createTelegramTool(),
|
||||||
createWhatsAppTool(),
|
createWhatsAppTool(),
|
||||||
createGatewayTool({ agentSessionKey: options?.agentSessionKey }),
|
createGatewayTool({ agentSessionKey: options?.agentSessionKey }),
|
||||||
|
|||||||
@@ -276,6 +276,13 @@ type ApiKeyInfo = {
|
|||||||
source: string;
|
source: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MessagingToolSend = {
|
||||||
|
tool: string;
|
||||||
|
provider: string;
|
||||||
|
accountId?: string;
|
||||||
|
to?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmbeddedPiRunResult = {
|
export type EmbeddedPiRunResult = {
|
||||||
payloads?: Array<{
|
payloads?: Array<{
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -290,6 +297,8 @@ export type EmbeddedPiRunResult = {
|
|||||||
didSendViaMessagingTool?: boolean;
|
didSendViaMessagingTool?: boolean;
|
||||||
// Texts successfully sent via messaging tools during the run.
|
// Texts successfully sent via messaging tools during the run.
|
||||||
messagingToolSentTexts?: string[];
|
messagingToolSentTexts?: string[];
|
||||||
|
// Messaging tool targets that successfully sent a message during the run.
|
||||||
|
messagingToolSentTargets?: MessagingToolSend[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmbeddedPiCompactResult = {
|
export type EmbeddedPiCompactResult = {
|
||||||
@@ -737,6 +746,7 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
|
agentAccountId?: string;
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
@@ -842,6 +852,7 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
},
|
},
|
||||||
sandbox,
|
sandbox,
|
||||||
messageProvider: params.messageProvider,
|
messageProvider: params.messageProvider,
|
||||||
|
agentAccountId: params.agentAccountId,
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
agentDir,
|
agentDir,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
@@ -962,6 +973,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
|
agentAccountId?: string;
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
@@ -1153,6 +1165,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
},
|
},
|
||||||
sandbox,
|
sandbox,
|
||||||
messageProvider: params.messageProvider,
|
messageProvider: params.messageProvider,
|
||||||
|
agentAccountId: params.agentAccountId,
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
agentDir,
|
agentDir,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
@@ -1283,6 +1296,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
unsubscribe,
|
unsubscribe,
|
||||||
waitForCompactionRetry,
|
waitForCompactionRetry,
|
||||||
getMessagingToolSentTexts,
|
getMessagingToolSentTexts,
|
||||||
|
getMessagingToolSentTargets,
|
||||||
didSendViaMessagingTool,
|
didSendViaMessagingTool,
|
||||||
} = subscription;
|
} = subscription;
|
||||||
|
|
||||||
@@ -1567,6 +1581,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
},
|
},
|
||||||
didSendViaMessagingTool: didSendViaMessagingTool(),
|
didSendViaMessagingTool: didSendViaMessagingTool(),
|
||||||
messagingToolSentTexts: getMessagingToolSentTexts(),
|
messagingToolSentTexts: getMessagingToolSentTexts(),
|
||||||
|
messagingToolSentTargets: getMessagingToolSentTargets(),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
restoreSkillEnv?.();
|
restoreSkillEnv?.();
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ const log = createSubsystemLogger("agent/embedded");
|
|||||||
|
|
||||||
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
|
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
|
||||||
|
|
||||||
|
type MessagingToolSend = {
|
||||||
|
tool: string;
|
||||||
|
provider: string;
|
||||||
|
accountId?: string;
|
||||||
|
to?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function truncateToolText(text: string): string {
|
function truncateToolText(text: string): string {
|
||||||
if (text.length <= TOOL_RESULT_MAX_CHARS) return text;
|
if (text.length <= TOOL_RESULT_MAX_CHARS) return text;
|
||||||
return `${text.slice(0, TOOL_RESULT_MAX_CHARS)}\n…(truncated)…`;
|
return `${text.slice(0, TOOL_RESULT_MAX_CHARS)}\n…(truncated)…`;
|
||||||
@@ -86,6 +93,127 @@ function stripUnpairedThinkingTags(text: string): string {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSlackTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
||||||
|
if (mentionMatch) return `user:${mentionMatch[1]}`;
|
||||||
|
if (trimmed.startsWith("user:")) {
|
||||||
|
const id = trimmed.slice(5).trim();
|
||||||
|
return id ? `user:${id}` : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("channel:")) {
|
||||||
|
const id = trimmed.slice(8).trim();
|
||||||
|
return id ? `channel:${id}` : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("slack:")) {
|
||||||
|
const id = trimmed.slice(6).trim();
|
||||||
|
return id ? `user:${id}` : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("@")) {
|
||||||
|
const id = trimmed.slice(1).trim();
|
||||||
|
return id ? `user:${id}` : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("#")) {
|
||||||
|
const id = trimmed.slice(1).trim();
|
||||||
|
return id ? `channel:${id}` : undefined;
|
||||||
|
}
|
||||||
|
return `channel:${trimmed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDiscordTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
|
||||||
|
if (mentionMatch) return `user:${mentionMatch[1]}`;
|
||||||
|
if (trimmed.startsWith("user:")) {
|
||||||
|
const id = trimmed.slice(5).trim();
|
||||||
|
return id ? `user:${id}` : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("channel:")) {
|
||||||
|
const id = trimmed.slice(8).trim();
|
||||||
|
return id ? `channel:${id}` : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("discord:")) {
|
||||||
|
const id = trimmed.slice(8).trim();
|
||||||
|
return id ? `user:${id}` : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("@")) {
|
||||||
|
const id = trimmed.slice(1).trim();
|
||||||
|
return id ? `user:${id}` : undefined;
|
||||||
|
}
|
||||||
|
return `channel:${trimmed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTelegramTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
let normalized = trimmed;
|
||||||
|
if (normalized.startsWith("telegram:")) {
|
||||||
|
normalized = normalized.slice("telegram:".length).trim();
|
||||||
|
} else if (normalized.startsWith("tg:")) {
|
||||||
|
normalized = normalized.slice("tg:".length).trim();
|
||||||
|
} else if (normalized.startsWith("group:")) {
|
||||||
|
normalized = normalized.slice("group:".length).trim();
|
||||||
|
}
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
const tmeMatch =
|
||||||
|
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
|
||||||
|
/^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
|
||||||
|
if (tmeMatch?.[1]) normalized = `@${tmeMatch[1]}`;
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
return `telegram:${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMessagingToolSend(
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, unknown>,
|
||||||
|
): MessagingToolSend | undefined {
|
||||||
|
const action = typeof args.action === "string" ? args.action.trim() : "";
|
||||||
|
const accountIdRaw =
|
||||||
|
typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
||||||
|
const accountId = accountIdRaw ? accountIdRaw : undefined;
|
||||||
|
if (toolName === "slack") {
|
||||||
|
if (action !== "sendMessage") return undefined;
|
||||||
|
const toRaw = typeof args.to === "string" ? args.to : undefined;
|
||||||
|
if (!toRaw) return undefined;
|
||||||
|
const to = normalizeSlackTarget(toRaw);
|
||||||
|
return to
|
||||||
|
? { tool: toolName, provider: "slack", accountId, to }
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
if (toolName === "discord") {
|
||||||
|
if (action === "sendMessage") {
|
||||||
|
const toRaw = typeof args.to === "string" ? args.to : undefined;
|
||||||
|
if (!toRaw) return undefined;
|
||||||
|
const to = normalizeDiscordTarget(toRaw);
|
||||||
|
return to
|
||||||
|
? { tool: toolName, provider: "discord", accountId, to }
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
if (action === "threadReply") {
|
||||||
|
const channelId =
|
||||||
|
typeof args.channelId === "string" ? args.channelId.trim() : "";
|
||||||
|
if (!channelId) return undefined;
|
||||||
|
const to = normalizeDiscordTarget(`channel:${channelId}`);
|
||||||
|
return to
|
||||||
|
? { tool: toolName, provider: "discord", accountId, to }
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (toolName === "telegram") {
|
||||||
|
if (action !== "sendMessage") return undefined;
|
||||||
|
const toRaw = typeof args.to === "string" ? args.to : undefined;
|
||||||
|
if (!toRaw) return undefined;
|
||||||
|
const to = normalizeTelegramTarget(toRaw);
|
||||||
|
return to
|
||||||
|
? { tool: toolName, provider: "telegram", accountId, to }
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function subscribeEmbeddedPiSession(params: {
|
export function subscribeEmbeddedPiSession(params: {
|
||||||
session: AgentSession;
|
session: AgentSession;
|
||||||
runId: string;
|
runId: string;
|
||||||
@@ -151,7 +279,9 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
"sessions_send",
|
"sessions_send",
|
||||||
]);
|
]);
|
||||||
const messagingToolSentTexts: string[] = [];
|
const messagingToolSentTexts: string[] = [];
|
||||||
|
const messagingToolSentTargets: MessagingToolSend[] = [];
|
||||||
const pendingMessagingTexts = new Map<string, string>();
|
const pendingMessagingTexts = new Map<string, string>();
|
||||||
|
const pendingMessagingTargets = new Map<string, MessagingToolSend>();
|
||||||
|
|
||||||
const ensureCompactionPromise = () => {
|
const ensureCompactionPromise = () => {
|
||||||
if (!compactionRetryPromise) {
|
if (!compactionRetryPromise) {
|
||||||
@@ -315,7 +445,9 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
toolMetaById.clear();
|
toolMetaById.clear();
|
||||||
toolSummaryById.clear();
|
toolSummaryById.clear();
|
||||||
messagingToolSentTexts.length = 0;
|
messagingToolSentTexts.length = 0;
|
||||||
|
messagingToolSentTargets.length = 0;
|
||||||
pendingMessagingTexts.clear();
|
pendingMessagingTexts.clear();
|
||||||
|
pendingMessagingTargets.clear();
|
||||||
deltaBuffer = "";
|
deltaBuffer = "";
|
||||||
blockBuffer = "";
|
blockBuffer = "";
|
||||||
blockChunker?.reset();
|
blockChunker?.reset();
|
||||||
@@ -398,6 +530,10 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
action === "threadReply" ||
|
action === "threadReply" ||
|
||||||
toolName === "sessions_send"
|
toolName === "sessions_send"
|
||||||
) {
|
) {
|
||||||
|
const sendTarget = extractMessagingToolSend(toolName, argsRecord);
|
||||||
|
if (sendTarget) {
|
||||||
|
pendingMessagingTargets.set(toolCallId, sendTarget);
|
||||||
|
}
|
||||||
// Field names vary by tool: Discord/Slack use "content", sessions_send uses "message"
|
// Field names vary by tool: Discord/Slack use "content", sessions_send uses "message"
|
||||||
const text =
|
const text =
|
||||||
(argsRecord.content as string) ?? (argsRecord.message as string);
|
(argsRecord.content as string) ?? (argsRecord.message as string);
|
||||||
@@ -460,6 +596,7 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
|
|
||||||
// Commit messaging tool text on success, discard on error
|
// Commit messaging tool text on success, discard on error
|
||||||
const pendingText = pendingMessagingTexts.get(toolCallId);
|
const pendingText = pendingMessagingTexts.get(toolCallId);
|
||||||
|
const pendingTarget = pendingMessagingTargets.get(toolCallId);
|
||||||
if (pendingText) {
|
if (pendingText) {
|
||||||
pendingMessagingTexts.delete(toolCallId);
|
pendingMessagingTexts.delete(toolCallId);
|
||||||
if (!isError) {
|
if (!isError) {
|
||||||
@@ -469,6 +606,12 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pendingTarget) {
|
||||||
|
pendingMessagingTargets.delete(toolCallId);
|
||||||
|
if (!isError) {
|
||||||
|
messagingToolSentTargets.push(pendingTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId: params.runId,
|
runId: params.runId,
|
||||||
@@ -779,6 +922,7 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
unsubscribe,
|
unsubscribe,
|
||||||
isCompacting: () => compactionInFlight || pendingCompactionRetry > 0,
|
isCompacting: () => compactionInFlight || pendingCompactionRetry > 0,
|
||||||
getMessagingToolSentTexts: () => messagingToolSentTexts.slice(),
|
getMessagingToolSentTexts: () => messagingToolSentTexts.slice(),
|
||||||
|
getMessagingToolSentTargets: () => messagingToolSentTargets.slice(),
|
||||||
// Returns true if any messaging tool successfully sent a message.
|
// Returns true if any messaging tool successfully sent a message.
|
||||||
// Used to suppress agent's confirmation text (e.g., "Respondi no Telegram!")
|
// Used to suppress agent's confirmation text (e.g., "Respondi no Telegram!")
|
||||||
// which is generated AFTER the tool sends the actual answer.
|
// which is generated AFTER the tool sends the actual answer.
|
||||||
|
|||||||
@@ -625,6 +625,7 @@ function shouldIncludeWhatsAppTool(messageProvider?: string): boolean {
|
|||||||
export function createClawdbotCodingTools(options?: {
|
export function createClawdbotCodingTools(options?: {
|
||||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
|
agentAccountId?: string;
|
||||||
sandbox?: SandboxContext | null;
|
sandbox?: SandboxContext | null;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
@@ -695,6 +696,7 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
browserControlUrl: sandbox?.browser?.controlUrl,
|
browserControlUrl: sandbox?.browser?.controlUrl,
|
||||||
agentSessionKey: options?.sessionKey,
|
agentSessionKey: options?.sessionKey,
|
||||||
agentProvider: options?.messageProvider,
|
agentProvider: options?.messageProvider,
|
||||||
|
agentAccountId: options?.agentAccountId,
|
||||||
agentDir: options?.agentDir,
|
agentDir: options?.agentDir,
|
||||||
sandboxed: !!sandbox,
|
sandboxed: !!sandbox,
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||||
|
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||||
import {
|
import {
|
||||||
deleteSlackMessage,
|
deleteSlackMessage,
|
||||||
editSlackMessage,
|
editSlackMessage,
|
||||||
@@ -38,7 +39,11 @@ export async function handleSlackAction(
|
|||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
): Promise<AgentToolResult<unknown>> {
|
): Promise<AgentToolResult<unknown>> {
|
||||||
const action = readStringParam(params, "action", { required: true });
|
const action = readStringParam(params, "action", { required: true });
|
||||||
const isActionEnabled = createActionGate(cfg.slack?.actions);
|
const accountId = readStringParam(params, "accountId");
|
||||||
|
const accountOpts = accountId ? { accountId } : undefined;
|
||||||
|
const account = resolveSlackAccount({ cfg, accountId });
|
||||||
|
const actionConfig = account.actions ?? cfg.slack?.actions;
|
||||||
|
const isActionEnabled = createActionGate(actionConfig);
|
||||||
|
|
||||||
if (reactionsActions.has(action)) {
|
if (reactionsActions.has(action)) {
|
||||||
if (!isActionEnabled("reactions")) {
|
if (!isActionEnabled("reactions")) {
|
||||||
@@ -51,17 +56,29 @@ export async function handleSlackAction(
|
|||||||
removeErrorMessage: "Emoji is required to remove a Slack reaction.",
|
removeErrorMessage: "Emoji is required to remove a Slack reaction.",
|
||||||
});
|
});
|
||||||
if (remove) {
|
if (remove) {
|
||||||
await removeSlackReaction(channelId, messageId, emoji);
|
if (accountOpts) {
|
||||||
|
await removeSlackReaction(channelId, messageId, emoji, accountOpts);
|
||||||
|
} else {
|
||||||
|
await removeSlackReaction(channelId, messageId, emoji);
|
||||||
|
}
|
||||||
return jsonResult({ ok: true, removed: emoji });
|
return jsonResult({ ok: true, removed: emoji });
|
||||||
}
|
}
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
const removed = await removeOwnSlackReactions(channelId, messageId);
|
const removed = accountOpts
|
||||||
|
? await removeOwnSlackReactions(channelId, messageId, accountOpts)
|
||||||
|
: await removeOwnSlackReactions(channelId, messageId);
|
||||||
return jsonResult({ ok: true, removed });
|
return jsonResult({ ok: true, removed });
|
||||||
}
|
}
|
||||||
await reactSlackMessage(channelId, messageId, emoji);
|
if (accountOpts) {
|
||||||
|
await reactSlackMessage(channelId, messageId, emoji, accountOpts);
|
||||||
|
} else {
|
||||||
|
await reactSlackMessage(channelId, messageId, emoji);
|
||||||
|
}
|
||||||
return jsonResult({ ok: true, added: emoji });
|
return jsonResult({ ok: true, added: emoji });
|
||||||
}
|
}
|
||||||
const reactions = await listSlackReactions(channelId, messageId);
|
const reactions = accountOpts
|
||||||
|
? await listSlackReactions(channelId, messageId, accountOpts)
|
||||||
|
: await listSlackReactions(channelId, messageId);
|
||||||
return jsonResult({ ok: true, reactions });
|
return jsonResult({ ok: true, reactions });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +92,7 @@ export async function handleSlackAction(
|
|||||||
const content = readStringParam(params, "content", { required: true });
|
const content = readStringParam(params, "content", { required: true });
|
||||||
const mediaUrl = readStringParam(params, "mediaUrl");
|
const mediaUrl = readStringParam(params, "mediaUrl");
|
||||||
const result = await sendSlackMessage(to, content, {
|
const result = await sendSlackMessage(to, content, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
mediaUrl: mediaUrl ?? undefined,
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
});
|
});
|
||||||
return jsonResult({ ok: true, result });
|
return jsonResult({ ok: true, result });
|
||||||
@@ -89,7 +107,11 @@ export async function handleSlackAction(
|
|||||||
const content = readStringParam(params, "content", {
|
const content = readStringParam(params, "content", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
await editSlackMessage(channelId, messageId, content);
|
if (accountOpts) {
|
||||||
|
await editSlackMessage(channelId, messageId, content, accountOpts);
|
||||||
|
} else {
|
||||||
|
await editSlackMessage(channelId, messageId, content);
|
||||||
|
}
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
case "deleteMessage": {
|
case "deleteMessage": {
|
||||||
@@ -99,7 +121,11 @@ export async function handleSlackAction(
|
|||||||
const messageId = readStringParam(params, "messageId", {
|
const messageId = readStringParam(params, "messageId", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
await deleteSlackMessage(channelId, messageId);
|
if (accountOpts) {
|
||||||
|
await deleteSlackMessage(channelId, messageId, accountOpts);
|
||||||
|
} else {
|
||||||
|
await deleteSlackMessage(channelId, messageId);
|
||||||
|
}
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
case "readMessages": {
|
case "readMessages": {
|
||||||
@@ -114,6 +140,7 @@ export async function handleSlackAction(
|
|||||||
const before = readStringParam(params, "before");
|
const before = readStringParam(params, "before");
|
||||||
const after = readStringParam(params, "after");
|
const after = readStringParam(params, "after");
|
||||||
const result = await readSlackMessages(channelId, {
|
const result = await readSlackMessages(channelId, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
limit,
|
limit,
|
||||||
before: before ?? undefined,
|
before: before ?? undefined,
|
||||||
after: after ?? undefined,
|
after: after ?? undefined,
|
||||||
@@ -134,17 +161,27 @@ export async function handleSlackAction(
|
|||||||
const messageId = readStringParam(params, "messageId", {
|
const messageId = readStringParam(params, "messageId", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
await pinSlackMessage(channelId, messageId);
|
if (accountOpts) {
|
||||||
|
await pinSlackMessage(channelId, messageId, accountOpts);
|
||||||
|
} else {
|
||||||
|
await pinSlackMessage(channelId, messageId);
|
||||||
|
}
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
if (action === "unpinMessage") {
|
if (action === "unpinMessage") {
|
||||||
const messageId = readStringParam(params, "messageId", {
|
const messageId = readStringParam(params, "messageId", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
await unpinSlackMessage(channelId, messageId);
|
if (accountOpts) {
|
||||||
|
await unpinSlackMessage(channelId, messageId, accountOpts);
|
||||||
|
} else {
|
||||||
|
await unpinSlackMessage(channelId, messageId);
|
||||||
|
}
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
const pins = await listSlackPins(channelId);
|
const pins = accountOpts
|
||||||
|
? await listSlackPins(channelId, accountOpts)
|
||||||
|
: await listSlackPins(channelId);
|
||||||
return jsonResult({ ok: true, pins });
|
return jsonResult({ ok: true, pins });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +190,9 @@ export async function handleSlackAction(
|
|||||||
throw new Error("Slack member info is disabled.");
|
throw new Error("Slack member info is disabled.");
|
||||||
}
|
}
|
||||||
const userId = readStringParam(params, "userId", { required: true });
|
const userId = readStringParam(params, "userId", { required: true });
|
||||||
const info = await getSlackMemberInfo(userId);
|
const info = accountOpts
|
||||||
|
? await getSlackMemberInfo(userId, accountOpts)
|
||||||
|
: await getSlackMemberInfo(userId);
|
||||||
return jsonResult({ ok: true, info });
|
return jsonResult({ ok: true, info });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +200,9 @@ export async function handleSlackAction(
|
|||||||
if (!isActionEnabled("emojiList")) {
|
if (!isActionEnabled("emojiList")) {
|
||||||
throw new Error("Slack emoji list is disabled.");
|
throw new Error("Slack emoji list is disabled.");
|
||||||
}
|
}
|
||||||
const emojis = await listSlackEmojis();
|
const emojis = accountOpts
|
||||||
|
? await listSlackEmojis(accountOpts)
|
||||||
|
: await listSlackEmojis();
|
||||||
return jsonResult({ ok: true, emojis });
|
return jsonResult({ ok: true, emojis });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,28 +9,35 @@ export const SlackToolSchema = Type.Union([
|
|||||||
messageId: Type.String(),
|
messageId: Type.String(),
|
||||||
},
|
},
|
||||||
includeRemove: true,
|
includeRemove: true,
|
||||||
|
extras: {
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("reactions"),
|
action: Type.Literal("reactions"),
|
||||||
channelId: Type.String(),
|
channelId: Type.String(),
|
||||||
messageId: Type.String(),
|
messageId: Type.String(),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("sendMessage"),
|
action: Type.Literal("sendMessage"),
|
||||||
to: Type.String(),
|
to: Type.String(),
|
||||||
content: Type.String(),
|
content: Type.String(),
|
||||||
mediaUrl: Type.Optional(Type.String()),
|
mediaUrl: Type.Optional(Type.String()),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("editMessage"),
|
action: Type.Literal("editMessage"),
|
||||||
channelId: Type.String(),
|
channelId: Type.String(),
|
||||||
messageId: Type.String(),
|
messageId: Type.String(),
|
||||||
content: Type.String(),
|
content: Type.String(),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("deleteMessage"),
|
action: Type.Literal("deleteMessage"),
|
||||||
channelId: Type.String(),
|
channelId: Type.String(),
|
||||||
messageId: Type.String(),
|
messageId: Type.String(),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("readMessages"),
|
action: Type.Literal("readMessages"),
|
||||||
@@ -38,26 +45,32 @@ export const SlackToolSchema = Type.Union([
|
|||||||
limit: Type.Optional(Type.Number()),
|
limit: Type.Optional(Type.Number()),
|
||||||
before: Type.Optional(Type.String()),
|
before: Type.Optional(Type.String()),
|
||||||
after: Type.Optional(Type.String()),
|
after: Type.Optional(Type.String()),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("pinMessage"),
|
action: Type.Literal("pinMessage"),
|
||||||
channelId: Type.String(),
|
channelId: Type.String(),
|
||||||
messageId: Type.String(),
|
messageId: Type.String(),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("unpinMessage"),
|
action: Type.Literal("unpinMessage"),
|
||||||
channelId: Type.String(),
|
channelId: Type.String(),
|
||||||
messageId: Type.String(),
|
messageId: Type.String(),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("listPins"),
|
action: Type.Literal("listPins"),
|
||||||
channelId: Type.String(),
|
channelId: Type.String(),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("memberInfo"),
|
action: Type.Literal("memberInfo"),
|
||||||
userId: Type.String(),
|
userId: Type.String(),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("emojiList"),
|
action: Type.Literal("emojiList"),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
85
src/agents/tools/slack-tool.test.ts
Normal file
85
src/agents/tools/slack-tool.test.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const handleSlackActionMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("./slack-actions.js", () => ({
|
||||||
|
handleSlackAction: (params: unknown, cfg: unknown) =>
|
||||||
|
handleSlackActionMock(params, cfg),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createSlackTool } from "./slack-tool.js";
|
||||||
|
|
||||||
|
describe("slack tool", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
handleSlackActionMock.mockReset();
|
||||||
|
handleSlackActionMock.mockResolvedValue({
|
||||||
|
content: [],
|
||||||
|
details: { ok: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects agentAccountId when accountId is missing", async () => {
|
||||||
|
const tool = createSlackTool({
|
||||||
|
agentAccountId: " Kev ",
|
||||||
|
config: { slack: { accounts: { kev: {} } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await tool.execute("call-1", {
|
||||||
|
action: "sendMessage",
|
||||||
|
to: "channel:C1",
|
||||||
|
content: "hello",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleSlackActionMock).toHaveBeenCalledTimes(1);
|
||||||
|
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
|
||||||
|
expect(params).toMatchObject({ accountId: "kev" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps explicit accountId when provided", async () => {
|
||||||
|
const tool = createSlackTool({
|
||||||
|
agentAccountId: "kev",
|
||||||
|
config: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tool.execute("call-2", {
|
||||||
|
action: "sendMessage",
|
||||||
|
to: "channel:C1",
|
||||||
|
content: "hello",
|
||||||
|
accountId: "rex",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleSlackActionMock).toHaveBeenCalledTimes(1);
|
||||||
|
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
|
||||||
|
expect(params).toMatchObject({ accountId: "rex" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not inject accountId when agentAccountId is missing", async () => {
|
||||||
|
const tool = createSlackTool({ config: {} });
|
||||||
|
|
||||||
|
await tool.execute("call-3", {
|
||||||
|
action: "sendMessage",
|
||||||
|
to: "channel:C1",
|
||||||
|
content: "hello",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleSlackActionMock).toHaveBeenCalledTimes(1);
|
||||||
|
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
|
||||||
|
expect(params).not.toHaveProperty("accountId");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not inject unknown agentAccountId when not configured", async () => {
|
||||||
|
const tool = createSlackTool({
|
||||||
|
agentAccountId: "unknown",
|
||||||
|
config: { slack: { accounts: { kev: {} } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await tool.execute("call-4", {
|
||||||
|
action: "sendMessage",
|
||||||
|
to: "channel:C1",
|
||||||
|
content: "hello",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
|
||||||
|
expect(params).not.toHaveProperty("accountId");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,44 @@
|
|||||||
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import { logVerbose } from "../../globals.js";
|
||||||
|
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||||
import type { AnyAgentTool } from "./common.js";
|
import type { AnyAgentTool } from "./common.js";
|
||||||
import { handleSlackAction } from "./slack-actions.js";
|
import { handleSlackAction } from "./slack-actions.js";
|
||||||
import { SlackToolSchema } from "./slack-schema.js";
|
import { SlackToolSchema } from "./slack-schema.js";
|
||||||
|
|
||||||
export function createSlackTool(): AnyAgentTool {
|
type SlackToolOptions = {
|
||||||
|
agentAccountId?: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveAgentAccountId(value?: string): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
return normalizeAccountId(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveConfiguredAccountId(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
accountId: string,
|
||||||
|
): string | undefined {
|
||||||
|
if (accountId === "default") return accountId;
|
||||||
|
const accounts = cfg.slack?.accounts;
|
||||||
|
if (!accounts || typeof accounts !== "object") return undefined;
|
||||||
|
if (accountId in accounts) return accountId;
|
||||||
|
const match = Object.keys(accounts).find(
|
||||||
|
(key) => key.toLowerCase() === accountId.toLowerCase(),
|
||||||
|
);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAccountId(params: Record<string, unknown>): boolean {
|
||||||
|
const raw = params.accountId;
|
||||||
|
if (typeof raw !== "string") return false;
|
||||||
|
return raw.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSlackTool(options?: SlackToolOptions): AnyAgentTool {
|
||||||
|
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
|
||||||
return {
|
return {
|
||||||
label: "Slack",
|
label: "Slack",
|
||||||
name: "slack",
|
name: "slack",
|
||||||
@@ -11,8 +46,24 @@ export function createSlackTool(): AnyAgentTool {
|
|||||||
parameters: SlackToolSchema,
|
parameters: SlackToolSchema,
|
||||||
execute: async (_toolCallId, args) => {
|
execute: async (_toolCallId, args) => {
|
||||||
const params = args as Record<string, unknown>;
|
const params = args as Record<string, unknown>;
|
||||||
const cfg = loadConfig();
|
const cfg = options?.config ?? loadConfig();
|
||||||
return await handleSlackAction(params, cfg);
|
const resolvedAccountId = agentAccountId
|
||||||
|
? resolveConfiguredAccountId(cfg, agentAccountId)
|
||||||
|
: undefined;
|
||||||
|
const resolvedParams =
|
||||||
|
resolvedAccountId && !hasAccountId(params)
|
||||||
|
? { ...params, accountId: resolvedAccountId }
|
||||||
|
: params;
|
||||||
|
if (hasAccountId(resolvedParams)) {
|
||||||
|
const action =
|
||||||
|
typeof params.action === "string" ? params.action : "unknown";
|
||||||
|
logVerbose(
|
||||||
|
`slack tool: action=${action} accountId=${String(
|
||||||
|
resolvedParams.accountId,
|
||||||
|
).trim()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await handleSlackAction(resolvedParams, cfg);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -791,6 +791,7 @@ export async function getReplyFromConfig(
|
|||||||
sessionId: sessionIdFinal,
|
sessionId: sessionIdFinal,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||||
|
agentAccountId: sessionCtx.AccountId,
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
|||||||
149
src/auto-reply/reply/agent-runner.messaging-tools.test.ts
Normal file
149
src/auto-reply/reply/agent-runner.messaging-tools.test.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { TemplateContext } from "../templating.js";
|
||||||
|
import type { FollowupRun, QueueSettings } from "./queue.js";
|
||||||
|
import { createMockTypingController } from "./test-helpers.js";
|
||||||
|
|
||||||
|
const runEmbeddedPiAgentMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("../../agents/model-fallback.js", () => ({
|
||||||
|
runWithModelFallback: async ({
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
run,
|
||||||
|
}: {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
run: (provider: string, model: string) => Promise<unknown>;
|
||||||
|
}) => ({
|
||||||
|
result: await run(provider, model),
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../agents/pi-embedded.js", () => ({
|
||||||
|
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||||
|
runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./queue.js", async () => {
|
||||||
|
const actual =
|
||||||
|
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
enqueueFollowupRun: vi.fn(),
|
||||||
|
scheduleFollowupDrain: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { runReplyAgent } from "./agent-runner.js";
|
||||||
|
|
||||||
|
function createRun(messageProvider = "slack") {
|
||||||
|
const typing = createMockTypingController();
|
||||||
|
const sessionCtx = {
|
||||||
|
Provider: messageProvider,
|
||||||
|
OriginatingTo: "channel:C1",
|
||||||
|
AccountId: "primary",
|
||||||
|
MessageSid: "msg",
|
||||||
|
} as unknown as TemplateContext;
|
||||||
|
const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings;
|
||||||
|
const followupRun = {
|
||||||
|
prompt: "hello",
|
||||||
|
summaryLine: "hello",
|
||||||
|
enqueuedAt: Date.now(),
|
||||||
|
run: {
|
||||||
|
sessionId: "session",
|
||||||
|
sessionKey: "main",
|
||||||
|
messageProvider,
|
||||||
|
sessionFile: "/tmp/session.jsonl",
|
||||||
|
workspaceDir: "/tmp",
|
||||||
|
config: {},
|
||||||
|
skillsSnapshot: {},
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude",
|
||||||
|
thinkLevel: "low",
|
||||||
|
verboseLevel: "off",
|
||||||
|
elevatedLevel: "off",
|
||||||
|
bashElevated: {
|
||||||
|
enabled: false,
|
||||||
|
allowed: false,
|
||||||
|
defaultLevel: "off",
|
||||||
|
},
|
||||||
|
timeoutMs: 1_000,
|
||||||
|
blockReplyBreak: "message_end",
|
||||||
|
},
|
||||||
|
} as unknown as FollowupRun;
|
||||||
|
|
||||||
|
return runReplyAgent({
|
||||||
|
commandBody: "hello",
|
||||||
|
followupRun,
|
||||||
|
queueKey: "main",
|
||||||
|
resolvedQueue,
|
||||||
|
shouldSteer: false,
|
||||||
|
shouldFollowup: false,
|
||||||
|
isActive: false,
|
||||||
|
isStreaming: false,
|
||||||
|
typing,
|
||||||
|
sessionCtx,
|
||||||
|
defaultModel: "anthropic/claude-opus-4-5",
|
||||||
|
resolvedVerboseLevel: "off",
|
||||||
|
isNewSession: false,
|
||||||
|
blockStreamingEnabled: false,
|
||||||
|
resolvedBlockStreamingBreak: "message_end",
|
||||||
|
shouldInjectGroupIntro: false,
|
||||||
|
typingMode: "instant",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("runReplyAgent messaging tool suppression", () => {
|
||||||
|
it("drops replies when a messaging tool sent via the same provider + target", async () => {
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||||
|
payloads: [{ text: "hello world!" }],
|
||||||
|
messagingToolSentTexts: ["different message"],
|
||||||
|
messagingToolSentTargets: [
|
||||||
|
{ tool: "slack", provider: "slack", to: "channel:C1" },
|
||||||
|
],
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createRun("slack");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delivers replies when tool provider does not match", async () => {
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||||
|
payloads: [{ text: "hello world!" }],
|
||||||
|
messagingToolSentTexts: ["different message"],
|
||||||
|
messagingToolSentTargets: [
|
||||||
|
{ tool: "discord", provider: "discord", to: "channel:C1" },
|
||||||
|
],
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createRun("slack");
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ text: "hello world!" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delivers replies when account ids do not match", async () => {
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||||
|
payloads: [{ text: "hello world!" }],
|
||||||
|
messagingToolSentTexts: ["different message"],
|
||||||
|
messagingToolSentTargets: [
|
||||||
|
{
|
||||||
|
tool: "slack",
|
||||||
|
provider: "slack",
|
||||||
|
to: "channel:C1",
|
||||||
|
accountId: "alt",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createRun("slack");
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ text: "hello world!" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
applyReplyThreading,
|
applyReplyThreading,
|
||||||
filterMessagingToolDuplicates,
|
filterMessagingToolDuplicates,
|
||||||
isRenderablePayload,
|
isRenderablePayload,
|
||||||
|
shouldSuppressMessagingToolReplies,
|
||||||
} from "./reply-payloads.js";
|
} from "./reply-payloads.js";
|
||||||
import {
|
import {
|
||||||
createReplyToModeFilter,
|
createReplyToModeFilter,
|
||||||
@@ -240,6 +241,7 @@ export async function runReplyAgent(params: {
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
messageProvider:
|
messageProvider:
|
||||||
sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||||
|
agentAccountId: sessionCtx.AccountId,
|
||||||
sessionFile: followupRun.run.sessionFile,
|
sessionFile: followupRun.run.sessionFile,
|
||||||
workspaceDir: followupRun.run.workspaceDir,
|
workspaceDir: followupRun.run.workspaceDir,
|
||||||
agentDir: followupRun.run.agentDir,
|
agentDir: followupRun.run.agentDir,
|
||||||
@@ -549,6 +551,13 @@ export async function runReplyAgent(params: {
|
|||||||
const shouldDropFinalPayloads =
|
const shouldDropFinalPayloads =
|
||||||
blockStreamingEnabled && didStreamBlockReply;
|
blockStreamingEnabled && didStreamBlockReply;
|
||||||
const messagingToolSentTexts = runResult.messagingToolSentTexts ?? [];
|
const messagingToolSentTexts = runResult.messagingToolSentTexts ?? [];
|
||||||
|
const messagingToolSentTargets = runResult.messagingToolSentTargets ?? [];
|
||||||
|
const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
|
||||||
|
messageProvider: followupRun.run.messageProvider,
|
||||||
|
messagingToolSentTargets,
|
||||||
|
originatingTo: sessionCtx.OriginatingTo ?? sessionCtx.To,
|
||||||
|
accountId: sessionCtx.AccountId,
|
||||||
|
});
|
||||||
const dedupedPayloads = filterMessagingToolDuplicates({
|
const dedupedPayloads = filterMessagingToolDuplicates({
|
||||||
payloads: replyTaggedPayloads,
|
payloads: replyTaggedPayloads,
|
||||||
sentTexts: messagingToolSentTexts,
|
sentTexts: messagingToolSentTexts,
|
||||||
@@ -560,10 +569,11 @@ export async function runReplyAgent(params: {
|
|||||||
(payload) => !streamedPayloadKeys.has(buildPayloadKey(payload)),
|
(payload) => !streamedPayloadKeys.has(buildPayloadKey(payload)),
|
||||||
)
|
)
|
||||||
: dedupedPayloads;
|
: dedupedPayloads;
|
||||||
|
const replyPayloads = suppressMessagingToolReplies ? [] : filteredPayloads;
|
||||||
|
|
||||||
if (filteredPayloads.length === 0) return finalizeWithFollowup(undefined);
|
if (replyPayloads.length === 0) return finalizeWithFollowup(undefined);
|
||||||
|
|
||||||
const shouldSignalTyping = filteredPayloads.some((payload) => {
|
const shouldSignalTyping = replyPayloads.some((payload) => {
|
||||||
const trimmed = payload.text?.trim();
|
const trimmed = payload.text?.trim();
|
||||||
if (trimmed && trimmed !== SILENT_REPLY_TOKEN) return true;
|
if (trimmed && trimmed !== SILENT_REPLY_TOKEN) return true;
|
||||||
if (payload.mediaUrl) return true;
|
if (payload.mediaUrl) return true;
|
||||||
@@ -628,7 +638,7 @@ export async function runReplyAgent(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If verbose is enabled and this is a new session, prepend a session hint.
|
// If verbose is enabled and this is a new session, prepend a session hint.
|
||||||
let finalPayloads = filteredPayloads;
|
let finalPayloads = replyPayloads;
|
||||||
if (autoCompactionCompleted) {
|
if (autoCompactionCompleted) {
|
||||||
const count = await incrementCompactionCount({
|
const count = await incrementCompactionCount({
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
|
|||||||
@@ -27,15 +27,17 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
|||||||
|
|
||||||
import { createFollowupRunner } from "./followup-runner.js";
|
import { createFollowupRunner } from "./followup-runner.js";
|
||||||
|
|
||||||
const baseQueuedRun = (): FollowupRun =>
|
const baseQueuedRun = (messageProvider = "whatsapp"): FollowupRun =>
|
||||||
({
|
({
|
||||||
prompt: "hello",
|
prompt: "hello",
|
||||||
summaryLine: "hello",
|
summaryLine: "hello",
|
||||||
enqueuedAt: Date.now(),
|
enqueuedAt: Date.now(),
|
||||||
|
originatingTo: "channel:C1",
|
||||||
run: {
|
run: {
|
||||||
sessionId: "session",
|
sessionId: "session",
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
messageProvider: "whatsapp",
|
messageProvider,
|
||||||
|
agentAccountId: "primary",
|
||||||
sessionFile: "/tmp/session.jsonl",
|
sessionFile: "/tmp/session.jsonl",
|
||||||
workspaceDir: "/tmp",
|
workspaceDir: "/tmp",
|
||||||
config: {},
|
config: {},
|
||||||
@@ -95,4 +97,27 @@ describe("createFollowupRunner messaging tool dedupe", () => {
|
|||||||
|
|
||||||
expect(onBlockReply).toHaveBeenCalledTimes(1);
|
expect(onBlockReply).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("suppresses replies when a messaging tool sent via the same provider + target", async () => {
|
||||||
|
const onBlockReply = vi.fn(async () => {});
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||||
|
payloads: [{ text: "hello world!" }],
|
||||||
|
messagingToolSentTexts: ["different message"],
|
||||||
|
messagingToolSentTargets: [
|
||||||
|
{ tool: "slack", provider: "slack", to: "channel:C1" },
|
||||||
|
],
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const runner = createFollowupRunner({
|
||||||
|
opts: { onBlockReply },
|
||||||
|
typing: createMockTypingController(),
|
||||||
|
typingMode: "instant",
|
||||||
|
defaultModel: "anthropic/claude-opus-4-5",
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner(baseQueuedRun("slack"));
|
||||||
|
|
||||||
|
expect(onBlockReply).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { FollowupRun } from "./queue.js";
|
|||||||
import {
|
import {
|
||||||
applyReplyThreading,
|
applyReplyThreading,
|
||||||
filterMessagingToolDuplicates,
|
filterMessagingToolDuplicates,
|
||||||
|
shouldSuppressMessagingToolReplies,
|
||||||
} from "./reply-payloads.js";
|
} from "./reply-payloads.js";
|
||||||
import {
|
import {
|
||||||
createReplyToModeFilter,
|
createReplyToModeFilter,
|
||||||
@@ -136,6 +137,7 @@ export function createFollowupRunner(params: {
|
|||||||
sessionId: queued.run.sessionId,
|
sessionId: queued.run.sessionId,
|
||||||
sessionKey: queued.run.sessionKey,
|
sessionKey: queued.run.sessionKey,
|
||||||
messageProvider: queued.run.messageProvider,
|
messageProvider: queued.run.messageProvider,
|
||||||
|
agentAccountId: queued.run.agentAccountId,
|
||||||
sessionFile: queued.run.sessionFile,
|
sessionFile: queued.run.sessionFile,
|
||||||
workspaceDir: queued.run.workspaceDir,
|
workspaceDir: queued.run.workspaceDir,
|
||||||
config: queued.run.config,
|
config: queued.run.config,
|
||||||
@@ -205,8 +207,15 @@ export function createFollowupRunner(params: {
|
|||||||
payloads: replyTaggedPayloads,
|
payloads: replyTaggedPayloads,
|
||||||
sentTexts: runResult.messagingToolSentTexts ?? [],
|
sentTexts: runResult.messagingToolSentTexts ?? [],
|
||||||
});
|
});
|
||||||
|
const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
|
||||||
|
messageProvider: queued.run.messageProvider,
|
||||||
|
messagingToolSentTargets: runResult.messagingToolSentTargets,
|
||||||
|
originatingTo: queued.originatingTo,
|
||||||
|
accountId: queued.run.agentAccountId,
|
||||||
|
});
|
||||||
|
const finalPayloads = suppressMessagingToolReplies ? [] : dedupedPayloads;
|
||||||
|
|
||||||
if (dedupedPayloads.length === 0) return;
|
if (finalPayloads.length === 0) return;
|
||||||
|
|
||||||
if (autoCompactionCompleted) {
|
if (autoCompactionCompleted) {
|
||||||
const count = await incrementCompactionCount({
|
const count = await incrementCompactionCount({
|
||||||
@@ -217,7 +226,7 @@ export function createFollowupRunner(params: {
|
|||||||
});
|
});
|
||||||
if (queued.run.verboseLevel === "on") {
|
if (queued.run.verboseLevel === "on") {
|
||||||
const suffix = typeof count === "number" ? ` (count ${count})` : "";
|
const suffix = typeof count === "number" ? ` (count ${count})` : "";
|
||||||
replyTaggedPayloads.unshift({
|
finalPayloads.unshift({
|
||||||
text: `🧹 Auto-compaction complete${suffix}.`,
|
text: `🧹 Auto-compaction complete${suffix}.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -271,7 +280,7 @@ export function createFollowupRunner(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendFollowupPayloads(dedupedPayloads, queued);
|
await sendFollowupPayloads(finalPayloads, queued);
|
||||||
} finally {
|
} finally {
|
||||||
typing.markRunComplete();
|
typing.markRunComplete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
GroupKeyResolution,
|
GroupKeyResolution,
|
||||||
SessionEntry,
|
SessionEntry,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
|
import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||||
import { normalizeGroupActivation } from "../group-activation.js";
|
import { normalizeGroupActivation } from "../group-activation.js";
|
||||||
import type { TemplateContext } from "../templating.js";
|
import type { TemplateContext } from "../templating.js";
|
||||||
|
|
||||||
@@ -148,7 +149,8 @@ export function resolveGroupRequireMention(params: {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (provider === "slack") {
|
if (provider === "slack") {
|
||||||
const channels = cfg.slack?.channels ?? {};
|
const account = resolveSlackAccount({ cfg, accountId: ctx.AccountId });
|
||||||
|
const channels = account.channels ?? {};
|
||||||
const keys = Object.keys(channels);
|
const keys = Object.keys(channels);
|
||||||
if (keys.length === 0) return true;
|
if (keys.length === 0) return true;
|
||||||
const channelId = groupId?.trim();
|
const channelId = groupId?.trim();
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export type FollowupRun = {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
|
agentAccountId?: string;
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
config: ClawdbotConfig;
|
config: ClawdbotConfig;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js";
|
import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js";
|
||||||
|
import type { MessagingToolSend } from "../../agents/pi-embedded-runner.js";
|
||||||
import type { ReplyPayload } from "../types.js";
|
import type { ReplyPayload } from "../types.js";
|
||||||
import { extractReplyToTag } from "./reply-tags.js";
|
import { extractReplyToTag } from "./reply-tags.js";
|
||||||
|
|
||||||
@@ -50,3 +51,126 @@ export function filterMessagingToolDuplicates(params: {
|
|||||||
(payload) => !isMessagingToolDuplicate(payload.text ?? "", sentTexts),
|
(payload) => !isMessagingToolDuplicate(payload.text ?? "", sentTexts),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSlackTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
||||||
|
if (mentionMatch) return `user:${mentionMatch[1]}`.toLowerCase();
|
||||||
|
if (trimmed.startsWith("user:")) {
|
||||||
|
const id = trimmed.slice(5).trim();
|
||||||
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("channel:")) {
|
||||||
|
const id = trimmed.slice(8).trim();
|
||||||
|
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("slack:")) {
|
||||||
|
const id = trimmed.slice(6).trim();
|
||||||
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("@")) {
|
||||||
|
const id = trimmed.slice(1).trim();
|
||||||
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("#")) {
|
||||||
|
const id = trimmed.slice(1).trim();
|
||||||
|
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
return `channel:${trimmed}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDiscordTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
|
||||||
|
if (mentionMatch) return `user:${mentionMatch[1]}`.toLowerCase();
|
||||||
|
if (trimmed.startsWith("user:")) {
|
||||||
|
const id = trimmed.slice(5).trim();
|
||||||
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("channel:")) {
|
||||||
|
const id = trimmed.slice(8).trim();
|
||||||
|
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("discord:")) {
|
||||||
|
const id = trimmed.slice(8).trim();
|
||||||
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("@")) {
|
||||||
|
const id = trimmed.slice(1).trim();
|
||||||
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
return `channel:${trimmed}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTelegramTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
let normalized = trimmed;
|
||||||
|
if (normalized.startsWith("telegram:")) {
|
||||||
|
normalized = normalized.slice("telegram:".length).trim();
|
||||||
|
} else if (normalized.startsWith("tg:")) {
|
||||||
|
normalized = normalized.slice("tg:".length).trim();
|
||||||
|
} else if (normalized.startsWith("group:")) {
|
||||||
|
normalized = normalized.slice("group:".length).trim();
|
||||||
|
}
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
const tmeMatch =
|
||||||
|
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
|
||||||
|
/^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
|
||||||
|
if (tmeMatch?.[1]) normalized = `@${tmeMatch[1]}`;
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
return `telegram:${normalized}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTargetForProvider(
|
||||||
|
provider: string,
|
||||||
|
raw?: string,
|
||||||
|
): string | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
switch (provider) {
|
||||||
|
case "slack":
|
||||||
|
return normalizeSlackTarget(raw);
|
||||||
|
case "discord":
|
||||||
|
return normalizeDiscordTarget(raw);
|
||||||
|
case "telegram":
|
||||||
|
return normalizeTelegramTarget(raw);
|
||||||
|
default:
|
||||||
|
return raw.trim().toLowerCase() || undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAccountId(value?: string): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
return trimmed ? trimmed.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldSuppressMessagingToolReplies(params: {
|
||||||
|
messageProvider?: string;
|
||||||
|
messagingToolSentTargets?: MessagingToolSend[];
|
||||||
|
originatingTo?: string;
|
||||||
|
accountId?: string;
|
||||||
|
}): boolean {
|
||||||
|
const provider = params.messageProvider?.trim().toLowerCase();
|
||||||
|
if (!provider) return false;
|
||||||
|
const originTarget = normalizeTargetForProvider(
|
||||||
|
provider,
|
||||||
|
params.originatingTo,
|
||||||
|
);
|
||||||
|
if (!originTarget) return false;
|
||||||
|
const originAccount = normalizeAccountId(params.accountId);
|
||||||
|
const sentTargets = params.messagingToolSentTargets ?? [];
|
||||||
|
if (sentTargets.length === 0) return false;
|
||||||
|
return sentTargets.some((target) => {
|
||||||
|
if (!target?.provider) return false;
|
||||||
|
if (target.provider.trim().toLowerCase() !== provider) return false;
|
||||||
|
const targetKey = normalizeTargetForProvider(provider, target.to);
|
||||||
|
if (!targetKey) return false;
|
||||||
|
const targetAccount = normalizeAccountId(target.accountId);
|
||||||
|
if (originAccount && targetAccount && originAccount !== targetAccount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return targetKey === originTarget;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
|||||||
? createOscProgressController({
|
? createOscProgressController({
|
||||||
env: process.env,
|
env: process.env,
|
||||||
isTty: stream.isTTY,
|
isTty: stream.isTTY,
|
||||||
write: (chunk) => stream.write(chunk),
|
write: (chunk: string) => stream.write(chunk),
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"discord.retry.jitter": "Discord Retry Jitter",
|
"discord.retry.jitter": "Discord Retry Jitter",
|
||||||
"discord.maxLinesPerMessage": "Discord Max Lines Per Message",
|
"discord.maxLinesPerMessage": "Discord Max Lines Per Message",
|
||||||
"slack.dm.policy": "Slack DM Policy",
|
"slack.dm.policy": "Slack DM Policy",
|
||||||
|
"slack.allowBots": "Slack Allow Bot Messages",
|
||||||
"discord.token": "Discord Bot Token",
|
"discord.token": "Discord Bot Token",
|
||||||
"slack.botToken": "Slack Bot Token",
|
"slack.botToken": "Slack Bot Token",
|
||||||
"slack.appToken": "Slack App Token",
|
"slack.appToken": "Slack App Token",
|
||||||
@@ -141,6 +142,8 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
'Hot reload strategy for config changes ("hybrid" recommended).',
|
'Hot reload strategy for config changes ("hybrid" recommended).',
|
||||||
"gateway.reload.debounceMs":
|
"gateway.reload.debounceMs":
|
||||||
"Debounce window (ms) before applying config changes.",
|
"Debounce window (ms) before applying config changes.",
|
||||||
|
"slack.allowBots":
|
||||||
|
"Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||||
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
|
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
|
||||||
"auth.order":
|
"auth.order":
|
||||||
"Ordered auth profile IDs per provider (used for automatic failover).",
|
"Ordered auth profile IDs per provider (used for automatic failover).",
|
||||||
|
|||||||
@@ -455,6 +455,8 @@ export type SlackChannelConfig = {
|
|||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
/** Require mentioning the bot to trigger replies. */
|
/** Require mentioning the bot to trigger replies. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
/** Allow bot-authored messages to trigger replies (default: false). */
|
||||||
|
allowBots?: boolean;
|
||||||
/** Allowlist of users that can invoke the bot in this channel. */
|
/** Allowlist of users that can invoke the bot in this channel. */
|
||||||
users?: Array<string | number>;
|
users?: Array<string | number>;
|
||||||
/** Optional skill filter for this channel. */
|
/** Optional skill filter for this channel. */
|
||||||
@@ -494,6 +496,8 @@ export type SlackAccountConfig = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
botToken?: string;
|
botToken?: string;
|
||||||
appToken?: string;
|
appToken?: string;
|
||||||
|
/** Allow bot-authored messages to trigger replies (default: false). */
|
||||||
|
allowBots?: boolean;
|
||||||
/**
|
/**
|
||||||
* Controls how channel messages are handled:
|
* Controls how channel messages are handled:
|
||||||
* - "open" (default): channels bypass allowlists; mention-gating applies
|
* - "open" (default): channels bypass allowlists; mention-gating applies
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ const SlackChannelSchema = z.object({
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
allowBots: z.boolean().optional(),
|
||||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
skills: z.array(z.string()).optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
systemPrompt: z.string().optional(),
|
systemPrompt: z.string().optional(),
|
||||||
@@ -319,11 +320,13 @@ const SlackAccountSchema = z.object({
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
botToken: z.string().optional(),
|
botToken: z.string().optional(),
|
||||||
appToken: z.string().optional(),
|
appToken: z.string().optional(),
|
||||||
|
allowBots: z.boolean().optional(),
|
||||||
groupPolicy: GroupPolicySchema.optional().default("open"),
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
mediaMaxMb: z.number().positive().optional(),
|
mediaMaxMb: z.number().positive().optional(),
|
||||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||||
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
|
replyToMode: ReplyToModeSchema.optional(),
|
||||||
actions: z
|
actions: z
|
||||||
.object({
|
.object({
|
||||||
reactions: z.boolean().optional(),
|
reactions: z.boolean().optional(),
|
||||||
@@ -1188,7 +1191,6 @@ export const ClawdbotSchema = z.object({
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
telegram: TelegramConfigSchema.optional(),
|
telegram: TelegramConfigSchema.optional(),
|
||||||
discord: DiscordConfigSchema.optional(),
|
discord: DiscordConfigSchema.optional(),
|
||||||
slack: SlackConfigSchema.optional(),
|
slack: SlackConfigSchema.optional(),
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ export type ResolvedSlackAccount = {
|
|||||||
botTokenSource: SlackTokenSource;
|
botTokenSource: SlackTokenSource;
|
||||||
appTokenSource: SlackTokenSource;
|
appTokenSource: SlackTokenSource;
|
||||||
config: SlackAccountConfig;
|
config: SlackAccountConfig;
|
||||||
|
groupPolicy?: SlackAccountConfig["groupPolicy"];
|
||||||
|
textChunkLimit?: SlackAccountConfig["textChunkLimit"];
|
||||||
|
mediaMaxMb?: SlackAccountConfig["mediaMaxMb"];
|
||||||
|
reactionNotifications?: SlackAccountConfig["reactionNotifications"];
|
||||||
|
reactionAllowlist?: SlackAccountConfig["reactionAllowlist"];
|
||||||
|
replyToMode?: SlackAccountConfig["replyToMode"];
|
||||||
|
actions?: SlackAccountConfig["actions"];
|
||||||
|
slashCommand?: SlackAccountConfig["slashCommand"];
|
||||||
|
dm?: SlackAccountConfig["dm"];
|
||||||
|
channels?: SlackAccountConfig["channels"];
|
||||||
};
|
};
|
||||||
|
|
||||||
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
||||||
@@ -66,31 +76,26 @@ export function resolveSlackAccount(params: {
|
|||||||
const accountEnabled = merged.enabled !== false;
|
const accountEnabled = merged.enabled !== false;
|
||||||
const enabled = baseEnabled && accountEnabled;
|
const enabled = baseEnabled && accountEnabled;
|
||||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||||
|
const envBot = allowEnv
|
||||||
const botToken = resolveSlackBotToken(
|
? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN)
|
||||||
merged.botToken ??
|
: undefined;
|
||||||
(allowEnv ? process.env.SLACK_BOT_TOKEN : undefined) ??
|
const envApp = allowEnv
|
||||||
(allowEnv ? params.cfg.slack?.botToken : undefined),
|
? resolveSlackAppToken(process.env.SLACK_APP_TOKEN)
|
||||||
);
|
: undefined;
|
||||||
const appToken = resolveSlackAppToken(
|
const configBot = resolveSlackBotToken(merged.botToken);
|
||||||
merged.appToken ??
|
const configApp = resolveSlackAppToken(merged.appToken);
|
||||||
(allowEnv ? process.env.SLACK_APP_TOKEN : undefined) ??
|
const botToken = configBot ?? envBot;
|
||||||
(allowEnv ? params.cfg.slack?.appToken : undefined),
|
const appToken = configApp ?? envApp;
|
||||||
);
|
const botTokenSource: SlackTokenSource = configBot
|
||||||
const botTokenSource: SlackTokenSource = merged.botToken
|
|
||||||
? "config"
|
? "config"
|
||||||
: allowEnv && process.env.SLACK_BOT_TOKEN
|
: envBot
|
||||||
? "env"
|
? "env"
|
||||||
: allowEnv && params.cfg.slack?.botToken
|
: "none";
|
||||||
? "config"
|
const appTokenSource: SlackTokenSource = configApp
|
||||||
: "none";
|
|
||||||
const appTokenSource: SlackTokenSource = merged.appToken
|
|
||||||
? "config"
|
? "config"
|
||||||
: allowEnv && process.env.SLACK_APP_TOKEN
|
: envApp
|
||||||
? "env"
|
? "env"
|
||||||
: allowEnv && params.cfg.slack?.appToken
|
: "none";
|
||||||
? "config"
|
|
||||||
: "none";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accountId,
|
accountId,
|
||||||
@@ -101,6 +106,16 @@ export function resolveSlackAccount(params: {
|
|||||||
botTokenSource,
|
botTokenSource,
|
||||||
appTokenSource,
|
appTokenSource,
|
||||||
config: merged,
|
config: merged,
|
||||||
|
groupPolicy: merged.groupPolicy,
|
||||||
|
textChunkLimit: merged.textChunkLimit,
|
||||||
|
mediaMaxMb: merged.mediaMaxMb,
|
||||||
|
reactionNotifications: merged.reactionNotifications,
|
||||||
|
reactionAllowlist: merged.reactionAllowlist,
|
||||||
|
replyToMode: merged.replyToMode,
|
||||||
|
actions: merged.actions,
|
||||||
|
slashCommand: merged.slashCommand,
|
||||||
|
dm: merged.dm,
|
||||||
|
channels: merged.channels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { WebClient } from "@slack/web-api";
|
import { WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import { logVerbose } from "../globals.js";
|
||||||
|
import { resolveSlackAccount } from "./accounts.js";
|
||||||
import { sendMessageSlack } from "./send.js";
|
import { sendMessageSlack } from "./send.js";
|
||||||
import { resolveSlackBotToken } from "./token.js";
|
import { resolveSlackBotToken } from "./token.js";
|
||||||
|
|
||||||
export type SlackActionClientOpts = {
|
export type SlackActionClientOpts = {
|
||||||
|
accountId?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
client?: WebClient;
|
client?: WebClient;
|
||||||
};
|
};
|
||||||
@@ -28,12 +31,16 @@ export type SlackPin = {
|
|||||||
file?: { id?: string; name?: string };
|
file?: { id?: string; name?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveToken(explicit?: string) {
|
function resolveToken(explicit?: string, accountId?: string) {
|
||||||
const cfgToken = loadConfig().slack?.botToken;
|
const cfg = loadConfig();
|
||||||
const token = resolveSlackBotToken(
|
const account = resolveSlackAccount({ cfg, accountId });
|
||||||
explicit ?? process.env.SLACK_BOT_TOKEN ?? cfgToken ?? undefined,
|
const token = resolveSlackBotToken(explicit ?? account.botToken ?? undefined);
|
||||||
);
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
logVerbose(
|
||||||
|
`slack actions: missing bot token for account=${account.accountId} explicit=${Boolean(
|
||||||
|
explicit,
|
||||||
|
)} source=${account.botTokenSource ?? "unknown"}`,
|
||||||
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"SLACK_BOT_TOKEN or slack.botToken is required for Slack actions",
|
"SLACK_BOT_TOKEN or slack.botToken is required for Slack actions",
|
||||||
);
|
);
|
||||||
@@ -50,7 +57,7 @@ function normalizeEmoji(raw: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getClient(opts: SlackActionClientOpts = {}) {
|
async function getClient(opts: SlackActionClientOpts = {}) {
|
||||||
const token = resolveToken(opts.token);
|
const token = resolveToken(opts.token, opts.accountId);
|
||||||
return opts.client ?? new WebClient(token);
|
return opts.client ?? new WebClient(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +148,7 @@ export async function sendSlackMessage(
|
|||||||
opts: SlackActionClientOpts & { mediaUrl?: string } = {},
|
opts: SlackActionClientOpts & { mediaUrl?: string } = {},
|
||||||
) {
|
) {
|
||||||
return await sendMessageSlack(to, content, {
|
return await sendMessageSlack(to, content, {
|
||||||
|
accountId: opts.accountId,
|
||||||
token: opts.token,
|
token: opts.token,
|
||||||
mediaUrl: opts.mediaUrl,
|
mediaUrl: opts.mediaUrl,
|
||||||
client: opts.client,
|
client: opts.client,
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
export {
|
||||||
|
listEnabledSlackAccounts,
|
||||||
|
listSlackAccountIds,
|
||||||
|
resolveDefaultSlackAccountId,
|
||||||
|
resolveSlackAccount,
|
||||||
|
} from "./accounts.js";
|
||||||
export {
|
export {
|
||||||
deleteSlackMessage,
|
deleteSlackMessage,
|
||||||
editSlackMessage,
|
editSlackMessage,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ type SlackMessageEvent = {
|
|||||||
user?: string;
|
user?: string;
|
||||||
bot_id?: string;
|
bot_id?: string;
|
||||||
subtype?: string;
|
subtype?: string;
|
||||||
|
username?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
ts?: string;
|
ts?: string;
|
||||||
thread_ts?: string;
|
thread_ts?: string;
|
||||||
@@ -93,6 +94,7 @@ type SlackAppMentionEvent = {
|
|||||||
type: "app_mention";
|
type: "app_mention";
|
||||||
user?: string;
|
user?: string;
|
||||||
bot_id?: string;
|
bot_id?: string;
|
||||||
|
username?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
ts?: string;
|
ts?: string;
|
||||||
thread_ts?: string;
|
thread_ts?: string;
|
||||||
@@ -170,6 +172,7 @@ type SlackThreadBroadcastEvent = {
|
|||||||
type SlackChannelConfigResolved = {
|
type SlackChannelConfigResolved = {
|
||||||
allowed: boolean;
|
allowed: boolean;
|
||||||
requireMention: boolean;
|
requireMention: boolean;
|
||||||
|
allowBots?: boolean;
|
||||||
users?: Array<string | number>;
|
users?: Array<string | number>;
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
@@ -294,6 +297,7 @@ function resolveSlackChannelConfig(params: {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
allowBots?: boolean;
|
||||||
users?: Array<string | number>;
|
users?: Array<string | number>;
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
@@ -317,6 +321,7 @@ function resolveSlackChannelConfig(params: {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
allowBots?: boolean;
|
||||||
users?: Array<string | number>;
|
users?: Array<string | number>;
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
@@ -349,13 +354,14 @@ function resolveSlackChannelConfig(params: {
|
|||||||
const requireMention =
|
const requireMention =
|
||||||
firstDefined(resolved.requireMention, fallback?.requireMention, true) ??
|
firstDefined(resolved.requireMention, fallback?.requireMention, true) ??
|
||||||
true;
|
true;
|
||||||
|
const allowBots = firstDefined(resolved.allowBots, fallback?.allowBots);
|
||||||
const users = firstDefined(resolved.users, fallback?.users);
|
const users = firstDefined(resolved.users, fallback?.users);
|
||||||
const skills = firstDefined(resolved.skills, fallback?.skills);
|
const skills = firstDefined(resolved.skills, fallback?.skills);
|
||||||
const systemPrompt = firstDefined(
|
const systemPrompt = firstDefined(
|
||||||
resolved.systemPrompt,
|
resolved.systemPrompt,
|
||||||
fallback?.systemPrompt,
|
fallback?.systemPrompt,
|
||||||
);
|
);
|
||||||
return { allowed, requireMention, users, skills, systemPrompt };
|
return { allowed, requireMention, allowBots, users, skills, systemPrompt };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveSlackMedia(params: {
|
async function resolveSlackMedia(params: {
|
||||||
@@ -706,15 +712,14 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
opts: { source: "message" | "app_mention"; wasMentioned?: boolean },
|
opts: { source: "message" | "app_mention"; wasMentioned?: boolean },
|
||||||
) => {
|
) => {
|
||||||
if (opts.source === "message" && message.type !== "message") return;
|
if (opts.source === "message" && message.type !== "message") return;
|
||||||
if (message.bot_id) return;
|
|
||||||
if (
|
if (
|
||||||
opts.source === "message" &&
|
opts.source === "message" &&
|
||||||
message.subtype &&
|
message.subtype &&
|
||||||
message.subtype !== "file_share"
|
message.subtype !== "file_share" &&
|
||||||
|
message.subtype !== "bot_message"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!message.user) return;
|
|
||||||
if (markMessageSeen(message.channel, message.ts)) return;
|
if (markMessageSeen(message.channel, message.ts)) return;
|
||||||
|
|
||||||
let channelInfo: {
|
let channelInfo: {
|
||||||
@@ -735,6 +740,40 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
const isRoom =
|
const isRoom =
|
||||||
resolvedChannelType === "channel" || resolvedChannelType === "group";
|
resolvedChannelType === "channel" || resolvedChannelType === "group";
|
||||||
|
|
||||||
|
const channelConfig = isRoom
|
||||||
|
? resolveSlackChannelConfig({
|
||||||
|
channelId: message.channel,
|
||||||
|
channelName,
|
||||||
|
channels: channelsConfig,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const allowBots =
|
||||||
|
channelConfig?.allowBots ??
|
||||||
|
account.config?.allowBots ??
|
||||||
|
cfg.slack?.allowBots ??
|
||||||
|
false;
|
||||||
|
const isBotMessage = Boolean(message.bot_id);
|
||||||
|
if (isBotMessage) {
|
||||||
|
if (message.user && botUserId && message.user === botUserId) return;
|
||||||
|
if (!allowBots) {
|
||||||
|
logVerbose(
|
||||||
|
`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isDirectMessage && !message.user) {
|
||||||
|
logVerbose("slack: drop dm message (missing user id)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const senderId =
|
||||||
|
message.user ?? (isBotMessage ? message.bot_id : undefined);
|
||||||
|
if (!senderId) {
|
||||||
|
logVerbose("slack: drop message (missing sender id)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isChannelAllowed({
|
!isChannelAllowed({
|
||||||
channelId: message.channel,
|
channelId: message.channel,
|
||||||
@@ -756,6 +795,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom);
|
const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom);
|
||||||
|
|
||||||
if (isDirectMessage) {
|
if (isDirectMessage) {
|
||||||
|
const directUserId = message.user;
|
||||||
|
if (!directUserId) {
|
||||||
|
logVerbose("slack: drop dm message (missing user id)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!dmEnabled || dmPolicy === "disabled") {
|
if (!dmEnabled || dmPolicy === "disabled") {
|
||||||
logVerbose("slack: drop dm (dms disabled)");
|
logVerbose("slack: drop dm (dms disabled)");
|
||||||
return;
|
return;
|
||||||
@@ -763,20 +807,20 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
if (dmPolicy !== "open") {
|
if (dmPolicy !== "open") {
|
||||||
const permitted = allowListMatches({
|
const permitted = allowListMatches({
|
||||||
allowList: effectiveAllowFromLower,
|
allowList: effectiveAllowFromLower,
|
||||||
id: message.user,
|
id: directUserId,
|
||||||
});
|
});
|
||||||
if (!permitted) {
|
if (!permitted) {
|
||||||
if (dmPolicy === "pairing") {
|
if (dmPolicy === "pairing") {
|
||||||
const sender = await resolveUserName(message.user);
|
const sender = await resolveUserName(directUserId);
|
||||||
const senderName = sender?.name ?? undefined;
|
const senderName = sender?.name ?? undefined;
|
||||||
const { code, created } = await upsertProviderPairingRequest({
|
const { code, created } = await upsertProviderPairingRequest({
|
||||||
provider: "slack",
|
provider: "slack",
|
||||||
id: message.user,
|
id: directUserId,
|
||||||
meta: { name: senderName },
|
meta: { name: senderName },
|
||||||
});
|
});
|
||||||
if (created) {
|
if (created) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`slack pairing request sender=${message.user} name=${senderName ?? "unknown"}`,
|
`slack pairing request sender=${directUserId} name=${senderName ?? "unknown"}`,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await sendMessageSlack(
|
await sendMessageSlack(
|
||||||
@@ -811,31 +855,28 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelConfig = isRoom
|
|
||||||
? resolveSlackChannelConfig({
|
|
||||||
channelId: message.channel,
|
|
||||||
channelName,
|
|
||||||
channels: channelsConfig,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const wasMentioned =
|
const wasMentioned =
|
||||||
opts.wasMentioned ??
|
opts.wasMentioned ??
|
||||||
(!isDirectMessage &&
|
(!isDirectMessage &&
|
||||||
(Boolean(botUserId && message.text?.includes(`<@${botUserId}>`)) ||
|
(Boolean(botUserId && message.text?.includes(`<@${botUserId}>`)) ||
|
||||||
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
|
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
|
||||||
const sender = await resolveUserName(message.user);
|
const sender = message.user ? await resolveUserName(message.user) : null;
|
||||||
const senderName = sender?.name ?? message.user;
|
const senderName =
|
||||||
|
sender?.name ??
|
||||||
|
message.username?.trim() ??
|
||||||
|
message.user ??
|
||||||
|
message.bot_id ??
|
||||||
|
"unknown";
|
||||||
const channelUserAuthorized = isRoom
|
const channelUserAuthorized = isRoom
|
||||||
? resolveSlackUserAllowed({
|
? resolveSlackUserAllowed({
|
||||||
allowList: channelConfig?.users,
|
allowList: channelConfig?.users,
|
||||||
userId: message.user,
|
userId: senderId,
|
||||||
userName: senderName,
|
userName: senderName,
|
||||||
})
|
})
|
||||||
: true;
|
: true;
|
||||||
if (isRoom && !channelUserAuthorized) {
|
if (isRoom && !channelUserAuthorized) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Blocked unauthorized slack sender ${message.user} (not in channel users)`,
|
`Blocked unauthorized slack sender ${senderId} (not in channel users)`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -844,7 +885,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
(allowList.length === 0 ||
|
(allowList.length === 0 ||
|
||||||
allowListMatches({
|
allowListMatches({
|
||||||
allowList,
|
allowList,
|
||||||
id: message.user,
|
id: senderId,
|
||||||
name: senderName,
|
name: senderName,
|
||||||
})) &&
|
})) &&
|
||||||
channelUserAuthorized;
|
channelUserAuthorized;
|
||||||
@@ -1010,7 +1051,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||||
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
|
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
|
||||||
SenderName: senderName,
|
SenderName: senderName,
|
||||||
SenderId: message.user,
|
SenderId: senderId,
|
||||||
Provider: "slack" as const,
|
Provider: "slack" as const,
|
||||||
Surface: "slack" as const,
|
Surface: "slack" as const,
|
||||||
MessageSid: message.ts,
|
MessageSid: message.ts,
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
resolveTextChunkLimit,
|
resolveTextChunkLimit,
|
||||||
} from "../auto-reply/chunk.js";
|
} from "../auto-reply/chunk.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import { logVerbose } from "../globals.js";
|
||||||
import { loadWebMedia } from "../web/media.js";
|
import { loadWebMedia } from "../web/media.js";
|
||||||
|
import type { SlackTokenSource } from "./accounts.js";
|
||||||
import { resolveSlackAccount } from "./accounts.js";
|
import { resolveSlackAccount } from "./accounts.js";
|
||||||
import { resolveSlackBotToken } from "./token.js";
|
import { resolveSlackBotToken } from "./token.js";
|
||||||
|
|
||||||
@@ -38,11 +40,17 @@ function resolveToken(params: {
|
|||||||
explicit?: string;
|
explicit?: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
fallbackToken?: string;
|
fallbackToken?: string;
|
||||||
|
fallbackSource?: SlackTokenSource;
|
||||||
}) {
|
}) {
|
||||||
const explicit = resolveSlackBotToken(params.explicit);
|
const explicit = resolveSlackBotToken(params.explicit);
|
||||||
if (explicit) return explicit;
|
if (explicit) return explicit;
|
||||||
const fallback = resolveSlackBotToken(params.fallbackToken);
|
const fallback = resolveSlackBotToken(params.fallbackToken);
|
||||||
if (!fallback) {
|
if (!fallback) {
|
||||||
|
logVerbose(
|
||||||
|
`slack send: missing bot token for account=${params.accountId} explicit=${Boolean(
|
||||||
|
params.explicit,
|
||||||
|
)} source=${params.fallbackSource ?? "unknown"}`,
|
||||||
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Slack bot token missing for account "${params.accountId}" (set slack.accounts.${params.accountId}.botToken or SLACK_BOT_TOKEN for default).`,
|
`Slack bot token missing for account "${params.accountId}" (set slack.accounts.${params.accountId}.botToken or SLACK_BOT_TOKEN for default).`,
|
||||||
);
|
);
|
||||||
@@ -154,6 +162,7 @@ export async function sendMessageSlack(
|
|||||||
explicit: opts.token,
|
explicit: opts.token,
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
fallbackToken: account.botToken,
|
fallbackToken: account.botToken,
|
||||||
|
fallbackSource: account.botTokenSource,
|
||||||
});
|
});
|
||||||
const client = opts.client ?? new WebClient(token);
|
const client = opts.client ?? new WebClient(token);
|
||||||
const recipient = parseRecipient(to);
|
const recipient = parseRecipient(to);
|
||||||
|
|||||||
@@ -140,17 +140,12 @@ describe("createTelegramBot", () => {
|
|||||||
globalThis.fetch = fetchSpy;
|
globalThis.fetch = fetchSpy;
|
||||||
try {
|
try {
|
||||||
createTelegramBot({ token: "tok" });
|
createTelegramBot({ token: "tok" });
|
||||||
const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun);
|
expect(botCtorSpy).toHaveBeenCalledWith(
|
||||||
if (isBun) {
|
"tok",
|
||||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
expect.objectContaining({
|
||||||
"tok",
|
client: expect.objectContaining({ fetch: fetchSpy }),
|
||||||
expect.objectContaining({
|
}),
|
||||||
client: expect.objectContaining({ fetch: fetchSpy }),
|
);
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
expect(botCtorSpy).toHaveBeenCalledWith("tok", undefined);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/types/osc-progress.d.ts
vendored
Normal file
19
src/types/osc-progress.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
declare module "osc-progress" {
|
||||||
|
export type OscProgressController = {
|
||||||
|
setIndeterminate: (label: string) => void;
|
||||||
|
setPercent: (label: string, percent: number) => void;
|
||||||
|
clear: () => void;
|
||||||
|
done?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createOscProgressController(params: {
|
||||||
|
env: NodeJS.ProcessEnv;
|
||||||
|
isTty: boolean;
|
||||||
|
write: (chunk: string) => void;
|
||||||
|
}): OscProgressController;
|
||||||
|
|
||||||
|
export function supportsOscProgress(
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
isTty: boolean,
|
||||||
|
): boolean;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user