Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
354d77b33f | ||
|
|
b9edeae961 | ||
|
|
824154deba | ||
|
|
50cec30c76 | ||
|
|
4e4ff506da |
No files matched your search
+1
-8
@@ -6,38 +6,31 @@ Docs: https://docs.clawd.bot
|
||||
|
||||
### Highlights
|
||||
- Ollama: provider discovery + docs. (#1606) Thanks @abhaymundhara. https://docs.clawd.bot/providers/ollama
|
||||
- Venius (Venice AI): highlight provider guide + cross-links + expanded guidance. https://docs.clawd.bot/providers/venice
|
||||
|
||||
### Changes
|
||||
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.clawd.bot/tts
|
||||
- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.clawd.bot/tools/web
|
||||
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
|
||||
- Docs: add verbose installer troubleshooting guidance.
|
||||
- Docs: update Fly.io guide notes.
|
||||
- Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.clawd.bot/bedrock
|
||||
- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/slash-commands
|
||||
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
|
||||
- BlueBubbles: add newline chunking mode option for streaming. (#1645) Thanks @tyler6204.
|
||||
|
||||
### Fixes
|
||||
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
||||
- Web UI: hide internal `message_id` hints in chat bubbles.
|
||||
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
|
||||
- Heartbeat: normalize target identifiers for consistent routing.
|
||||
- TUI: reload history after gateway reconnect to restore session state. (#1663)
|
||||
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
|
||||
- Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
|
||||
- Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.
|
||||
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
|
||||
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
||||
- Agents: use the active auth profile for auto-compaction recovery.
|
||||
- Models: default missing custom provider fields so minimal configs are accepted.
|
||||
- Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671)
|
||||
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
||||
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
|
||||
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
|
||||
- Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
|
||||
- Google Chat: normalize space targets without double `spaces/` prefix.
|
||||
- Messaging: keep newline chunking safe for fenced markdown blocks across channels.
|
||||
|
||||
## 2026.1.23-1
|
||||
|
||||
|
||||
@@ -477,32 +477,31 @@ Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and
|
||||
Thanks to all clawtributors:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a>
|
||||
<a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a>
|
||||
<a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></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/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></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/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a>
|
||||
<a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a>
|
||||
<a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></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/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a>
|
||||
<a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></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/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></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/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></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/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></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/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></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/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></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/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/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/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></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/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></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/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></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/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></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/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></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/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></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/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></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/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></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/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></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/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></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/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></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/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></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/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a>
|
||||
<a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></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/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/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></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/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></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/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/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></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/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></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/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a>
|
||||
<a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></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/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></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/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a>
|
||||
<a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></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/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/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></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/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a>
|
||||
<a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></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/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></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/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a>
|
||||
<a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></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/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></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/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/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></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/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a>
|
||||
<a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></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/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a> <a href="https://github.com/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></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/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></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/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a>
|
||||
<a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></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/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></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=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></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/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></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/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a>
|
||||
<a href="https://github.com/dasilva333"><img src="https://avatars.githubusercontent.com/u/947827?v=4&s=48" width="48" height="48" alt="dasilva333" title="dasilva333"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/grrowl"><img src="https://avatars.githubusercontent.com/u/907140?v=4&s=48" width="48" height="48" alt="grrowl" title="grrowl"/></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/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></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=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></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/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></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/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></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/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></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/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></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/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></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/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/sergical"><img src="https://avatars.githubusercontent.com/u/3760543?v=4&s=48" width="48" height="48" alt="sergical" title="sergical"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></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/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a>
|
||||
<a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></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/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></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/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></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/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></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/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></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/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></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/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
||||
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></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/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></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/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a>
|
||||
<a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a>
|
||||
<a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></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/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></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/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></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/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a>
|
||||
<a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></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/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></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/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></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/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/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/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></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/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></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/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></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/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></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/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></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/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></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/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></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/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></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/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></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/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></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/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></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/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></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/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a>
|
||||
<a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></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/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></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/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></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/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/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></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/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></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/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/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></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/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></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/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></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/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a>
|
||||
<a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></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/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/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></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/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a>
|
||||
<a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></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/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></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/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></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/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></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/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/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a>
|
||||
<a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></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/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></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/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></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/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
|
||||
<a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></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/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></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/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a>
|
||||
<a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></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=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></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/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></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/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a>
|
||||
<a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></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/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></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=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></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/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></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/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></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/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
|
||||
<a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></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/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></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/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></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/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></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/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
|
||||
<a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></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/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></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/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></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/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></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/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></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/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></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>
|
||||
@@ -1167,29 +1167,17 @@ public struct ConfigApplyParams: Codable, Sendable {
|
||||
public struct ConfigPatchParams: Codable, Sendable {
|
||||
public let raw: String
|
||||
public let basehash: String?
|
||||
public let sessionkey: String?
|
||||
public let note: String?
|
||||
public let restartdelayms: Int?
|
||||
|
||||
public init(
|
||||
raw: String,
|
||||
basehash: String?,
|
||||
sessionkey: String?,
|
||||
note: String?,
|
||||
restartdelayms: Int?
|
||||
basehash: String?
|
||||
) {
|
||||
self.raw = raw
|
||||
self.basehash = basehash
|
||||
self.sessionkey = sessionkey
|
||||
self.note = note
|
||||
self.restartdelayms = restartdelayms
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case raw
|
||||
case basehash = "baseHash"
|
||||
case sessionkey = "sessionKey"
|
||||
case note
|
||||
case restartdelayms = "restartDelayMs"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1167,29 +1167,17 @@ public struct ConfigApplyParams: Codable, Sendable {
|
||||
public struct ConfigPatchParams: Codable, Sendable {
|
||||
public let raw: String
|
||||
public let basehash: String?
|
||||
public let sessionkey: String?
|
||||
public let note: String?
|
||||
public let restartdelayms: Int?
|
||||
|
||||
public init(
|
||||
raw: String,
|
||||
basehash: String?,
|
||||
sessionkey: String?,
|
||||
note: String?,
|
||||
restartdelayms: Int?
|
||||
basehash: String?
|
||||
) {
|
||||
self.raw = raw
|
||||
self.basehash = basehash
|
||||
self.sessionkey = sessionkey
|
||||
self.note = note
|
||||
self.restartdelayms = restartdelayms
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case raw
|
||||
case basehash = "baseHash"
|
||||
case sessionkey = "sessionKey"
|
||||
case note
|
||||
case restartdelayms = "restartDelayMs"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,6 @@ Notes:
|
||||
## Capabilities & limits
|
||||
- DMs and guild text channels (threads are treated as separate channels; voice not supported).
|
||||
- Typing indicators sent best-effort; message chunking uses `channels.discord.textChunkLimit` (default 2000) and splits tall replies by line count (`channels.discord.maxLinesPerMessage`, default 17).
|
||||
- Optional newline chunking: set `channels.discord.chunkMode="newline"` to split on each line before length chunking.
|
||||
- File uploads supported up to the configured `channels.discord.mediaMaxMb` (default 8 MB).
|
||||
- Mention-gated guild replies by default to avoid noisy bots.
|
||||
- Reply context is injected when a message references another message (quoted content + ids).
|
||||
@@ -307,7 +306,6 @@ ack reaction after the bot replies.
|
||||
- `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel).
|
||||
- `guilds.<id>.reactionNotifications`: reaction system event mode (`off`, `own`, `all`, `allowlist`).
|
||||
- `textChunkLimit`: outbound text chunk size (chars). Default: 2000.
|
||||
- `chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on every newline before length chunking.
|
||||
- `maxLinesPerMessage`: soft max line count per message. Default: 17.
|
||||
- `mediaMaxMb`: clamp inbound media saved to disk.
|
||||
- `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20; falls back to `messages.groupChat.historyLimit`; `0` disables).
|
||||
|
||||
@@ -32,7 +32,7 @@ Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).
|
||||
- Under **Connection settings**, select **HTTP endpoint URL**.
|
||||
- Under **Triggers**, select **Use a common HTTP endpoint URL for all triggers** and set it to your gateway's public URL followed by `/googlechat`.
|
||||
- *Tip: Run `clawdbot status` to find your gateway's public URL.*
|
||||
- Under **Visibility**, check **Make this Chat app available to specific people and groups in <Your Domain>**.
|
||||
- Under **Visibility**, check **Make this Chat app available to specific people and groups in <Your Domain>**.
|
||||
- Enter your email address (e.g. `user@example.com`) in the text box.
|
||||
- Click **Save** at the bottom.
|
||||
6) **Enable the app status**:
|
||||
|
||||
@@ -219,7 +219,6 @@ This is useful when you want an isolated personality/model for a specific thread
|
||||
|
||||
## Limits
|
||||
- Outbound text is chunked to `channels.imessage.textChunkLimit` (default 4000).
|
||||
- Optional newline chunking: set `channels.imessage.chunkMode="newline"` to split on each line before length chunking.
|
||||
- Media uploads are capped by `channels.imessage.mediaMaxMb` (default 16).
|
||||
|
||||
## Addressing / delivery targets
|
||||
@@ -254,7 +253,6 @@ Provider options:
|
||||
- `channels.imessage.includeAttachments`: ingest attachments into context.
|
||||
- `channels.imessage.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||
- `channels.imessage.textChunkLimit`: outbound chunk size (chars).
|
||||
- `channels.imessage.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking.
|
||||
|
||||
Related global options:
|
||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
|
||||
|
||||
@@ -32,8 +32,6 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
## Notes
|
||||
|
||||
- Channels can run simultaneously; configure multiple and Clawdbot will route per chat.
|
||||
- Fastest setup is usually **Telegram** (simple bot token). WhatsApp requires QR pairing and
|
||||
stores more state on disk.
|
||||
- Group behavior varies by channel; see [Groups](/concepts/groups).
|
||||
- DM pairing and allowlists are enforced for safety; see [Security](/gateway/security).
|
||||
- Telegram internals: [grammY notes](/channels/grammy).
|
||||
|
||||
@@ -215,7 +215,6 @@ Provider options:
|
||||
- `channels.matrix.initialSyncLimit`: initial sync limit.
|
||||
- `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound).
|
||||
- `channels.matrix.textChunkLimit`: outbound text chunk size (chars).
|
||||
- `channels.matrix.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking.
|
||||
- `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing).
|
||||
- `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible.
|
||||
- `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist).
|
||||
|
||||
@@ -415,7 +415,6 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
|
||||
- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
|
||||
- `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available.
|
||||
- `channels.msteams.textChunkLimit`: outbound text chunk size.
|
||||
- `channels.msteams.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking.
|
||||
- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
|
||||
- `channels.msteams.requireMention`: require @mention in channels/groups (default true).
|
||||
- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
|
||||
|
||||
@@ -114,7 +114,6 @@ Provider options:
|
||||
- `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables).
|
||||
- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit).
|
||||
- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars).
|
||||
- `channels.nextcloud-talk.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking.
|
||||
- `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel.
|
||||
- `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning.
|
||||
- `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB).
|
||||
@@ -95,7 +95,6 @@ Groups:
|
||||
|
||||
## Media + limits
|
||||
- Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
|
||||
- Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on each line before length chunking.
|
||||
- Attachments supported (base64 fetched from `signal-cli`).
|
||||
- Default media cap: `channels.signal.mediaMaxMb` (default 8).
|
||||
- Use `channels.signal.ignoreAttachments` to skip downloading media.
|
||||
@@ -106,29 +105,8 @@ Groups:
|
||||
- **Read receipts**: when `channels.signal.sendReadReceipts` is true, Clawdbot forwards read receipts for allowed DMs.
|
||||
- Signal-cli does not expose read receipts for groups.
|
||||
|
||||
## Reactions (message tool)
|
||||
- Use `message action=react` with `channel=signal`.
|
||||
- Targets: sender E.164 or UUID (use `uuid:<id>` from pairing output; bare UUID works too).
|
||||
- `messageId` is the Signal timestamp for the message you’re reacting to.
|
||||
- Group reactions require `targetAuthor` or `targetAuthorUuid`.
|
||||
|
||||
Examples:
|
||||
```
|
||||
message action=react channel=signal target=uuid:123e4567-e89b-12d3-a456-426614174000 messageId=1737630212345 emoji=🔥
|
||||
message action=react channel=signal target=+15551234567 messageId=1737630212345 emoji=🔥 remove=true
|
||||
message action=react channel=signal target=signal:group:<groupId> targetAuthor=uuid:<sender-uuid> messageId=1737630212345 emoji=✅
|
||||
```
|
||||
|
||||
Config:
|
||||
- `channels.signal.actions.reactions`: enable/disable reaction actions (default true).
|
||||
- `channels.signal.reactionLevel`: `off | ack | minimal | extensive`.
|
||||
- `off`/`ack` disables agent reactions (message tool `react` will error).
|
||||
- `minimal`/`extensive` enables agent reactions and sets the guidance level.
|
||||
- Per-account overrides: `channels.signal.accounts.<id>.actions.reactions`, `channels.signal.accounts.<id>.reactionLevel`.
|
||||
|
||||
## Delivery targets (CLI/cron)
|
||||
- DMs: `signal:+15551234567` (or plain E.164).
|
||||
- UUID DMs: `uuid:<id>` (or bare UUID).
|
||||
- Groups: `signal:group:<groupId>`.
|
||||
- Usernames: `username:<name>` (if supported by your Signal account).
|
||||
|
||||
@@ -153,7 +131,6 @@ Provider options:
|
||||
- `channels.signal.historyLimit`: max group messages to include as context (0 disables).
|
||||
- `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms["<phone_or_uuid>"].historyLimit`.
|
||||
- `channels.signal.textChunkLimit`: outbound chunk size (chars).
|
||||
- `channels.signal.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking.
|
||||
- `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||
|
||||
Related global options:
|
||||
|
||||
@@ -349,7 +349,6 @@ ack reaction after the bot replies.
|
||||
|
||||
## Limits
|
||||
- Outbound text is chunked to `channels.slack.textChunkLimit` (default 4000).
|
||||
- Optional newline chunking: set `channels.slack.chunkMode="newline"` to split on each line before length chunking.
|
||||
- Media uploads are capped by `channels.slack.mediaMaxMb` (default 20).
|
||||
|
||||
## Reply threading
|
||||
|
||||
@@ -128,7 +128,6 @@ Notes:
|
||||
|
||||
## Limits
|
||||
- Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000).
|
||||
- Optional newline chunking: set `channels.telegram.chunkMode="newline"` to split on each line before length chunking.
|
||||
- Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5).
|
||||
- Telegram Bot API requests time out after `channels.telegram.timeoutSeconds` (default 500 via grammY). Set lower to avoid long hangs.
|
||||
- Group history context uses `channels.telegram.historyLimit` (or `channels.telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
|
||||
@@ -517,7 +516,6 @@ Provider options:
|
||||
- `channels.telegram.accounts.<account>.capabilities.inlineButtons`: per-account override.
|
||||
- `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
|
||||
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
|
||||
- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking.
|
||||
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
|
||||
- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||
- `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
||||
|
||||
@@ -271,13 +271,12 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
|
||||
|
||||
## Limits
|
||||
- Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000).
|
||||
- Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on each line before length chunking.
|
||||
- Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB).
|
||||
- Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).
|
||||
|
||||
## Outbound send (text + media)
|
||||
- Uses active web listener; error if gateway not running.
|
||||
- Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`, optional `channels.whatsapp.chunkMode`).
|
||||
- Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`).
|
||||
- Media:
|
||||
- Image/video/audio/document supported.
|
||||
- Audio sent as PTT; `audio/ogg` => `audio/ogg; codecs=opus`.
|
||||
|
||||
+2
-10
@@ -66,12 +66,11 @@ Name lookup:
|
||||
- Discord only: `--poll-duration-hours`, `--message`
|
||||
|
||||
- `react`
|
||||
- Channels: Discord/Google Chat/Slack/Telegram/WhatsApp/Signal
|
||||
- Channels: Discord/Google Chat/Slack/Telegram/WhatsApp
|
||||
- Required: `--message-id`, `--target`
|
||||
- Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--target-author`, `--target-author-uuid`
|
||||
- Optional: `--emoji`, `--remove`, `--participant`, `--from-me`
|
||||
- Note: `--remove` requires `--emoji` (omit `--emoji` to clear own reactions where supported; see /tools/reactions)
|
||||
- WhatsApp only: `--participant`, `--from-me`
|
||||
- Signal group reactions: `--target-author` or `--target-author-uuid` required
|
||||
|
||||
- `reactions`
|
||||
- Channels: Discord/Google Chat/Slack
|
||||
@@ -214,13 +213,6 @@ clawdbot message react --channel slack \
|
||||
--target C123 --message-id 456 --emoji "✅"
|
||||
```
|
||||
|
||||
React in a Signal group:
|
||||
```
|
||||
clawdbot message react --channel signal \
|
||||
--target signal:group:abc123 --message-id 1737630212345 \
|
||||
--emoji "✅" --target-author-uuid 123e4567-e89b-12d3-a456-426614174000
|
||||
```
|
||||
|
||||
Send Telegram inline buttons:
|
||||
```
|
||||
clawdbot message send --channel telegram --target @mychat --message "Choose:" \
|
||||
|
||||
@@ -31,7 +31,6 @@ These files live under the workspace (`agents.defaults.workspace`, default
|
||||
- Decisions, preferences, and durable facts go to `MEMORY.md`.
|
||||
- Day-to-day notes and running context go to `memory/YYYY-MM-DD.md`.
|
||||
- If someone says "remember this," write it down (do not keep it in RAM).
|
||||
- If you want something to stick, **ask the bot to write it** into memory.
|
||||
|
||||
## Automatic memory flush (pre-compaction ping)
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ Legend:
|
||||
- `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`.
|
||||
- `agents.defaults.blockStreamingCoalesce`: `{ minChars?, maxChars?, idleMs? }` (merge streamed blocks before send).
|
||||
- Channel hard cap: `*.textChunkLimit` (e.g., `channels.whatsapp.textChunkLimit`).
|
||||
- Channel chunk mode: `*.chunkMode` (`length` default, `newline` splits on each line before length chunking).
|
||||
- Discord soft cap: `channels.discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
|
||||
|
||||
**Boundary semantics:**
|
||||
|
||||
@@ -504,7 +504,6 @@ For groups, use `channels.whatsapp.groupPolicy` + `channels.whatsapp.groupAllowF
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["+15555550123", "+447700900123"],
|
||||
textChunkLimit: 4000, // optional outbound chunk size (chars)
|
||||
chunkMode: "length", // optional chunking mode (length | newline)
|
||||
mediaMaxMb: 50 // optional inbound media cap (MB)
|
||||
}
|
||||
}
|
||||
@@ -1106,7 +1105,6 @@ Multi-account support lives under `channels.discord.accounts` (see the multi-acc
|
||||
},
|
||||
historyLimit: 20, // include last N guild messages as context
|
||||
textChunkLimit: 2000, // optional outbound text chunk size (chars)
|
||||
chunkMode: "length", // optional chunking mode (length | newline)
|
||||
maxLinesPerMessage: 17, // soft max lines per message (Discord UI clipping)
|
||||
retry: { // outbound retry policy
|
||||
attempts: 3,
|
||||
@@ -1127,7 +1125,7 @@ Reaction notification modes:
|
||||
- `own`: reactions on the bot's own messages (default).
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
||||
Outbound text is chunked by `channels.discord.textChunkLimit` (default 2000). Set `channels.discord.chunkMode="newline"` to split on line boundaries before length chunking. Discord clients can clip very tall messages, so `channels.discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
|
||||
Outbound text is chunked by `channels.discord.textChunkLimit` (default 2000). Discord clients can clip very tall messages, so `channels.discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
|
||||
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
|
||||
|
||||
### `channels.googlechat` (Chat API webhook)
|
||||
@@ -1220,7 +1218,6 @@ Slack runs in Socket Mode and requires both a bot token and app token:
|
||||
ephemeral: true
|
||||
},
|
||||
textChunkLimit: 4000,
|
||||
chunkMode: "length",
|
||||
mediaMaxMb: 20
|
||||
}
|
||||
}
|
||||
@@ -1270,8 +1267,7 @@ Mattermost requires a bot token plus the base URL for your server:
|
||||
dmPolicy: "pairing",
|
||||
chatmode: "oncall", // oncall | onmessage | onchar
|
||||
oncharPrefixes: [">", "!"],
|
||||
textChunkLimit: 4000,
|
||||
chunkMode: "length"
|
||||
textChunkLimit: 4000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2850,11 +2846,6 @@ Related docs:
|
||||
- [Tailscale](/gateway/tailscale)
|
||||
- [Remote access](/gateway/remote)
|
||||
|
||||
Trusted proxies:
|
||||
- `gateway.trustedProxies`: list of reverse proxy IPs that terminate TLS in front of the Gateway.
|
||||
- When a connection comes from one of these IPs, Clawdbot uses `x-forwarded-for` (or `x-real-ip`) to determine the client IP for local pairing checks and HTTP auth/local checks.
|
||||
- Only list proxies you fully control, and ensure they **overwrite** incoming `x-forwarded-for`.
|
||||
|
||||
Notes:
|
||||
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
||||
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
|
||||
|
||||
@@ -29,8 +29,6 @@ Clawdbot is both a product and an experiment: you’re wiring frontier-model beh
|
||||
- where the bot is allowed to act
|
||||
- what the bot can touch
|
||||
|
||||
Start with the smallest access that still works, then widen it as you gain confidence.
|
||||
|
||||
### What the audit checks (high level)
|
||||
|
||||
- **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot?
|
||||
@@ -324,11 +322,6 @@ Tailscale.
|
||||
you terminate TLS or proxy in front of the gateway, disable
|
||||
`gateway.auth.allowTailscale` and use token/password auth instead.
|
||||
|
||||
Trusted proxies:
|
||||
- If you terminate TLS in front of the Gateway, set `gateway.trustedProxies` to your proxy IPs.
|
||||
- Clawdbot will trust `x-forwarded-for` (or `x-real-ip`) from those IPs to determine the client IP for local pairing checks and HTTP auth/local checks.
|
||||
- Ensure your proxy **overwrites** `x-forwarded-for` and blocks direct access to the Gateway port.
|
||||
|
||||
See [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
||||
|
||||
### 0.6.1) Browser control server over Tailscale (recommended)
|
||||
|
||||
+31
-177
@@ -7,8 +7,10 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [What is Clawdbot?](#what-is-clawdbot)
|
||||
- [What is Clawdbot, in one paragraph?](#what-is-clawdbot-in-one-paragraph)
|
||||
- [What’s the value proposition?](#whats-the-value-proposition)
|
||||
- [Quick start and first-run setup](#quick-start-and-first-run-setup)
|
||||
- [I'm stuck — what's the fastest way to get unstuck?](#im-stuck--whats-the-fastest-way-to-get-unstuck)
|
||||
- [What’s the recommended way to install and set up Clawdbot?](#whats-the-recommended-way-to-install-and-set-up-clawdbot)
|
||||
- [How do I open the dashboard after onboarding?](#how-do-i-open-the-dashboard-after-onboarding)
|
||||
- [How do I authenticate the dashboard (token) on localhost vs remote?](#how-do-i-authenticate-the-dashboard-token-on-localhost-vs-remote)
|
||||
@@ -48,10 +50,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [What’s the difference between the hackable (git) install and npm install?](#whats-the-difference-between-the-hackable-git-install-and-npm-install)
|
||||
- [Can I switch between npm and git installs later?](#can-i-switch-between-npm-and-git-installs-later)
|
||||
- [Should I run the Gateway on my laptop or a VPS?](#should-i-run-the-gateway-on-my-laptop-or-a-vps)
|
||||
- [How important is it to run Clawdbot on a dedicated machine?](#how-important-is-it-to-run-clawdbot-on-a-dedicated-machine)
|
||||
- [What is Clawdbot?](#what-is-clawdbot)
|
||||
- [What is Clawdbot, in one paragraph?](#what-is-clawdbot-in-one-paragraph)
|
||||
- [What’s the value proposition?](#whats-the-value-proposition)
|
||||
- [Skills and automation](#skills-and-automation)
|
||||
- [How do I customize skills without keeping the repo dirty?](#how-do-i-customize-skills-without-keeping-the-repo-dirty)
|
||||
- [Can I load skills from a custom folder?](#can-i-load-skills-from-a-custom-folder)
|
||||
@@ -67,7 +65,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [How does memory work?](#how-does-memory-work)
|
||||
- [Does semantic memory search require an OpenAI API key?](#does-semantic-memory-search-require-an-openai-api-key)
|
||||
- [Where things live on disk](#where-things-live-on-disk)
|
||||
- [Is all data used with Clawdbot saved locally?](#is-all-data-used-with-clawdbot-saved-locally)
|
||||
- [Where does Clawdbot store its data?](#where-does-clawdbot-store-its-data)
|
||||
- [Where should AGENTS.md / SOUL.md / USER.md / MEMORY.md live?](#where-should-agentsmd--soulmd--usermd--memorymd-live)
|
||||
- [What’s the recommended backup strategy?](#whats-the-recommended-backup-strategy)
|
||||
@@ -86,7 +83,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [Remote gateways + nodes](#remote-gateways-nodes)
|
||||
- [How do commands propagate between Telegram, the gateway, and nodes?](#how-do-commands-propagate-between-telegram-the-gateway-and-nodes)
|
||||
- [How can my agent access my computer if the Gateway is hosted remotely?](#how-can-my-agent-access-my-computer-if-the-gateway-is-hosted-remotely)
|
||||
- [Can two Clawdbots talk to each other (local + VPS)?](#can-two-clawdbots-talk-to-each-other-local--vps)
|
||||
- [Is there a benefit to using a node on my personal laptop instead of SSH from a VPS?](#is-there-a-benefit-to-using-a-node-on-my-personal-laptop-instead-of-ssh-from-a-vps)
|
||||
- [Do nodes run a gateway service?](#do-nodes-run-a-gateway-service)
|
||||
- [Is there an API / RPC way to apply config?](#is-there-an-api-rpc-way-to-apply-config)
|
||||
@@ -144,7 +140,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [Logging and debugging](#logging-and-debugging)
|
||||
- [Where are logs?](#where-are-logs)
|
||||
- [How do I start/stop/restart the Gateway service?](#how-do-i-startstoprestart-the-gateway-service)
|
||||
- [I closed my terminal on Windows — how do I restart Clawdbot?](#i-closed-my-terminal-on-windows--how-do-i-restart-clawdbot)
|
||||
- [How do I completely stop then start the Gateway?](#how-do-i-completely-stop-then-start-the-gateway)
|
||||
- [ELI5: `clawdbot gateway restart` vs `clawdbot gateway`](#eli5-clawdbot-gateway-restart-vs-clawdbot-gateway)
|
||||
- [What’s the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails)
|
||||
@@ -210,55 +205,36 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
```
|
||||
Asks the running gateway for a full snapshot (WS-only). See [Health](/gateway/health).
|
||||
|
||||
## What is Clawdbot?
|
||||
|
||||
### What is Clawdbot, in one paragraph?
|
||||
|
||||
Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Mattermost (plugin), Discord, Google Chat, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the always-on control plane; the assistant is the product.
|
||||
|
||||
### What’s the value proposition?
|
||||
|
||||
Clawdbot is not “just a Claude wrapper.” It’s a **local-first control plane** that lets you run a
|
||||
capable assistant on **your own hardware**, reachable from the chat apps you already use, with
|
||||
stateful sessions, memory, and tools — without handing control of your workflows to a hosted
|
||||
SaaS.
|
||||
|
||||
Highlights:
|
||||
- **Your devices, your data:** run the Gateway wherever you want (Mac, Linux, VPS) and keep the
|
||||
workspace + session history local.
|
||||
- **Real channels, not a web sandbox:** WhatsApp/Telegram/Slack/Discord/Signal/iMessage/etc,
|
||||
plus mobile voice and Canvas on supported platforms.
|
||||
- **Model-agnostic:** use Anthropic, OpenAI, MiniMax, OpenRouter, etc., with per‑agent routing
|
||||
and failover.
|
||||
- **Local-only option:** run local models so **all data can stay on your device** if you want.
|
||||
- **Multi-agent routing:** separate agents per channel, account, or task, each with its own
|
||||
workspace and defaults.
|
||||
- **Open source and hackable:** inspect, extend, and self-host without vendor lock‑in.
|
||||
|
||||
Docs: [Gateway](/gateway), [Channels](/channels), [Multi‑agent](/concepts/multi-agent),
|
||||
[Memory](/concepts/memory).
|
||||
|
||||
## Quick start and first-run setup
|
||||
|
||||
### I'm stuck — what's the fastest way to get unstuck?
|
||||
|
||||
Use a local AI agent that can **see your machine**. That is far more effective than asking
|
||||
in Discord, because most "I'm stuck" cases are **local config or environment issues** that
|
||||
remote helpers cannot inspect.
|
||||
|
||||
- **Claude Code**: https://www.anthropic.com/claude-code/
|
||||
- **OpenAI Codex**: https://openai.com/codex/
|
||||
|
||||
These tools can read the repo, run commands, inspect logs, and help fix your machine-level
|
||||
setup (PATH, services, permissions, auth files). Give them the **full source checkout** via
|
||||
the hackable (git) install:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://clawd.bot/install.sh | bash -s -- --install-method git
|
||||
```
|
||||
|
||||
This installs Clawdbot **from a git checkout**, so the agent can read the code + docs and
|
||||
reason about the exact version you are running. You can always switch back to stable later
|
||||
by re-running the installer without `--install-method git`.
|
||||
|
||||
Tip: ask the agent to **plan and supervise** the fix (step-by-step), then execute only the
|
||||
necessary commands. That keeps changes small and easier to audit.
|
||||
|
||||
If you discover a real bug or fix, please file a GitHub issue or send a PR:
|
||||
https://github.com/clawdbot/clawdbot/issues
|
||||
https://github.com/clawdbot/clawdbot/pulls
|
||||
|
||||
Start with these commands (share outputs when asking for help):
|
||||
|
||||
```bash
|
||||
clawdbot status
|
||||
clawdbot models status
|
||||
clawdbot doctor
|
||||
```
|
||||
|
||||
What they do:
|
||||
- `clawdbot status`: quick snapshot of gateway/agent health + basic config.
|
||||
- `clawdbot models status`: checks provider auth + model availability.
|
||||
- `clawdbot doctor`: validates and repairs common config/state issues.
|
||||
|
||||
Other useful CLI checks: `clawdbot status --all`, `clawdbot logs --follow`,
|
||||
`clawdbot gateway status`, `clawdbot health --verbose`.
|
||||
|
||||
Quick debug loop: [First 60 seconds if something's broken](#first-60-seconds-if-somethings-broken).
|
||||
Install docs: [Install](/install), [Installer flags](/install/installer), [Updating](/install/updating).
|
||||
|
||||
### What’s the recommended way to install and set up Clawdbot?
|
||||
|
||||
The repo recommends running from source and using the onboarding wizard:
|
||||
@@ -313,9 +289,6 @@ disk as enough for personal use, and note that a **Raspberry Pi 4 can run it**.
|
||||
If you want extra headroom (logs, media, other services), **2GB is recommended**, but it’s
|
||||
not a hard minimum.
|
||||
|
||||
Tip: a small Pi/VPS can host the Gateway, and you can pair **nodes** on your laptop/phone for
|
||||
local screen/camera/canvas or command execution. See [Nodes](/nodes).
|
||||
|
||||
### Can I migrate my setup to a new machine (Mac mini) without redoing onboarding?
|
||||
|
||||
Yes. Copy the **state directory** and **workspace**, then run Doctor once. This
|
||||
@@ -729,43 +702,6 @@ lowest friction and you’re okay with sleep/restarts, run it locally.
|
||||
|
||||
**Recommended default:** VPS if you had gateway disconnects before. Local is great when you’re actively using the Mac and want local file access or UI automation with a visible browser.
|
||||
|
||||
### How important is it to run Clawdbot on a dedicated machine?
|
||||
|
||||
Not required, but **recommended for reliability and isolation**.
|
||||
|
||||
- **Dedicated host (VPS/Mac mini/Pi):** always‑on, fewer sleep/reboot interruptions, cleaner permissions, easier to keep running.
|
||||
- **Shared laptop/desktop:** totally fine for testing and active use, but expect pauses when the machine sleeps or updates.
|
||||
|
||||
If you want the best of both worlds, keep the Gateway on a dedicated host and pair your laptop as a **node** for local screen/camera/exec tools. See [Nodes](/nodes).
|
||||
|
||||
## What is Clawdbot?
|
||||
|
||||
### What is Clawdbot, in one paragraph?
|
||||
|
||||
Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Mattermost (plugin), Discord, Google Chat, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the always-on control plane; the assistant is the product.
|
||||
|
||||
### What’s the value proposition?
|
||||
|
||||
Clawdbot is not “just a Claude wrapper.” It’s a **local-first control plane** that lets you run a
|
||||
capable assistant on **your own hardware**, reachable from the chat apps you already use, with
|
||||
stateful sessions, memory, and tools — without handing control of your workflows to a hosted
|
||||
SaaS.
|
||||
|
||||
Highlights:
|
||||
- **Your devices, your data:** run the Gateway wherever you want (Mac, Linux, VPS) and keep the
|
||||
workspace + session history local.
|
||||
- **Real channels, not a web sandbox:** WhatsApp/Telegram/Slack/Discord/Signal/iMessage/etc,
|
||||
plus mobile voice and Canvas on supported platforms.
|
||||
- **Model-agnostic:** use Anthropic, OpenAI, MiniMax, OpenRouter, etc., with per‑agent routing
|
||||
and failover.
|
||||
- **Local-only option:** run local models so **all data can stay on your device** if you want.
|
||||
- **Multi-agent routing:** separate agents per channel, account, or task, each with its own
|
||||
workspace and defaults.
|
||||
- **Open source and hackable:** inspect, extend, and self-host without vendor lock‑in.
|
||||
|
||||
Docs: [Gateway](/gateway), [Channels](/channels), [Multi‑agent](/concepts/multi-agent),
|
||||
[Memory](/concepts/memory).
|
||||
|
||||
## Skills and automation
|
||||
|
||||
### How do I customize skills without keeping the repo dirty?
|
||||
@@ -937,20 +873,6 @@ models — see [Memory](/concepts/memory) for the setup details.
|
||||
|
||||
## Where things live on disk
|
||||
|
||||
### Is all data used with Clawdbot saved locally?
|
||||
|
||||
No — **Clawdbot’s state is local**, but **external services still see what you send them**.
|
||||
|
||||
- **Local by default:** sessions, memory files, config, and workspace live on the Gateway host
|
||||
(`~/.clawdbot` + your workspace directory).
|
||||
- **Remote by necessity:** messages you send to model providers (Anthropic/OpenAI/etc.) go to
|
||||
their APIs, and chat platforms (WhatsApp/Telegram/Slack/etc.) store message data on their
|
||||
servers.
|
||||
- **You control the footprint:** using local models keeps prompts on your machine, but channel
|
||||
traffic still goes through the channel’s servers.
|
||||
|
||||
Related: [Agent workspace](/concepts/agent-workspace), [Memory](/concepts/memory).
|
||||
|
||||
### Where does Clawdbot store its data?
|
||||
|
||||
Everything lives under `$CLAWDBOT_STATE_DIR` (default: `~/.clawdbot`):
|
||||
@@ -991,9 +913,6 @@ If the bot “forgets” after a restart, confirm the Gateway is using the same
|
||||
workspace on every launch (and remember: remote mode uses the **gateway host’s**
|
||||
workspace, not your local laptop).
|
||||
|
||||
Tip: if you want a durable behavior or preference, ask the bot to **write it into
|
||||
AGENTS.md or MEMORY.md** rather than relying on chat history.
|
||||
|
||||
See [Agent workspace](/concepts/agent-workspace) and [Memory](/concepts/memory).
|
||||
|
||||
### What’s the recommended backup strategy?
|
||||
@@ -1188,29 +1107,6 @@ pair devices you trust, and review [Security](/gateway/security).
|
||||
|
||||
Docs: [Nodes](/nodes), [Gateway protocol](/gateway/protocol), [macOS remote mode](/platforms/mac/remote), [Security](/gateway/security).
|
||||
|
||||
### Can two Clawdbots talk to each other (local + VPS)?
|
||||
|
||||
Yes. There is no built-in "bot-to-bot" bridge, but you can wire it up in a few
|
||||
reliable ways:
|
||||
|
||||
**Simplest:** use a normal chat channel both bots can access (Telegram/Slack/WhatsApp).
|
||||
Have Bot A send a message to Bot B, then let Bot B reply as usual.
|
||||
|
||||
**CLI bridge (generic):** run a script that calls the other Gateway with
|
||||
`clawdbot agent --message ... --deliver`, targeting a chat where the other bot
|
||||
listens. If one bot is on Railway/VPS, point your CLI at that remote Gateway
|
||||
via SSH/Tailscale (see [Remote access](/gateway/remote)).
|
||||
|
||||
Example pattern (run from a machine that can reach the target Gateway):
|
||||
```bash
|
||||
clawdbot agent --message "Hello from local bot" --deliver --channel telegram --reply-to <chat-id>
|
||||
```
|
||||
|
||||
Tip: add a guardrail so the two bots do not loop endlessly (mention-only, channel
|
||||
allowlists, or a "do not reply to bot messages" rule).
|
||||
|
||||
Docs: [Remote access](/gateway/remote), [Agent CLI](/cli/agent), [Agent send](/tools/agent-send).
|
||||
|
||||
### Is there a benefit to using a node on my personal laptop instead of SSH from a VPS?
|
||||
|
||||
Yes — nodes are the first‑class way to reach your laptop from a remote Gateway, and they
|
||||
@@ -1559,11 +1455,6 @@ Models are referenced as `provider/model` (example: `anthropic/claude-opus-4-5`)
|
||||
MiniMax M2.1 has its own docs: [MiniMax](/providers/minimax) and
|
||||
[Local models](/gateway/local-models).
|
||||
|
||||
Rule of thumb: use the **best model you can afford** for high-stakes work, and a cheaper
|
||||
model for routine chat or summaries. You can route models per agent and use sub-agents to
|
||||
parallelize long tasks (each sub-agent consumes tokens). See [Models](/concepts/models) and
|
||||
[Sub-agents](/tools/subagents).
|
||||
|
||||
Strong warning: weaker/over-quantized models are more vulnerable to prompt
|
||||
injection and unsafe behavior. See [Security](/gateway/security).
|
||||
|
||||
@@ -2047,43 +1938,6 @@ clawdbot gateway restart
|
||||
|
||||
If you run the gateway manually, `clawdbot gateway --force` can reclaim the port. See [Gateway](/gateway).
|
||||
|
||||
### I closed my terminal on Windows — how do I restart Clawdbot?
|
||||
|
||||
There are **two Windows install modes**:
|
||||
|
||||
**1) WSL2 (recommended):** the Gateway runs inside Linux.
|
||||
|
||||
Open PowerShell, enter WSL, then restart:
|
||||
|
||||
```powershell
|
||||
wsl
|
||||
clawdbot gateway status
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
If you never installed the service, start it in the foreground:
|
||||
|
||||
```bash
|
||||
clawdbot gateway run
|
||||
```
|
||||
|
||||
**2) Native Windows (not recommended):** the Gateway runs directly in Windows.
|
||||
|
||||
Open PowerShell and run:
|
||||
|
||||
```powershell
|
||||
clawdbot gateway status
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
If you run it manually (no service), use:
|
||||
|
||||
```powershell
|
||||
clawdbot gateway run
|
||||
```
|
||||
|
||||
Docs: [Windows (WSL2)](/platforms/windows), [Gateway service runbook](/gateway).
|
||||
|
||||
### How do I completely stop then start the Gateway?
|
||||
|
||||
If you installed the service:
|
||||
|
||||
@@ -7,8 +7,7 @@ read_when:
|
||||
# Windows (WSL2)
|
||||
|
||||
Clawdbot on Windows is recommended **via WSL2** (Ubuntu recommended). The
|
||||
CLI + Gateway run inside Linux, which keeps the runtime consistent and makes
|
||||
tooling far more compatible (Node/Bun/pnpm, Linux binaries, skills). Native
|
||||
CLI + Gateway run inside Linux, which keeps the runtime consistent. Native
|
||||
Windows installs are untested and more problematic.
|
||||
|
||||
Native Windows companion apps are planned.
|
||||
|
||||
@@ -11,15 +11,6 @@ default model as `provider/model`.
|
||||
|
||||
Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/etc.)? See [Channels](/channels).
|
||||
|
||||
## Highlight: Venius (Venice AI)
|
||||
|
||||
Venius is our recommended Venice AI setup for privacy-first inference with an option to use Opus for hard tasks.
|
||||
|
||||
- Default: `venice/llama-3.3-70b`
|
||||
- Best overall: `venice/claude-opus-45` (Opus remains the strongest)
|
||||
|
||||
See [Venice AI](/providers/venice).
|
||||
|
||||
## Quick start
|
||||
|
||||
1) Authenticate with the provider (usually via `clawdbot onboard`).
|
||||
@@ -44,7 +35,6 @@ See [Venice AI](/providers/venice).
|
||||
- [Z.AI](/providers/zai)
|
||||
- [GLM models](/providers/glm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Venius (Venice AI, privacy-focused)](/providers/venice)
|
||||
- [Ollama (local models)](/providers/ollama)
|
||||
|
||||
## Transcription providers
|
||||
|
||||
@@ -9,15 +9,6 @@ read_when:
|
||||
Clawdbot can use many LLM providers. Pick one, authenticate, then set the default
|
||||
model as `provider/model`.
|
||||
|
||||
## Highlight: Venius (Venice AI)
|
||||
|
||||
Venius is our recommended Venice AI setup for privacy-first inference with an option to use Opus for the hardest tasks.
|
||||
|
||||
- Default: `venice/llama-3.3-70b`
|
||||
- Best overall: `venice/claude-opus-45` (Opus remains the strongest)
|
||||
|
||||
See [Venice AI](/providers/venice).
|
||||
|
||||
## Quick start (two steps)
|
||||
|
||||
1) Authenticate with the provider (usually via `clawdbot onboard`).
|
||||
@@ -41,7 +32,6 @@ See [Venice AI](/providers/venice).
|
||||
- [Z.AI](/providers/zai)
|
||||
- [GLM models](/providers/glm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Venius (Venice AI)](/providers/venice)
|
||||
- [Amazon Bedrock](/bedrock)
|
||||
|
||||
For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration,
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
---
|
||||
summary: "Use Venice AI privacy-focused models in Clawdbot"
|
||||
read_when:
|
||||
- You want privacy-focused inference in Clawdbot
|
||||
- You want Venice AI setup guidance
|
||||
---
|
||||
# Venice AI (Venius highlight)
|
||||
|
||||
**Venius** is our highlight Venice setup for privacy-first inference with optional anonymized access to proprietary models.
|
||||
|
||||
Venice AI provides privacy-focused AI inference with support for uncensored models and access to major proprietary models through their anonymized proxy. All inference is private by default—no training on your data, no logging.
|
||||
|
||||
## Why Venice in Clawdbot
|
||||
|
||||
- **Private inference** for open-source models (no logging).
|
||||
- **Uncensored models** when you need them.
|
||||
- **Anonymized access** to proprietary models (Opus/GPT/Gemini) when quality matters.
|
||||
- OpenAI-compatible `/v1` endpoints.
|
||||
|
||||
## Privacy Modes
|
||||
|
||||
Venice offers two privacy levels — understanding this is key to choosing your model:
|
||||
|
||||
| Mode | Description | Models |
|
||||
|------|-------------|--------|
|
||||
| **Private** | Fully private. Prompts/responses are **never stored or logged**. Ephemeral. | Llama, Qwen, DeepSeek, Venice Uncensored, etc. |
|
||||
| **Anonymized** | Proxied through Venice with metadata stripped. The underlying provider (OpenAI, Anthropic) sees anonymized requests. | Claude, GPT, Gemini, Grok, Kimi, MiniMax |
|
||||
|
||||
## Features
|
||||
|
||||
- **Privacy-focused**: Choose between "private" (fully private) and "anonymized" (proxied) modes
|
||||
- **Uncensored models**: Access to models without content restrictions
|
||||
- **Major model access**: Use Claude, GPT-5.2, Gemini, Grok via Venice's anonymized proxy
|
||||
- **OpenAI-compatible API**: Standard `/v1` endpoints for easy integration
|
||||
- **Streaming**: ✅ Supported on all models
|
||||
- **Function calling**: ✅ Supported on select models (check model capabilities)
|
||||
- **Vision**: ✅ Supported on models with vision capability
|
||||
- **No hard rate limits**: Fair-use throttling may apply for extreme usage
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Get API Key
|
||||
|
||||
1. Sign up at [venice.ai](https://venice.ai)
|
||||
2. Go to **Settings → API Keys → Create new key**
|
||||
3. Copy your API key (format: `vapi_xxxxxxxxxxxx`)
|
||||
|
||||
### 2. Configure Clawdbot
|
||||
|
||||
**Option A: Environment Variable**
|
||||
|
||||
```bash
|
||||
export VENICE_API_KEY="vapi_xxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
**Option B: Interactive Setup (Recommended)**
|
||||
|
||||
```bash
|
||||
clawdbot onboard --auth-choice venice-api-key
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Prompt for your API key (or use existing `VENICE_API_KEY`)
|
||||
2. Show all available Venice models
|
||||
3. Let you pick your default model
|
||||
4. Configure the provider automatically
|
||||
|
||||
**Option C: Non-interactive**
|
||||
|
||||
```bash
|
||||
clawdbot onboard --non-interactive \
|
||||
--auth-choice venice-api-key \
|
||||
--venice-api-key "vapi_xxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
### 3. Verify Setup
|
||||
|
||||
```bash
|
||||
clawdbot chat --model venice/llama-3.3-70b "Hello, are you working?"
|
||||
```
|
||||
|
||||
## Model Selection
|
||||
|
||||
After setup, Clawdbot shows all available Venice models. Pick based on your needs:
|
||||
|
||||
- **Default (our pick)**: `venice/llama-3.3-70b` for private, balanced performance.
|
||||
- **Best overall quality**: `venice/claude-opus-45` for hard jobs (Opus remains the strongest).
|
||||
- **Privacy**: Choose "private" models for fully private inference.
|
||||
- **Capability**: Choose "anonymized" models to access Claude, GPT, Gemini via Venice's proxy.
|
||||
|
||||
Change your default model anytime:
|
||||
|
||||
```bash
|
||||
clawdbot models set venice/claude-opus-45
|
||||
clawdbot models set venice/llama-3.3-70b
|
||||
```
|
||||
|
||||
List all available models:
|
||||
|
||||
```bash
|
||||
clawdbot models list | grep venice
|
||||
```
|
||||
|
||||
## Configure via `clawdbot configure`
|
||||
|
||||
1. Run `clawdbot configure`
|
||||
2. Select **Model/auth**
|
||||
3. Choose **Venice AI**
|
||||
|
||||
## Which Model Should I Use?
|
||||
|
||||
| Use Case | Recommended Model | Why |
|
||||
|----------|-------------------|-----|
|
||||
| **General chat** | `llama-3.3-70b` | Good all-around, fully private |
|
||||
| **Best overall quality** | `claude-opus-45` | Opus remains the strongest for hard tasks |
|
||||
| **Privacy + Claude quality** | `claude-opus-45` | Best reasoning via anonymized proxy |
|
||||
| **Coding** | `qwen3-coder-480b-a35b-instruct` | Code-optimized, 262k context |
|
||||
| **Vision tasks** | `qwen3-vl-235b-a22b` | Best private vision model |
|
||||
| **Uncensored** | `venice-uncensored` | No content restrictions |
|
||||
| **Fast + cheap** | `qwen3-4b` | Lightweight, still capable |
|
||||
| **Complex reasoning** | `deepseek-v3.2` | Strong reasoning, private |
|
||||
|
||||
## Available Models (25 Total)
|
||||
|
||||
### Private Models (15) — Fully Private, No Logging
|
||||
|
||||
| Model ID | Name | Context (tokens) | Features |
|
||||
|----------|------|------------------|----------|
|
||||
| `llama-3.3-70b` | Llama 3.3 70B | 131k | General |
|
||||
| `llama-3.2-3b` | Llama 3.2 3B | 131k | Fast, lightweight |
|
||||
| `hermes-3-llama-3.1-405b` | Hermes 3 Llama 3.1 405B | 131k | Complex tasks |
|
||||
| `qwen3-235b-a22b-thinking-2507` | Qwen3 235B Thinking | 131k | Reasoning |
|
||||
| `qwen3-235b-a22b-instruct-2507` | Qwen3 235B Instruct | 131k | General |
|
||||
| `qwen3-coder-480b-a35b-instruct` | Qwen3 Coder 480B | 262k | Code |
|
||||
| `qwen3-next-80b` | Qwen3 Next 80B | 262k | General |
|
||||
| `qwen3-vl-235b-a22b` | Qwen3 VL 235B | 262k | Vision |
|
||||
| `qwen3-4b` | Venice Small (Qwen3 4B) | 32k | Fast, reasoning |
|
||||
| `deepseek-v3.2` | DeepSeek V3.2 | 163k | Reasoning |
|
||||
| `venice-uncensored` | Venice Uncensored | 32k | Uncensored |
|
||||
| `mistral-31-24b` | Venice Medium (Mistral) | 131k | Vision |
|
||||
| `google-gemma-3-27b-it` | Gemma 3 27B Instruct | 202k | Vision |
|
||||
| `openai-gpt-oss-120b` | OpenAI GPT OSS 120B | 131k | General |
|
||||
| `zai-org-glm-4.7` | GLM 4.7 | 202k | Reasoning, multilingual |
|
||||
|
||||
### Anonymized Models (10) — Via Venice Proxy
|
||||
|
||||
| Model ID | Original | Context (tokens) | Features |
|
||||
|----------|----------|------------------|----------|
|
||||
| `claude-opus-45` | Claude Opus 4.5 | 202k | Reasoning, vision |
|
||||
| `claude-sonnet-45` | Claude Sonnet 4.5 | 202k | Reasoning, vision |
|
||||
| `openai-gpt-52` | GPT-5.2 | 262k | Reasoning |
|
||||
| `openai-gpt-52-codex` | GPT-5.2 Codex | 262k | Reasoning, vision |
|
||||
| `gemini-3-pro-preview` | Gemini 3 Pro | 202k | Reasoning, vision |
|
||||
| `gemini-3-flash-preview` | Gemini 3 Flash | 262k | Reasoning, vision |
|
||||
| `grok-41-fast` | Grok 4.1 Fast | 262k | Reasoning, vision |
|
||||
| `grok-code-fast-1` | Grok Code Fast 1 | 262k | Reasoning, code |
|
||||
| `kimi-k2-thinking` | Kimi K2 Thinking | 262k | Reasoning |
|
||||
| `minimax-m21` | MiniMax M2.1 | 202k | Reasoning |
|
||||
|
||||
## Model Discovery
|
||||
|
||||
Clawdbot automatically discovers models from the Venice API when `VENICE_API_KEY` is set. If the API is unreachable, it falls back to a static catalog.
|
||||
|
||||
The `/models` endpoint is public (no auth needed for listing), but inference requires a valid API key.
|
||||
|
||||
## Streaming & Tool Support
|
||||
|
||||
| Feature | Support |
|
||||
|---------|---------|
|
||||
| **Streaming** | ✅ All models |
|
||||
| **Function calling** | ✅ Most models (check `supportsFunctionCalling` in API) |
|
||||
| **Vision/Images** | ✅ Models marked with "Vision" feature |
|
||||
| **JSON mode** | ✅ Supported via `response_format` |
|
||||
|
||||
## Pricing
|
||||
|
||||
Venice uses a credit-based system. Check [venice.ai/pricing](https://venice.ai/pricing) for current rates:
|
||||
|
||||
- **Private models**: Generally lower cost
|
||||
- **Anonymized models**: Similar to direct API pricing + small Venice fee
|
||||
|
||||
## Comparison: Venice vs Direct API
|
||||
|
||||
| Aspect | Venice (Anonymized) | Direct API |
|
||||
|--------|---------------------|------------|
|
||||
| **Privacy** | Metadata stripped, anonymized | Your account linked |
|
||||
| **Latency** | +10-50ms (proxy) | Direct |
|
||||
| **Features** | Most features supported | Full features |
|
||||
| **Billing** | Venice credits | Provider billing |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Use default private model
|
||||
clawdbot chat --model venice/llama-3.3-70b
|
||||
|
||||
# Use Claude via Venice (anonymized)
|
||||
clawdbot chat --model venice/claude-opus-45
|
||||
|
||||
# Use uncensored model
|
||||
clawdbot chat --model venice/venice-uncensored
|
||||
|
||||
# Use vision model with image
|
||||
clawdbot chat --model venice/qwen3-vl-235b-a22b
|
||||
|
||||
# Use coding model
|
||||
clawdbot chat --model venice/qwen3-coder-480b-a35b-instruct
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### API key not recognized
|
||||
|
||||
```bash
|
||||
echo $VENICE_API_KEY
|
||||
clawdbot models list | grep venice
|
||||
```
|
||||
|
||||
Ensure the key starts with `vapi_`.
|
||||
|
||||
### Model not available
|
||||
|
||||
The Venice model catalog updates dynamically. Run `clawdbot models list` to see currently available models. Some models may be temporarily offline.
|
||||
|
||||
### Connection issues
|
||||
|
||||
Venice API is at `https://api.venice.ai/api/v1`. Ensure your network allows HTTPS connections.
|
||||
|
||||
## Config file example
|
||||
|
||||
```json5
|
||||
{
|
||||
env: { VENICE_API_KEY: "vapi_..." },
|
||||
agents: { defaults: { model: { primary: "venice/llama-3.3-70b" } } },
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
venice: {
|
||||
baseUrl: "https://api.venice.ai/api/v1",
|
||||
apiKey: "${VENICE_API_KEY}",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "llama-3.3-70b",
|
||||
name: "Llama 3.3 70B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Venice AI](https://venice.ai)
|
||||
- [API Documentation](https://docs.venice.ai)
|
||||
- [Pricing](https://venice.ai/pricing)
|
||||
- [Status](https://status.venice.ai)
|
||||
@@ -45,7 +45,7 @@ run on host, set an explicit per-agent override:
|
||||
See [Web tools](/tools/web).
|
||||
|
||||
macOS: if you plan to build the apps, install Xcode / CLT. For the CLI + gateway only, Node is enough.
|
||||
Windows: use **WSL2** (Ubuntu recommended). WSL2 is strongly recommended; native Windows is untested, more problematic, and has poorer tool compatibility. Install WSL2 first, then run the Linux steps inside WSL. See [Windows (WSL2)](/platforms/windows).
|
||||
Windows: use **WSL2** (Ubuntu recommended). WSL2 is strongly recommended; native Windows is untested and more problematic. Install WSL2 first, then run the Linux steps inside WSL. See [Windows (WSL2)](/platforms/windows).
|
||||
|
||||
## 1) Install the CLI (recommended)
|
||||
|
||||
|
||||
@@ -23,16 +23,6 @@ Today, complex workflows require many back-and-forth tool calls. Each call costs
|
||||
- **Approvals built in**: Side effects (send email, post comment) halt the workflow until explicitly approved.
|
||||
- **Resumable**: Halted workflows return a token; approve and resume without re-running everything.
|
||||
|
||||
## Why a DSL instead of plain programs?
|
||||
|
||||
Lobster is intentionally small. The goal is not "a new language," it's a predictable, AI-friendly pipeline spec with first-class approvals and resume tokens.
|
||||
|
||||
- **Approve/resume is built in**: A normal program can prompt a human, but it can’t *pause and resume* with a durable token without you inventing that runtime yourself.
|
||||
- **Determinism + auditability**: Pipelines are data, so they’re easy to log, diff, replay, and review.
|
||||
- **Constrained surface for AI**: A tiny grammar + JSON piping reduces “creative” code paths and makes validation realistic.
|
||||
- **Safety policy baked in**: Timeouts, output caps, sandbox checks, and allowlists are enforced by the runtime, not each script.
|
||||
- **Still programmable**: Each step can call any CLI or script. If you want JS/TS, generate `.lobster` files from code.
|
||||
|
||||
## How it works
|
||||
|
||||
Clawdbot launches the local `lobster` CLI in **tool mode** and parses a JSON envelope from stdout.
|
||||
|
||||
@@ -26,10 +26,6 @@ Primary goals:
|
||||
- Keep the tool surface hard to misuse: sub-agents do **not** get session tools by default.
|
||||
- Avoid nested fan-out: sub-agents cannot spawn sub-agents.
|
||||
|
||||
Cost note: each sub-agent has its **own** context and token usage. For heavy or repetitive
|
||||
tasks, set a cheaper model for sub-agents and keep your main agent on a higher-quality model.
|
||||
You can configure this via `agents.defaults.subagents.model` or per-agent overrides.
|
||||
|
||||
## Tool
|
||||
|
||||
Use `sessions_spawn`:
|
||||
|
||||
@@ -174,7 +174,6 @@ Search the web using your configured provider.
|
||||
- `country` (optional): 2-letter country code for region-specific results (e.g., "DE", "US", "ALL"). If omitted, Brave chooses its default region.
|
||||
- `search_lang` (optional): ISO language code for search results (e.g., "de", "en", "fr")
|
||||
- `ui_lang` (optional): ISO language code for UI elements
|
||||
- `freshness` (optional, Brave only): filter by discovery time (`pd`, `pw`, `pm`, `py`, or `YYYY-MM-DDtoYYYY-MM-DD`)
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -194,12 +193,6 @@ await web_search({
|
||||
search_lang: "fr",
|
||||
ui_lang: "fr"
|
||||
});
|
||||
|
||||
// Recent results (past week)
|
||||
await web_search({
|
||||
query: "TMBG interview",
|
||||
freshness: "pw"
|
||||
});
|
||||
```
|
||||
|
||||
## web_fetch
|
||||
|
||||
+7
-81
@@ -8,37 +8,21 @@ read_when:
|
||||
|
||||
# Text-to-speech (TTS)
|
||||
|
||||
Clawdbot can convert outbound replies into audio using ElevenLabs, OpenAI, or Edge TTS.
|
||||
Clawdbot can convert outbound replies into audio using ElevenLabs or OpenAI.
|
||||
It works anywhere Clawdbot can send audio; Telegram gets a round voice-note bubble.
|
||||
|
||||
## Supported services
|
||||
|
||||
- **ElevenLabs** (primary or fallback provider)
|
||||
- **OpenAI** (primary or fallback provider; also used for summaries)
|
||||
- **Edge TTS** (primary or fallback provider; uses `node-edge-tts`, default when no API keys)
|
||||
|
||||
### Edge TTS notes
|
||||
## Required keys
|
||||
|
||||
Edge TTS uses Microsoft Edge's online neural TTS service via the `node-edge-tts`
|
||||
library. It's a hosted service (not local), uses Microsoft’s endpoints, and does
|
||||
not require an API key. `node-edge-tts` exposes speech configuration options and
|
||||
output formats, but not all options are supported by the Edge service. citeturn2search0
|
||||
|
||||
Because Edge TTS is a public web service without a published SLA or quota, treat it
|
||||
as best-effort. If you need guaranteed limits and support, use OpenAI or ElevenLabs.
|
||||
Microsoft's Speech REST API documents a 10‑minute audio limit per request; Edge TTS
|
||||
does not publish limits, so assume similar or lower limits. citeturn0search3
|
||||
|
||||
## Optional keys
|
||||
|
||||
If you want OpenAI or ElevenLabs:
|
||||
At least one of:
|
||||
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
Edge TTS does **not** require an API key. If no API keys are found, Clawdbot defaults
|
||||
to Edge TTS (unless disabled via `messages.tts.edge.enabled=false`).
|
||||
|
||||
If multiple providers are configured, the selected provider is used first and the others are fallback options.
|
||||
If both are configured, the selected provider is used first and the other is a fallback.
|
||||
Auto-summary uses the configured `summaryModel` (or `agents.defaults.model.primary`),
|
||||
so that provider must also be authenticated if you enable summaries.
|
||||
|
||||
@@ -48,17 +32,12 @@ so that provider must also be authenticated if you enable summaries.
|
||||
- [OpenAI Audio API reference](https://platform.openai.com/docs/api-reference/audio)
|
||||
- [ElevenLabs Text to Speech](https://elevenlabs.io/docs/api-reference/text-to-speech)
|
||||
- [ElevenLabs Authentication](https://elevenlabs.io/docs/api-reference/authentication)
|
||||
- [node-edge-tts](https://github.com/SchneeHertz/node-edge-tts)
|
||||
- [Microsoft Speech output formats](https://learn.microsoft.com/azure/ai-services/speech-service/rest-text-to-speech#audio-outputs)
|
||||
|
||||
## Is it enabled by default?
|
||||
|
||||
No. TTS is **disabled** by default. Enable it in config or with `/tts on`,
|
||||
which writes a local preference override.
|
||||
|
||||
Edge TTS **is** enabled by default once TTS is on, and is used automatically
|
||||
when no OpenAI or ElevenLabs API keys are available.
|
||||
|
||||
## Config
|
||||
|
||||
TTS config lives under `messages.tts` in `clawdbot.json`.
|
||||
@@ -115,41 +94,6 @@ Full schema is in [Gateway configuration](/gateway/configuration).
|
||||
}
|
||||
```
|
||||
|
||||
### Edge TTS primary (no API key)
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
enabled: true,
|
||||
provider: "edge",
|
||||
edge: {
|
||||
enabled: true,
|
||||
voice: "en-US-MichelleNeural",
|
||||
lang: "en-US",
|
||||
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
|
||||
rate: "+10%",
|
||||
pitch: "-5%"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Disable Edge TTS
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
edge: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom limits + prefs path
|
||||
|
||||
```json5
|
||||
@@ -187,9 +131,7 @@ Then run:
|
||||
|
||||
- `enabled`: master toggle (default `false`; local prefs can override).
|
||||
- `mode`: `"final"` (default) or `"all"` (includes tool/block replies).
|
||||
- `provider`: `"elevenlabs"`, `"openai"`, or `"edge"` (fallback is automatic).
|
||||
- If `provider` is **unset**, Clawdbot prefers `openai` (if key), then `elevenlabs` (if key),
|
||||
otherwise `edge`.
|
||||
- `provider`: `"elevenlabs"` or `"openai"` (fallback is automatic).
|
||||
- `summaryModel`: optional cheap model for auto-summary; defaults to `agents.defaults.model.primary`.
|
||||
- Accepts `provider/model` or a configured model alias.
|
||||
- `modelOverrides`: allow the model to emit TTS directives (on by default).
|
||||
@@ -205,15 +147,6 @@ Then run:
|
||||
- `elevenlabs.applyTextNormalization`: `auto|on|off`
|
||||
- `elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
|
||||
- `elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
|
||||
- `edge.enabled`: allow Edge TTS usage (default `true`; no API key).
|
||||
- `edge.voice`: Edge neural voice name (e.g. `en-US-MichelleNeural`).
|
||||
- `edge.lang`: language code (e.g. `en-US`).
|
||||
- `edge.outputFormat`: Edge output format (e.g. `audio-24khz-48kbitrate-mono-mp3`).
|
||||
- See Microsoft Speech output formats for valid values; not all formats are supported by Edge.
|
||||
- `edge.rate` / `edge.pitch` / `edge.volume`: percent strings (e.g. `+10%`, `-5%`).
|
||||
- `edge.saveSubtitles`: write JSON subtitles alongside the audio file.
|
||||
- `edge.proxy`: proxy URL for Edge TTS requests.
|
||||
- `edge.timeoutMs`: request timeout override (ms).
|
||||
|
||||
## Model-driven overrides (default on)
|
||||
|
||||
@@ -234,7 +167,7 @@ Here you go.
|
||||
```
|
||||
|
||||
Available directive keys (when enabled):
|
||||
- `provider` (`openai` | `elevenlabs` | `edge`)
|
||||
- `provider` (`openai` | `elevenlabs`)
|
||||
- `voice` (OpenAI voice) or `voiceId` (ElevenLabs)
|
||||
- `model` (OpenAI TTS model or ElevenLabs model id)
|
||||
- `stability`, `similarityBoost`, `style`, `speed`, `useSpeakerBoost`
|
||||
@@ -292,15 +225,8 @@ These override `messages.tts.*` for that host.
|
||||
- 48kHz / 64kbps is a good voice-note tradeoff and required for the round bubble.
|
||||
- **Other channels**: MP3 (`mp3_44100_128` from ElevenLabs, `mp3` from OpenAI).
|
||||
- 44.1kHz / 128kbps is the default balance for speech clarity.
|
||||
- **Edge TTS**: uses `edge.outputFormat` (default `audio-24khz-48kbitrate-mono-mp3`).
|
||||
- `node-edge-tts` accepts an `outputFormat`, but not all formats are available
|
||||
from the Edge service. citeturn2search0
|
||||
- Output format values follow Microsoft Speech output formats (including Ogg/WebM Opus). citeturn1search0
|
||||
- Telegram `sendVoice` accepts OGG/MP3/M4A; use OpenAI/ElevenLabs if you need
|
||||
guaranteed Opus voice notes. citeturn1search1
|
||||
- If the configured Edge output format fails, Clawdbot retries with MP3.
|
||||
|
||||
OpenAI/ElevenLabs formats are fixed; Telegram expects Opus for voice-note UX.
|
||||
This is not configurable; Telegram expects Opus for voice-note UX.
|
||||
|
||||
## Auto-TTS behavior
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export type BlueBubblesAccountConfig = {
|
||||
dms?: Record<string, unknown>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "newline" (default) splits on every newline; "length" splits by size. */
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
|
||||
@@ -374,7 +374,6 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) =>
|
||||
getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to, allowFrom, mode }) => {
|
||||
const trimmed = to?.trim() ?? "";
|
||||
|
||||
@@ -684,7 +684,6 @@ async function processMessageWithPipeline(params: {
|
||||
spaceId,
|
||||
runtime,
|
||||
core,
|
||||
config,
|
||||
statusSink,
|
||||
typingMessageName,
|
||||
});
|
||||
@@ -726,11 +725,10 @@ async function deliverGoogleChatReply(params: {
|
||||
spaceId: string;
|
||||
runtime: GoogleChatRuntimeEnv;
|
||||
core: GoogleChatCoreRuntime;
|
||||
config: ClawdbotConfig;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
typingMessageName?: string;
|
||||
}): Promise<void> {
|
||||
const { payload, account, spaceId, runtime, core, config, statusSink, typingMessageName } = params;
|
||||
const { payload, account, spaceId, runtime, core, statusSink, typingMessageName } = params;
|
||||
const mediaList = payload.mediaUrls?.length
|
||||
? payload.mediaUrls
|
||||
: payload.mediaUrl
|
||||
@@ -801,16 +799,7 @@ async function deliverGoogleChatReply(params: {
|
||||
|
||||
if (payload.text) {
|
||||
const chunkLimit = account.config.textChunkLimit ?? 4000;
|
||||
const chunkMode = core.channel.text.resolveChunkMode(
|
||||
config,
|
||||
"googlechat",
|
||||
account.accountId,
|
||||
);
|
||||
const chunks = core.channel.text.chunkMarkdownTextWithMode(
|
||||
payload.text,
|
||||
chunkLimit,
|
||||
chunkMode,
|
||||
);
|
||||
const chunks = core.channel.text.chunkMarkdownText(payload.text, chunkLimit);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
try {
|
||||
|
||||
@@ -186,7 +186,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getIMessageRuntime().channel.text.chunkText(text, limit),
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? getIMessageRuntime().channel.imessage.sendMessageIMessage;
|
||||
|
||||
@@ -50,7 +50,6 @@ export const MatrixConfigSchema = z.object({
|
||||
replyToMode: z.enum(["off", "first", "all"]).optional(),
|
||||
threadReplies: z.enum(["off", "inbound", "always"]).optional(),
|
||||
textChunkLimit: z.number().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
mediaMaxMb: z.number().optional(),
|
||||
autoJoin: z.enum(["always", "allowlist", "off"]).optional(),
|
||||
autoJoinAllowlist: z.array(allowFromEntry).optional(),
|
||||
|
||||
@@ -16,11 +16,10 @@ export async function deliverMatrixReplies(params: {
|
||||
tableMode?: MarkdownTableMode;
|
||||
}): Promise<void> {
|
||||
const core = getMatrixRuntime();
|
||||
const cfg = core.config.loadConfig();
|
||||
const tableMode =
|
||||
params.tableMode ??
|
||||
core.channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
cfg: core.config.loadConfig(),
|
||||
channel: "matrix",
|
||||
accountId: params.accountId,
|
||||
});
|
||||
@@ -30,7 +29,6 @@ export async function deliverMatrixReplies(params: {
|
||||
}
|
||||
};
|
||||
const chunkLimit = Math.min(params.textLimit, 4000);
|
||||
const chunkMode = core.channel.text.resolveChunkMode(cfg, "matrix", params.accountId);
|
||||
let hasReplied = false;
|
||||
for (const reply of params.replies) {
|
||||
const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
|
||||
@@ -56,11 +54,7 @@ export async function deliverMatrixReplies(params: {
|
||||
Boolean(id) && (params.replyToMode === "all" || !hasReplied);
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
for (const chunk of core.channel.text.chunkMarkdownTextWithMode(
|
||||
text,
|
||||
chunkLimit,
|
||||
chunkMode,
|
||||
)) {
|
||||
for (const chunk of core.channel.text.chunkMarkdownText(text, chunkLimit)) {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed) continue;
|
||||
await sendMessageMatrix(params.roomId, trimmed, {
|
||||
|
||||
@@ -42,9 +42,7 @@ const runtimeStub = {
|
||||
channel: {
|
||||
text: {
|
||||
resolveTextChunkLimit: () => 4000,
|
||||
resolveChunkMode: () => "length",
|
||||
chunkMarkdownText: (text: string) => (text ? [text] : []),
|
||||
chunkMarkdownTextWithMode: (text: string) => (text ? [text] : []),
|
||||
resolveMarkdownTableMode: () => "code",
|
||||
convertMarkdownTables: (text: string) => text,
|
||||
},
|
||||
|
||||
@@ -61,12 +61,7 @@ export async function sendMessageMatrix(
|
||||
);
|
||||
const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix");
|
||||
const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
|
||||
const chunkMode = getCore().channel.text.resolveChunkMode(cfg, "matrix", opts.accountId);
|
||||
const chunks = getCore().channel.text.chunkMarkdownTextWithMode(
|
||||
convertedMessage,
|
||||
chunkLimit,
|
||||
chunkMode,
|
||||
);
|
||||
const chunks = getCore().channel.text.chunkMarkdownText(convertedMessage, chunkLimit);
|
||||
const threadId = normalizeThreadId(opts.threadId);
|
||||
const relation = threadId
|
||||
? buildThreadRelation(threadId, opts.replyToId)
|
||||
|
||||
@@ -6,7 +6,6 @@ import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js";
|
||||
export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, deps, replyToId, threadId }) => {
|
||||
const send = deps?.sendMatrix ?? sendMessageMatrix;
|
||||
|
||||
@@ -69,8 +69,6 @@ export type MatrixConfig = {
|
||||
threadReplies?: "off" | "inbound" | "always";
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
/** Max outbound media size in MB. */
|
||||
mediaMaxMb?: number;
|
||||
/** Auto-join invites (always|allowlist|off). Default: always. */
|
||||
|
||||
@@ -158,7 +158,6 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getMattermostRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
|
||||
@@ -25,7 +25,6 @@ const MattermostAccountSchemaBase = z
|
||||
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
})
|
||||
|
||||
@@ -738,12 +738,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
if (mediaUrls.length === 0) {
|
||||
const chunkMode = core.channel.text.resolveChunkMode(
|
||||
cfg,
|
||||
"mattermost",
|
||||
account.accountId,
|
||||
);
|
||||
const chunks = core.channel.text.chunkMarkdownTextWithMode(text, textLimit, chunkMode);
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
for (const chunk of chunks.length > 0 ? chunks : [text]) {
|
||||
if (!chunk) continue;
|
||||
await sendMessageMattermost(to, chunk, {
|
||||
|
||||
@@ -36,8 +36,6 @@ export type MattermostAccountConfig = {
|
||||
groupPolicy?: GroupPolicy;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
/** Disable block streaming for this account. */
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
|
||||
@@ -9,21 +9,18 @@ import {
|
||||
} from "./messenger.js";
|
||||
import { setMSTeamsRuntime } from "./runtime.js";
|
||||
|
||||
const chunkMarkdownText = (text: string, limit: number) => {
|
||||
if (!text) return [];
|
||||
if (limit <= 0 || text.length <= limit) return [text];
|
||||
const chunks: string[] = [];
|
||||
for (let index = 0; index < text.length; index += limit) {
|
||||
chunks.push(text.slice(index, index + limit));
|
||||
}
|
||||
return chunks;
|
||||
};
|
||||
|
||||
const runtimeStub = {
|
||||
channel: {
|
||||
text: {
|
||||
chunkMarkdownText,
|
||||
chunkMarkdownTextWithMode: chunkMarkdownText,
|
||||
chunkMarkdownText: (text: string, limit: number) => {
|
||||
if (!text) return [];
|
||||
if (limit <= 0 || text.length <= limit) return [text];
|
||||
const chunks: string[] = [];
|
||||
for (let index = 0; index < text.length; index += limit) {
|
||||
chunks.push(text.slice(index, index + limit));
|
||||
}
|
||||
return chunks;
|
||||
},
|
||||
resolveMarkdownTableMode: () => "code",
|
||||
convertMarkdownTables: (text: string) => text,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
type ChunkMode,
|
||||
isSilentReplyText,
|
||||
loadWebMedia,
|
||||
type MarkdownTableMode,
|
||||
@@ -64,7 +63,6 @@ export type MSTeamsReplyRenderOptions = {
|
||||
chunkText?: boolean;
|
||||
mediaMode?: "split" | "inline";
|
||||
tableMode?: MarkdownTableMode;
|
||||
chunkMode?: ChunkMode;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -131,16 +129,11 @@ function pushTextMessages(
|
||||
opts: {
|
||||
chunkText: boolean;
|
||||
chunkLimit: number;
|
||||
chunkMode: ChunkMode;
|
||||
},
|
||||
) {
|
||||
if (!text) return;
|
||||
if (opts.chunkText) {
|
||||
for (const chunk of getMSTeamsRuntime().channel.text.chunkMarkdownTextWithMode(
|
||||
text,
|
||||
opts.chunkLimit,
|
||||
opts.chunkMode,
|
||||
)) {
|
||||
for (const chunk of getMSTeamsRuntime().channel.text.chunkMarkdownText(text, opts.chunkLimit)) {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) continue;
|
||||
out.push({ text: trimmed });
|
||||
@@ -204,7 +197,6 @@ export function renderReplyPayloadsToMessages(
|
||||
const out: MSTeamsRenderedMessage[] = [];
|
||||
const chunkLimit = Math.min(options.textChunkLimit, 4000);
|
||||
const chunkText = options.chunkText !== false;
|
||||
const chunkMode = options.chunkMode ?? "length";
|
||||
const mediaMode = options.mediaMode ?? "split";
|
||||
const tableMode =
|
||||
options.tableMode ??
|
||||
@@ -223,7 +215,7 @@ export function renderReplyPayloadsToMessages(
|
||||
if (!text && mediaList.length === 0) continue;
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
||||
pushTextMessages(out, text, { chunkText, chunkLimit });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -237,13 +229,13 @@ export function renderReplyPayloadsToMessages(
|
||||
if (mediaList[i]) out.push({ mediaUrl: mediaList[i] });
|
||||
}
|
||||
} else {
|
||||
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
||||
pushTextMessages(out, text, { chunkText, chunkLimit });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// mediaMode === "split"
|
||||
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
||||
pushTextMessages(out, text, { chunkText, chunkLimit });
|
||||
for (const mediaUrl of mediaList) {
|
||||
if (!mediaUrl) continue;
|
||||
out.push({ mediaUrl });
|
||||
|
||||
@@ -7,7 +7,6 @@ import { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
|
||||
export const msteamsOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getMSTeamsRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
pollMaxOptions: 12,
|
||||
sendText: async ({ cfg, to, text, deps }) => {
|
||||
|
||||
@@ -59,7 +59,6 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const chunkMode = core.channel.text.resolveChunkMode(params.cfg, "msteams");
|
||||
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
core.channel.reply.createReplyDispatcherWithTyping({
|
||||
@@ -76,7 +75,6 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
chunkText: true,
|
||||
mediaMode: "split",
|
||||
tableMode,
|
||||
chunkMode,
|
||||
});
|
||||
const mediaMaxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -247,7 +247,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getNextcloudTalkRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, accountId, replyToId }) => {
|
||||
const result = await sendMessageNextcloudTalk(to, text, {
|
||||
|
||||
@@ -44,7 +44,6 @@ export const NextcloudTalkAccountSchemaBase = z
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
|
||||
@@ -62,8 +62,6 @@ export type NextcloudTalkAccountConfig = {
|
||||
dms?: Record<string, DmConfig>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
/** Disable block streaming for this account. */
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
|
||||
@@ -18,20 +18,12 @@ import {
|
||||
setAccountEnabledInConfigSection,
|
||||
signalOnboardingAdapter,
|
||||
SignalConfigSchema,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelPlugin,
|
||||
type ResolvedSignalAccount,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
import { getSignalRuntime } from "./runtime.js";
|
||||
|
||||
const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: (ctx) => getSignalRuntime().channel.signal.messageActions.listActions(ctx),
|
||||
supportsAction: (ctx) => getSignalRuntime().channel.signal.messageActions.supportsAction?.(ctx),
|
||||
handleAction: async (ctx) =>
|
||||
await getSignalRuntime().channel.signal.messageActions.handleAction(ctx),
|
||||
};
|
||||
|
||||
const meta = getChatChannelMeta("signal");
|
||||
|
||||
export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
@@ -50,9 +42,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
reactions: true,
|
||||
},
|
||||
actions: signalMessageActions,
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
@@ -125,7 +115,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
normalizeTarget: normalizeSignalMessagingTarget,
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeSignalTargetId,
|
||||
hint: "<E.164|uuid:ID|group:ID|signal:group:ID|signal:+E.164>",
|
||||
hint: "<E.164|group:ID|signal:group:ID|signal:+E.164>",
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
@@ -207,7 +197,6 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getSignalRuntime().channel.text.chunkText(text, limit),
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendSignal ?? getSignalRuntime().channel.signal.sendMessageSignal;
|
||||
|
||||
@@ -251,7 +251,6 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getTelegramRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
|
||||
const send =
|
||||
|
||||
@@ -276,7 +276,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
outbound: {
|
||||
deliveryMode: "gateway",
|
||||
chunker: (text, limit) => getWhatsAppRuntime().channel.text.chunkText(text, limit),
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
pollMaxOptions: 12,
|
||||
resolveTarget: ({ to, allowFrom, mode }) => {
|
||||
|
||||
@@ -288,7 +288,6 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
||||
if (remaining.length) chunks.push(remaining);
|
||||
return chunks;
|
||||
},
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 2000,
|
||||
sendText: async ({ to, text, accountId, cfg }) => {
|
||||
const result = await sendMessageZalo(to, text, {
|
||||
|
||||
@@ -596,8 +596,6 @@ async function processMessageWithPipeline(params: {
|
||||
chatId,
|
||||
runtime,
|
||||
core,
|
||||
config,
|
||||
accountId: account.accountId,
|
||||
statusSink,
|
||||
fetcher,
|
||||
tableMode,
|
||||
@@ -616,13 +614,11 @@ async function deliverZaloReply(params: {
|
||||
chatId: string;
|
||||
runtime: ZaloRuntimeEnv;
|
||||
core: ZaloCoreRuntime;
|
||||
config: ClawdbotConfig;
|
||||
accountId?: string;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
fetcher?: ZaloFetch;
|
||||
tableMode?: MarkdownTableMode;
|
||||
}): Promise<void> {
|
||||
const { payload, token, chatId, runtime, core, config, accountId, statusSink, fetcher } = params;
|
||||
const { payload, token, chatId, runtime, core, statusSink, fetcher } = params;
|
||||
const tableMode = params.tableMode ?? "code";
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
|
||||
@@ -648,12 +644,7 @@ async function deliverZaloReply(params: {
|
||||
}
|
||||
|
||||
if (text) {
|
||||
const chunkMode = core.channel.text.resolveChunkMode(config, "zalo", accountId);
|
||||
const chunks = core.channel.text.chunkMarkdownTextWithMode(
|
||||
text,
|
||||
ZALO_TEXT_LIMIT,
|
||||
chunkMode,
|
||||
);
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, ZALO_TEXT_LIMIT);
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher);
|
||||
|
||||
@@ -506,7 +506,6 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
if (remaining.length) chunks.push(remaining);
|
||||
return chunks;
|
||||
},
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 2000,
|
||||
sendText: async ({ to, text, accountId, cfg }) => {
|
||||
const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
|
||||
|
||||
@@ -332,8 +332,6 @@ async function processMessage(
|
||||
isGroup,
|
||||
runtime,
|
||||
core,
|
||||
config,
|
||||
accountId: account.accountId,
|
||||
statusSink,
|
||||
tableMode: core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
@@ -358,13 +356,10 @@ async function deliverZalouserReply(params: {
|
||||
isGroup: boolean;
|
||||
runtime: RuntimeEnv;
|
||||
core: ZalouserCoreRuntime;
|
||||
config: ClawdbotConfig;
|
||||
accountId?: string;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
tableMode?: MarkdownTableMode;
|
||||
}): Promise<void> {
|
||||
const { payload, profile, chatId, isGroup, runtime, core, config, accountId, statusSink } =
|
||||
params;
|
||||
const { payload, profile, chatId, isGroup, runtime, core, statusSink } = params;
|
||||
const tableMode = params.tableMode ?? "code";
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
|
||||
@@ -395,12 +390,7 @@ async function deliverZalouserReply(params: {
|
||||
}
|
||||
|
||||
if (text) {
|
||||
const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
|
||||
const chunks = core.channel.text.chunkMarkdownTextWithMode(
|
||||
text,
|
||||
ZALOUSER_TEXT_LIMIT,
|
||||
chunkMode,
|
||||
);
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, ZALOUSER_TEXT_LIMIT);
|
||||
logVerbose(core, runtime, `Sending ${chunks.length} text chunk(s) to ${chatId}`);
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
|
||||
@@ -185,7 +185,6 @@
|
||||
"linkedom": "^0.18.12",
|
||||
"long": "5.3.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"node-edge-tts": "^1.2.9",
|
||||
"osc-progress": "^0.3.0",
|
||||
"pdfjs-dist": "^5.4.530",
|
||||
"playwright-core": "1.58.0",
|
||||
|
||||
Generated
-17
@@ -127,9 +127,6 @@ importers:
|
||||
markdown-it:
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
node-edge-tts:
|
||||
specifier: ^1.2.9
|
||||
version: 1.2.9
|
||||
osc-progress:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
@@ -4301,10 +4298,6 @@ packages:
|
||||
engines: {node: '>=14.18'}
|
||||
hasBin: true
|
||||
|
||||
node-edge-tts@1.2.9:
|
||||
resolution: {integrity: sha512-fvfW1dUgJdZAdTniC6MzLTMwnNUFKGKaUdRJ1OsveOYlfnPUETBU973CG89565txvbBowCQ4Czdeu3qSX8bNOg==}
|
||||
hasBin: true
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
@@ -10209,16 +10202,6 @@ snapshots:
|
||||
|
||||
node-downloader-helper@2.1.10: {}
|
||||
|
||||
node-edge-tts@1.2.9:
|
||||
dependencies:
|
||||
https-proxy-agent: 7.0.6
|
||||
ws: 8.19.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
@@ -282,7 +282,6 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
||||
"kimi-code": "KIMICODE_API_KEY",
|
||||
minimax: "MINIMAX_API_KEY",
|
||||
synthetic: "SYNTHETIC_API_KEY",
|
||||
venice: "VENICE_API_KEY",
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
opencode: "OPENCODE_API_KEY",
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
SYNTHETIC_BASE_URL,
|
||||
SYNTHETIC_MODEL_CATALOG,
|
||||
} from "./synthetic-models.js";
|
||||
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
|
||||
|
||||
type ModelsConfig = NonNullable<ClawdbotConfig["models"]>;
|
||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||
@@ -341,15 +340,6 @@ function buildSyntheticProvider(): ProviderConfig {
|
||||
};
|
||||
}
|
||||
|
||||
async function buildVeniceProvider(): Promise<ProviderConfig> {
|
||||
const models = await discoverVeniceModels();
|
||||
return {
|
||||
baseUrl: VENICE_BASE_URL,
|
||||
api: "openai-completions",
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildOllamaProvider(): Promise<ProviderConfig> {
|
||||
const models = await discoverOllamaModels();
|
||||
return {
|
||||
@@ -395,13 +385,6 @@ export async function resolveImplicitProviders(params: {
|
||||
providers.synthetic = { ...buildSyntheticProvider(), apiKey: syntheticKey };
|
||||
}
|
||||
|
||||
const veniceKey =
|
||||
resolveEnvApiKeyVarName("venice") ??
|
||||
resolveApiKeyFromProfiles({ provider: "venice", store: authStore });
|
||||
if (veniceKey) {
|
||||
providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey };
|
||||
}
|
||||
|
||||
const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal");
|
||||
if (qwenProfiles.length > 0) {
|
||||
providers["qwen-portal"] = {
|
||||
|
||||
@@ -48,19 +48,15 @@ describe("models-config", () => {
|
||||
const previous = process.env.COPILOT_GITHUB_TOKEN;
|
||||
const previousGh = process.env.GH_TOKEN;
|
||||
const previousGithub = process.env.GITHUB_TOKEN;
|
||||
const previousKimiCode = process.env.KIMICODE_API_KEY;
|
||||
const previousMinimax = process.env.MINIMAX_API_KEY;
|
||||
const previousMoonshot = process.env.MOONSHOT_API_KEY;
|
||||
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
||||
const previousVenice = process.env.VENICE_API_KEY;
|
||||
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||
delete process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
delete process.env.KIMICODE_API_KEY;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MOONSHOT_API_KEY;
|
||||
delete process.env.SYNTHETIC_API_KEY;
|
||||
delete process.env.VENICE_API_KEY;
|
||||
|
||||
try {
|
||||
vi.resetModules();
|
||||
@@ -83,16 +79,12 @@ describe("models-config", () => {
|
||||
else process.env.GH_TOKEN = previousGh;
|
||||
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
||||
else process.env.GITHUB_TOKEN = previousGithub;
|
||||
if (previousKimiCode === undefined) delete process.env.KIMICODE_API_KEY;
|
||||
else process.env.KIMICODE_API_KEY = previousKimiCode;
|
||||
if (previousMinimax === undefined) delete process.env.MINIMAX_API_KEY;
|
||||
else process.env.MINIMAX_API_KEY = previousMinimax;
|
||||
if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY;
|
||||
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
||||
if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY;
|
||||
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
||||
if (previousVenice === undefined) delete process.env.VENICE_API_KEY;
|
||||
else process.env.VENICE_API_KEY = previousVenice;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,8 +15,6 @@ import { resolveChannelCapabilities } from "../../config/channel-capabilities.js
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { getMachineDisplayName } from "../../infra/machine-name.js";
|
||||
import { resolveTelegramInlineButtonsScope } from "../../telegram/inline-buttons.js";
|
||||
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
|
||||
import { resolveSignalReactionLevel } from "../../signal/reaction-level.js";
|
||||
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
|
||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import { isSubagentSessionKey } from "../../routing/session-key.js";
|
||||
@@ -257,28 +255,6 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
}
|
||||
}
|
||||
}
|
||||
const reactionGuidance =
|
||||
runtimeChannel && params.config
|
||||
? (() => {
|
||||
if (runtimeChannel === "telegram") {
|
||||
const resolved = resolveTelegramReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Telegram" } : undefined;
|
||||
}
|
||||
if (runtimeChannel === "signal") {
|
||||
const resolved = resolveSignalReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Signal" } : undefined;
|
||||
}
|
||||
return undefined;
|
||||
})()
|
||||
: undefined;
|
||||
// Resolve channel-specific message actions for system prompt
|
||||
const channelActions = runtimeChannel
|
||||
? listChannelSupportedActions({
|
||||
@@ -337,7 +313,6 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
ttsHint,
|
||||
promptMode,
|
||||
runtimeInfo,
|
||||
reactionGuidance,
|
||||
messageToolHints,
|
||||
sandboxInfo,
|
||||
tools,
|
||||
|
||||
@@ -15,7 +15,6 @@ import { resolveChannelCapabilities } from "../../../config/channel-capabilities
|
||||
import { getMachineDisplayName } from "../../../infra/machine-name.js";
|
||||
import { resolveTelegramInlineButtonsScope } from "../../../telegram/inline-buttons.js";
|
||||
import { resolveTelegramReactionLevel } from "../../../telegram/reaction-level.js";
|
||||
import { resolveSignalReactionLevel } from "../../../signal/reaction-level.js";
|
||||
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
||||
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||
@@ -256,25 +255,14 @@ export async function runEmbeddedAttempt(
|
||||
}
|
||||
}
|
||||
const reactionGuidance =
|
||||
runtimeChannel && params.config
|
||||
runtimeChannel === "telegram" && params.config
|
||||
? (() => {
|
||||
if (runtimeChannel === "telegram") {
|
||||
const resolved = resolveTelegramReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Telegram" } : undefined;
|
||||
}
|
||||
if (runtimeChannel === "signal") {
|
||||
const resolved = resolveSignalReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Signal" } : undefined;
|
||||
}
|
||||
return undefined;
|
||||
const resolved = resolveTelegramReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Telegram" } : undefined;
|
||||
})()
|
||||
: undefined;
|
||||
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
||||
|
||||
@@ -94,9 +94,6 @@ function buildReactionSchema() {
|
||||
messageId: Type.Optional(Type.String()),
|
||||
emoji: Type.Optional(Type.String()),
|
||||
remove: Type.Optional(Type.Boolean()),
|
||||
targetAuthor: Type.Optional(Type.String()),
|
||||
targetAuthorUuid: Type.Optional(Type.String()),
|
||||
groupId: Type.Optional(Type.String()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { __testing } from "./web-search.js";
|
||||
|
||||
const { inferPerplexityBaseUrlFromApiKey, resolvePerplexityBaseUrl, normalizeFreshness } =
|
||||
__testing;
|
||||
const { inferPerplexityBaseUrlFromApiKey, resolvePerplexityBaseUrl } = __testing;
|
||||
|
||||
describe("web_search perplexity baseUrl defaults", () => {
|
||||
it("detects a Perplexity key prefix", () => {
|
||||
@@ -52,20 +51,3 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("web_search freshness normalization", () => {
|
||||
it("accepts Brave shortcut values", () => {
|
||||
expect(normalizeFreshness("pd")).toBe("pd");
|
||||
expect(normalizeFreshness("PW")).toBe("pw");
|
||||
});
|
||||
|
||||
it("accepts valid date ranges", () => {
|
||||
expect(normalizeFreshness("2024-01-01to2024-01-31")).toBe("2024-01-01to2024-01-31");
|
||||
});
|
||||
|
||||
it("rejects invalid date ranges", () => {
|
||||
expect(normalizeFreshness("2024-13-01to2024-01-31")).toBeUndefined();
|
||||
expect(normalizeFreshness("2024-02-30to2024-03-01")).toBeUndefined();
|
||||
expect(normalizeFreshness("2024-03-10to2024-03-01")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -29,8 +29,6 @@ const PERPLEXITY_KEY_PREFIXES = ["pplx-"];
|
||||
const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
|
||||
|
||||
const SEARCH_CACHE = new Map<string, CacheEntry<Record<string, unknown>>>();
|
||||
const BRAVE_FRESHNESS_SHORTCUTS = new Set(["pd", "pw", "pm", "py"]);
|
||||
const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
|
||||
|
||||
const WebSearchSchema = Type.Object({
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
@@ -57,12 +55,6 @@ const WebSearchSchema = Type.Object({
|
||||
description: "ISO language code for UI elements.",
|
||||
}),
|
||||
),
|
||||
freshness: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
type WebSearchConfig = NonNullable<ClawdbotConfig["tools"]>["web"] extends infer Web
|
||||
@@ -227,37 +219,6 @@ function resolveSearchCount(value: unknown, fallback: number): number {
|
||||
return clamped;
|
||||
}
|
||||
|
||||
function normalizeFreshness(value: string | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (BRAVE_FRESHNESS_SHORTCUTS.has(lower)) return lower;
|
||||
|
||||
const match = trimmed.match(BRAVE_FRESHNESS_RANGE);
|
||||
if (!match) return undefined;
|
||||
|
||||
const [, start, end] = match;
|
||||
if (!isValidIsoDate(start) || !isValidIsoDate(end)) return undefined;
|
||||
if (start > end) return undefined;
|
||||
|
||||
return `${start}to${end}`;
|
||||
}
|
||||
|
||||
function isValidIsoDate(value: string): boolean {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
||||
const [year, month, day] = value.split("-").map((part) => Number.parseInt(part, 10));
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return false;
|
||||
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
return (
|
||||
date.getUTCFullYear() === year &&
|
||||
date.getUTCMonth() === month - 1 &&
|
||||
date.getUTCDate() === day
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSiteName(url: string | undefined): string | undefined {
|
||||
if (!url) return undefined;
|
||||
try {
|
||||
@@ -318,14 +279,11 @@ async function runWebSearch(params: {
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
freshness?: string;
|
||||
perplexityBaseUrl?: string;
|
||||
perplexityModel?: string;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const cacheKey = normalizeCacheKey(
|
||||
params.provider === "brave"
|
||||
? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
|
||||
: `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`,
|
||||
`${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`,
|
||||
);
|
||||
const cached = readCache(SEARCH_CACHE, cacheKey);
|
||||
if (cached) return { ...cached.value, cached: true };
|
||||
@@ -369,9 +327,6 @@ async function runWebSearch(params: {
|
||||
if (params.ui_lang) {
|
||||
url.searchParams.set("ui_lang", params.ui_lang);
|
||||
}
|
||||
if (params.freshness) {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
}
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
@@ -444,23 +399,6 @@ export function createWebSearchTool(options?: {
|
||||
const country = readStringParam(params, "country");
|
||||
const search_lang = readStringParam(params, "search_lang");
|
||||
const ui_lang = readStringParam(params, "ui_lang");
|
||||
const rawFreshness = readStringParam(params, "freshness");
|
||||
if (rawFreshness && provider !== "brave") {
|
||||
return jsonResult({
|
||||
error: "unsupported_freshness",
|
||||
message: "freshness is only supported by the Brave web_search provider.",
|
||||
docs: "https://docs.clawd.bot/tools/web",
|
||||
});
|
||||
}
|
||||
const freshness = rawFreshness ? normalizeFreshness(rawFreshness) : undefined;
|
||||
if (rawFreshness && !freshness) {
|
||||
return jsonResult({
|
||||
error: "invalid_freshness",
|
||||
message:
|
||||
"freshness must be one of pd, pw, pm, py, or a range like YYYY-MM-DDtoYYYY-MM-DD.",
|
||||
docs: "https://docs.clawd.bot/tools/web",
|
||||
});
|
||||
}
|
||||
const result = await runWebSearch({
|
||||
query,
|
||||
count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
@@ -471,7 +409,6 @@ export function createWebSearchTool(options?: {
|
||||
country,
|
||||
search_lang,
|
||||
ui_lang,
|
||||
freshness,
|
||||
perplexityBaseUrl: resolvePerplexityBaseUrl(
|
||||
perplexityConfig,
|
||||
perplexityAuth?.source,
|
||||
@@ -487,5 +424,4 @@ export function createWebSearchTool(options?: {
|
||||
export const __testing = {
|
||||
inferPerplexityBaseUrlFromApiKey,
|
||||
resolvePerplexityBaseUrl,
|
||||
normalizeFreshness,
|
||||
} as const;
|
||||
@@ -88,40 +88,6 @@ describe("web_search country and language parameters", () => {
|
||||
const url = new URL(mockFetch.mock.calls[0][0] as string);
|
||||
expect(url.searchParams.get("ui_lang")).toBe("de");
|
||||
});
|
||||
|
||||
it("should pass freshness parameter to Brave API", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ web: { results: [] } }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
await tool?.execute?.(1, { query: "test", freshness: "pw" });
|
||||
|
||||
const url = new URL(mockFetch.mock.calls[0][0] as string);
|
||||
expect(url.searchParams.get("freshness")).toBe("pw");
|
||||
});
|
||||
|
||||
it("rejects invalid freshness values", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ web: { results: [] } }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
const result = await tool?.execute?.(1, { query: "test", freshness: "yesterday" });
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(result?.details).toMatchObject({ error: "invalid_freshness" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("web_search perplexity baseUrl defaults", () => {
|
||||
@@ -154,27 +120,6 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://api.perplexity.ai/chat/completions");
|
||||
});
|
||||
|
||||
it("rejects freshness for Perplexity provider", async () => {
|
||||
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: { tools: { web: { search: { provider: "perplexity" } } } },
|
||||
sandboxed: true,
|
||||
});
|
||||
const result = await tool?.execute?.(1, { query: "test", freshness: "pw" });
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(result?.details).toMatchObject({ error: "unsupported_freshness" });
|
||||
});
|
||||
|
||||
it("defaults to OpenRouter when OPENROUTER_API_KEY is set", async () => {
|
||||
vi.stubEnv("PERPLEXITY_API_KEY", "");
|
||||
vi.stubEnv("OPENROUTER_API_KEY", "sk-or-test");
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
import type { ModelDefinitionConfig } from "../config/types.js";
|
||||
|
||||
export const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
|
||||
export const VENICE_DEFAULT_MODEL_ID = "llama-3.3-70b";
|
||||
export const VENICE_DEFAULT_MODEL_REF = `venice/${VENICE_DEFAULT_MODEL_ID}`;
|
||||
|
||||
// Venice uses credit-based pricing, not per-token costs.
|
||||
// Set to 0 as costs vary by model and account type.
|
||||
export const VENICE_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete catalog of Venice AI models.
|
||||
*
|
||||
* Venice provides two privacy modes:
|
||||
* - "private": Fully private inference, no logging, ephemeral
|
||||
* - "anonymized": Proxied through Venice with metadata stripped (for proprietary models)
|
||||
*
|
||||
* Note: The `privacy` field is included for documentation purposes but is not
|
||||
* propagated to ModelDefinitionConfig as it's not part of the core model schema.
|
||||
* Privacy mode is determined by the model itself, not configurable at runtime.
|
||||
*
|
||||
* This catalog serves as a fallback when the Venice API is unreachable.
|
||||
*/
|
||||
export const VENICE_MODEL_CATALOG = [
|
||||
// ============================================
|
||||
// PRIVATE MODELS (Fully private, no logging)
|
||||
// ============================================
|
||||
|
||||
// Llama models
|
||||
{
|
||||
id: "llama-3.3-70b",
|
||||
name: "Llama 3.3 70B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "llama-3.2-3b",
|
||||
name: "Llama 3.2 3B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "hermes-3-llama-3.1-405b",
|
||||
name: "Hermes 3 Llama 3.1 405B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// Qwen models
|
||||
{
|
||||
id: "qwen3-235b-a22b-thinking-2507",
|
||||
name: "Qwen3 235B Thinking",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-235b-a22b-instruct-2507",
|
||||
name: "Qwen3 235B Instruct",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-coder-480b-a35b-instruct",
|
||||
name: "Qwen3 Coder 480B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-next-80b",
|
||||
name: "Qwen3 Next 80B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-vl-235b-a22b",
|
||||
name: "Qwen3 VL 235B (Vision)",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-4b",
|
||||
name: "Venice Small (Qwen3 4B)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 32768,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// DeepSeek
|
||||
{
|
||||
id: "deepseek-v3.2",
|
||||
name: "DeepSeek V3.2",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 163840,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// Venice-specific models
|
||||
{
|
||||
id: "venice-uncensored",
|
||||
name: "Venice Uncensored (Dolphin-Mistral)",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 32768,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "mistral-31-24b",
|
||||
name: "Venice Medium (Mistral)",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// Other private models
|
||||
{
|
||||
id: "google-gemma-3-27b-it",
|
||||
name: "Google Gemma 3 27B Instruct",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "openai-gpt-oss-120b",
|
||||
name: "OpenAI GPT OSS 120B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "zai-org-glm-4.7",
|
||||
name: "GLM 4.7",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// ANONYMIZED MODELS (Proxied through Venice)
|
||||
// These are proprietary models accessed via Venice's proxy
|
||||
// ============================================
|
||||
|
||||
// Anthropic (via Venice)
|
||||
{
|
||||
id: "claude-opus-45",
|
||||
name: "Claude Opus 4.5 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-45",
|
||||
name: "Claude Sonnet 4.5 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// OpenAI (via Venice)
|
||||
{
|
||||
id: "openai-gpt-52",
|
||||
name: "GPT-5.2 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "openai-gpt-52-codex",
|
||||
name: "GPT-5.2 Codex (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// Google (via Venice)
|
||||
{
|
||||
id: "gemini-3-pro-preview",
|
||||
name: "Gemini 3 Pro (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "gemini-3-flash-preview",
|
||||
name: "Gemini 3 Flash (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// xAI (via Venice)
|
||||
{
|
||||
id: "grok-41-fast",
|
||||
name: "Grok 4.1 Fast (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "grok-code-fast-1",
|
||||
name: "Grok Code Fast 1 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// Other anonymized models
|
||||
{
|
||||
id: "kimi-k2-thinking",
|
||||
name: "Kimi K2 Thinking (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "minimax-m21",
|
||||
name: "MiniMax M2.1 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type VeniceCatalogEntry = (typeof VENICE_MODEL_CATALOG)[number];
|
||||
|
||||
/**
|
||||
* Build a ModelDefinitionConfig from a Venice catalog entry.
|
||||
*
|
||||
* Note: The `privacy` field from the catalog is not included in the output
|
||||
* as ModelDefinitionConfig doesn't support custom metadata fields. Privacy
|
||||
* mode is inherent to each model and documented in the catalog/docs.
|
||||
*/
|
||||
export function buildVeniceModelDefinition(entry: VeniceCatalogEntry): ModelDefinitionConfig {
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
reasoning: entry.reasoning,
|
||||
input: [...entry.input],
|
||||
cost: VENICE_DEFAULT_COST,
|
||||
contextWindow: entry.contextWindow,
|
||||
maxTokens: entry.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
// Venice API response types
|
||||
interface VeniceModelSpec {
|
||||
name: string;
|
||||
privacy: "private" | "anonymized";
|
||||
availableContextTokens: number;
|
||||
capabilities: {
|
||||
supportsReasoning: boolean;
|
||||
supportsVision: boolean;
|
||||
supportsFunctionCalling: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface VeniceModel {
|
||||
id: string;
|
||||
model_spec: VeniceModelSpec;
|
||||
}
|
||||
|
||||
interface VeniceModelsResponse {
|
||||
data: VeniceModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover models from Venice API with fallback to static catalog.
|
||||
* The /models endpoint is public and doesn't require authentication.
|
||||
*/
|
||||
export async function discoverVeniceModels(): Promise<ModelDefinitionConfig[]> {
|
||||
// Skip API discovery in test environment
|
||||
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${VENICE_BASE_URL}/models`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`[venice-models] Failed to discover models: HTTP ${response.status}, using static catalog`,
|
||||
);
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as VeniceModelsResponse;
|
||||
if (!Array.isArray(data.data) || data.data.length === 0) {
|
||||
console.warn("[venice-models] No models found from API, using static catalog");
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
|
||||
// Merge discovered models with catalog metadata
|
||||
const catalogById = new Map<string, VeniceCatalogEntry>(
|
||||
VENICE_MODEL_CATALOG.map((m) => [m.id, m]),
|
||||
);
|
||||
const models: ModelDefinitionConfig[] = [];
|
||||
|
||||
for (const apiModel of data.data) {
|
||||
const catalogEntry = catalogById.get(apiModel.id);
|
||||
if (catalogEntry) {
|
||||
// Use catalog metadata for known models
|
||||
models.push(buildVeniceModelDefinition(catalogEntry));
|
||||
} else {
|
||||
// Create definition for newly discovered models not in catalog
|
||||
const isReasoning =
|
||||
apiModel.model_spec.capabilities.supportsReasoning ||
|
||||
apiModel.id.toLowerCase().includes("thinking") ||
|
||||
apiModel.id.toLowerCase().includes("reason") ||
|
||||
apiModel.id.toLowerCase().includes("r1");
|
||||
|
||||
const hasVision = apiModel.model_spec.capabilities.supportsVision;
|
||||
|
||||
models.push({
|
||||
id: apiModel.id,
|
||||
name: apiModel.model_spec.name || apiModel.id,
|
||||
reasoning: isReasoning,
|
||||
input: hasVision ? ["text", "image"] : ["text"],
|
||||
cost: VENICE_DEFAULT_COST,
|
||||
contextWindow: apiModel.model_spec.availableContextTokens || 128000,
|
||||
maxTokens: 8192,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return models.length > 0 ? models : VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
} catch (error) {
|
||||
console.warn(`[venice-models] Discovery failed: ${String(error)}, using static catalog`);
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkMarkdownTextWithMode,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
@@ -276,12 +275,6 @@ describe("chunkByNewline", () => {
|
||||
expect(chunks[4]).toBe("Another short");
|
||||
});
|
||||
|
||||
it("does not split long lines when splitLongLines is false", () => {
|
||||
const text = "a".repeat(50);
|
||||
const chunks = chunkByNewline(text, 20, { splitLongLines: false });
|
||||
expect(chunks).toEqual([text]);
|
||||
});
|
||||
|
||||
it("returns empty array for empty input", () => {
|
||||
expect(chunkByNewline("", 100)).toEqual([]);
|
||||
});
|
||||
@@ -289,18 +282,6 @@ describe("chunkByNewline", () => {
|
||||
it("returns empty array for whitespace-only input", () => {
|
||||
expect(chunkByNewline(" \n\n ", 100)).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves trailing blank lines on the last chunk", () => {
|
||||
const text = "Line one\n\n";
|
||||
const chunks = chunkByNewline(text, 1000);
|
||||
expect(chunks).toEqual(["Line one\n\n"]);
|
||||
});
|
||||
|
||||
it("keeps whitespace when trimLines is false", () => {
|
||||
const text = " indented line \nNext";
|
||||
const chunks = chunkByNewline(text, 1000, { trimLines: false });
|
||||
expect(chunks).toEqual([" indented line ", "Next"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkTextWithMode", () => {
|
||||
@@ -317,26 +298,6 @@ describe("chunkTextWithMode", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkMarkdownTextWithMode", () => {
|
||||
it("uses markdown-aware chunking for length mode", () => {
|
||||
const text = "Line one\nLine two";
|
||||
expect(chunkMarkdownTextWithMode(text, 1000, "length")).toEqual(chunkMarkdownText(text, 1000));
|
||||
});
|
||||
|
||||
it("uses newline-based chunking for newline mode", () => {
|
||||
const text = "Line one\nLine two";
|
||||
expect(chunkMarkdownTextWithMode(text, 1000, "newline")).toEqual(["Line one", "Line two"]);
|
||||
});
|
||||
|
||||
it("does not split inside code fences for newline mode", () => {
|
||||
const text = "```js\nconst a = 1;\nconst b = 2;\n```\nAfter";
|
||||
expect(chunkMarkdownTextWithMode(text, 1000, "newline")).toEqual([
|
||||
"```js\nconst a = 1;\nconst b = 2;\n```",
|
||||
"After",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChunkMode", () => {
|
||||
it("returns length as default", () => {
|
||||
expect(resolveChunkMode(undefined, "telegram")).toBe("length");
|
||||
@@ -349,16 +310,16 @@ describe("resolveChunkMode", () => {
|
||||
expect(resolveChunkMode(cfg, "__internal__")).toBe("length");
|
||||
});
|
||||
|
||||
it("supports provider-level overrides for slack", () => {
|
||||
const cfg = { channels: { slack: { chunkMode: "newline" as const } } };
|
||||
expect(resolveChunkMode(cfg, "slack")).toBe("newline");
|
||||
it("supports provider-level overrides for bluebubbles", () => {
|
||||
const cfg = { channels: { bluebubbles: { chunkMode: "newline" as const } } };
|
||||
expect(resolveChunkMode(cfg, "bluebubbles")).toBe("newline");
|
||||
expect(resolveChunkMode(cfg, "discord")).toBe("length");
|
||||
});
|
||||
|
||||
it("supports account-level overrides for slack", () => {
|
||||
it("supports account-level overrides for bluebubbles", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
slack: {
|
||||
bluebubbles: {
|
||||
chunkMode: "length" as const,
|
||||
accounts: {
|
||||
primary: { chunkMode: "newline" as const },
|
||||
@@ -366,7 +327,12 @@ describe("resolveChunkMode", () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(resolveChunkMode(cfg, "slack", "primary")).toBe("newline");
|
||||
expect(resolveChunkMode(cfg, "slack", "other")).toBe("length");
|
||||
expect(resolveChunkMode(cfg, "bluebubbles", "primary")).toBe("newline");
|
||||
expect(resolveChunkMode(cfg, "bluebubbles", "other")).toBe("length");
|
||||
});
|
||||
|
||||
it("ignores chunkMode for non-bluebubbles providers", () => {
|
||||
const cfg = { channels: { ["telegram" as string]: { chunkMode: "newline" as const } } };
|
||||
expect(resolveChunkMode(cfg, "telegram")).toBe("length");
|
||||
});
|
||||
});
|
||||
+11
-73
@@ -101,6 +101,8 @@ export function resolveChunkMode(
|
||||
accountId?: string | null,
|
||||
): ChunkMode {
|
||||
if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return DEFAULT_CHUNK_MODE;
|
||||
// Chunk mode is only supported for BlueBubbles.
|
||||
if (provider !== "bluebubbles") return DEFAULT_CHUNK_MODE;
|
||||
const channelsConfig = cfg?.channels as Record<string, unknown> | undefined;
|
||||
const providerConfig = (channelsConfig?.[provider] ??
|
||||
(cfg as Record<string, unknown> | undefined)?.[provider]) as ProviderChunkConfig | undefined;
|
||||
@@ -109,24 +111,13 @@ export function resolveChunkMode(
|
||||
}
|
||||
|
||||
/**
|
||||
* Split text on newlines, trimming line whitespace.
|
||||
* Blank lines are folded into the next non-empty line as leading "\n" prefixes.
|
||||
* Long lines can be split by length (default) or kept intact via splitLongLines:false.
|
||||
* Split text on newlines, filtering empty lines.
|
||||
* Lines exceeding maxLineLength are further split using length-based chunking.
|
||||
*/
|
||||
export function chunkByNewline(
|
||||
text: string,
|
||||
maxLineLength: number,
|
||||
opts?: {
|
||||
splitLongLines?: boolean;
|
||||
trimLines?: boolean;
|
||||
isSafeBreak?: (index: number) => boolean;
|
||||
},
|
||||
): string[] {
|
||||
export function chunkByNewline(text: string, maxLineLength: number): string[] {
|
||||
if (!text) return [];
|
||||
if (maxLineLength <= 0) return text.trim() ? [text] : [];
|
||||
const splitLongLines = opts?.splitLongLines !== false;
|
||||
const trimLines = opts?.trimLines !== false;
|
||||
const lines = splitByNewline(text, opts?.isSafeBreak);
|
||||
const lines = text.split("\n");
|
||||
const chunks: string[] = [];
|
||||
let pendingBlankLines = 0;
|
||||
|
||||
@@ -142,25 +133,20 @@ export function chunkByNewline(
|
||||
const prefix = cappedBlankLines > 0 ? "\n".repeat(cappedBlankLines) : "";
|
||||
pendingBlankLines = 0;
|
||||
|
||||
const lineValue = trimLines ? trimmed : line;
|
||||
if (!splitLongLines || lineValue.length + prefix.length <= maxLineLength) {
|
||||
chunks.push(prefix + lineValue);
|
||||
if (trimmed.length + prefix.length <= maxLineLength) {
|
||||
chunks.push(prefix + trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstLimit = Math.max(1, maxLineLength - prefix.length);
|
||||
const first = lineValue.slice(0, firstLimit);
|
||||
const first = trimmed.slice(0, firstLimit);
|
||||
chunks.push(prefix + first);
|
||||
const remaining = lineValue.slice(firstLimit);
|
||||
const remaining = trimmed.slice(firstLimit);
|
||||
if (remaining) {
|
||||
chunks.push(...chunkText(remaining, maxLineLength));
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingBlankLines > 0 && chunks.length > 0) {
|
||||
chunks[chunks.length - 1] += "\n".repeat(pendingBlankLines);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
@@ -169,59 +155,11 @@ export function chunkByNewline(
|
||||
*/
|
||||
export function chunkTextWithMode(text: string, limit: number, mode: ChunkMode): string[] {
|
||||
if (mode === "newline") {
|
||||
const chunks: string[] = [];
|
||||
const lineChunks = chunkByNewline(text, limit, { splitLongLines: false });
|
||||
for (const line of lineChunks) {
|
||||
const nested = chunkText(line, limit);
|
||||
if (!nested.length && line) {
|
||||
chunks.push(line);
|
||||
continue;
|
||||
}
|
||||
chunks.push(...nested);
|
||||
}
|
||||
return chunks;
|
||||
return chunkByNewline(text, limit);
|
||||
}
|
||||
return chunkText(text, limit);
|
||||
}
|
||||
|
||||
export function chunkMarkdownTextWithMode(text: string, limit: number, mode: ChunkMode): string[] {
|
||||
if (mode === "newline") {
|
||||
const spans = parseFenceSpans(text);
|
||||
const chunks: string[] = [];
|
||||
const lineChunks = chunkByNewline(text, limit, {
|
||||
splitLongLines: false,
|
||||
trimLines: false,
|
||||
isSafeBreak: (index) => isSafeFenceBreak(spans, index),
|
||||
});
|
||||
for (const line of lineChunks) {
|
||||
const nested = chunkMarkdownText(line, limit);
|
||||
if (!nested.length && line) {
|
||||
chunks.push(line);
|
||||
continue;
|
||||
}
|
||||
chunks.push(...nested);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
return chunkMarkdownText(text, limit);
|
||||
}
|
||||
|
||||
function splitByNewline(
|
||||
text: string,
|
||||
isSafeBreak: (index: number) => boolean = () => true,
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
let start = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i] === "\n" && isSafeBreak(i)) {
|
||||
lines.push(text.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
lines.push(text.slice(start));
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function chunkText(text: string, limit: number): string[] {
|
||||
if (!text) return [];
|
||||
if (limit <= 0) return [text];
|
||||
|
||||
@@ -69,7 +69,7 @@ export function resolveBlockStreamingChunking(
|
||||
});
|
||||
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
|
||||
|
||||
// When chunkMode is "newline", use newline-based streaming
|
||||
// BlueBubbles-only: if chunkMode is "newline", use newline-based streaming
|
||||
const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId);
|
||||
if (channelChunkMode === "newline") {
|
||||
// For newline mode: use very low minChars to flush quickly on newlines
|
||||
@@ -103,7 +103,7 @@ export function resolveBlockStreamingCoalescing(
|
||||
): BlockStreamingCoalescing | undefined {
|
||||
const providerKey = normalizeChunkProvider(provider);
|
||||
|
||||
// When chunkMode is "newline", disable coalescing to send each line immediately
|
||||
// BlueBubbles-only: when chunkMode is "newline", disable coalescing to send each line immediately
|
||||
const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId);
|
||||
if (channelChunkMode === "newline") {
|
||||
return undefined;
|
||||
|
||||
@@ -7,11 +7,9 @@ import {
|
||||
getTtsProvider,
|
||||
isSummarizationEnabled,
|
||||
isTtsEnabled,
|
||||
isTtsProviderConfigured,
|
||||
resolveTtsApiKey,
|
||||
resolveTtsConfig,
|
||||
resolveTtsPrefsPath,
|
||||
resolveTtsProviderOrder,
|
||||
setLastTtsAttempt,
|
||||
setSummarizationEnabled,
|
||||
setTtsEnabled,
|
||||
@@ -43,7 +41,6 @@ function ttsUsage(): ReplyPayload {
|
||||
"\nExamples:\n" +
|
||||
"/tts on\n" +
|
||||
"/tts provider openai\n" +
|
||||
"/tts provider edge\n" +
|
||||
"/tts limit 2000\n" +
|
||||
"/tts summary off\n" +
|
||||
"/tts audio Hello from Clawdbot",
|
||||
@@ -129,45 +126,33 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
if (action === "provider") {
|
||||
const currentProvider = getTtsProvider(config, prefsPath);
|
||||
if (!args.trim()) {
|
||||
const fallback = resolveTtsProviderOrder(currentProvider)
|
||||
.slice(1)
|
||||
.filter((provider) => isTtsProviderConfigured(config, provider));
|
||||
const fallback = currentProvider === "openai" ? "elevenlabs" : "openai";
|
||||
const hasOpenAI = Boolean(resolveTtsApiKey(config, "openai"));
|
||||
const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs"));
|
||||
const hasEdge = isTtsProviderConfigured(config, "edge");
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
`🎙️ TTS provider\n` +
|
||||
`Primary: ${currentProvider}\n` +
|
||||
`Fallbacks: ${fallback.join(", ") || "none"}\n` +
|
||||
`Fallback: ${fallback}\n` +
|
||||
`OpenAI key: ${hasOpenAI ? "✅" : "❌"}\n` +
|
||||
`ElevenLabs key: ${hasElevenLabs ? "✅" : "❌"}\n` +
|
||||
`Edge enabled: ${hasEdge ? "✅" : "❌"}\n` +
|
||||
`Usage: /tts provider openai | elevenlabs | edge`,
|
||||
`Usage: /tts provider openai | elevenlabs`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const requested = args.trim().toLowerCase();
|
||||
if (requested !== "openai" && requested !== "elevenlabs" && requested !== "edge") {
|
||||
if (requested !== "openai" && requested !== "elevenlabs") {
|
||||
return { shouldContinue: false, reply: ttsUsage() };
|
||||
}
|
||||
|
||||
setTtsProvider(prefsPath, requested);
|
||||
const fallback = resolveTtsProviderOrder(requested)
|
||||
.slice(1)
|
||||
.filter((provider) => isTtsProviderConfigured(config, provider));
|
||||
const fallback = requested === "openai" ? "elevenlabs" : "openai";
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
`✅ TTS provider set to ${requested} (fallbacks: ${fallback.join(", ") || "none"}).` +
|
||||
(requested === "edge"
|
||||
? "\nEnable Edge TTS in config: messages.tts.edge.enabled = true."
|
||||
: ""),
|
||||
},
|
||||
reply: { text: `✅ TTS provider set to ${requested} (fallback: ${fallback}).` },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,22 +199,14 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
if (action === "status") {
|
||||
const enabled = isTtsEnabled(config, prefsPath);
|
||||
const provider = getTtsProvider(config, prefsPath);
|
||||
const hasKey = isTtsProviderConfigured(config, provider);
|
||||
const providerStatus =
|
||||
provider === "edge"
|
||||
? hasKey
|
||||
? "✅ enabled"
|
||||
: "❌ disabled"
|
||||
: hasKey
|
||||
? "✅ key"
|
||||
: "❌ no key";
|
||||
const hasKey = Boolean(resolveTtsApiKey(config, provider));
|
||||
const maxLength = getTtsMaxLength(prefsPath);
|
||||
const summarize = isSummarizationEnabled(prefsPath);
|
||||
const last = getLastTtsAttempt();
|
||||
const lines = [
|
||||
"📊 TTS status",
|
||||
`State: ${enabled ? "✅ enabled" : "❌ disabled"}`,
|
||||
`Provider: ${provider} (${providerStatus})`,
|
||||
`Provider: ${provider} (${hasKey ? "✅ key" : "❌ no key"})`,
|
||||
`Text limit: ${maxLength} chars`,
|
||||
`Auto-summary: ${summarize ? "on" : "off"}`,
|
||||
];
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import { signalMessageActions } from "./signal.js";
|
||||
|
||||
const sendReactionSignal = vi.fn(async () => ({ ok: true }));
|
||||
const removeReactionSignal = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
vi.mock("../../../signal/send-reactions.js", () => ({
|
||||
sendReactionSignal: (...args: unknown[]) => sendReactionSignal(...args),
|
||||
removeReactionSignal: (...args: unknown[]) => removeReactionSignal(...args),
|
||||
}));
|
||||
|
||||
describe("signalMessageActions", () => {
|
||||
it("returns no actions when no configured accounts exist", () => {
|
||||
const cfg = {} as ClawdbotConfig;
|
||||
expect(signalMessageActions.listActions({ cfg })).toEqual([]);
|
||||
});
|
||||
|
||||
it("hides react when reactions are disabled", () => {
|
||||
const cfg = {
|
||||
channels: { signal: { account: "+15550001111", actions: { reactions: false } } },
|
||||
} as ClawdbotConfig;
|
||||
expect(signalMessageActions.listActions({ cfg })).toEqual(["send"]);
|
||||
});
|
||||
|
||||
it("enables react when at least one account allows reactions", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
signal: {
|
||||
actions: { reactions: false },
|
||||
accounts: {
|
||||
work: { account: "+15550001111", actions: { reactions: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(signalMessageActions.listActions({ cfg })).toEqual(["send", "react"]);
|
||||
});
|
||||
|
||||
it("skips send for plugin dispatch", () => {
|
||||
expect(signalMessageActions.supportsAction?.({ action: "send" })).toBe(false);
|
||||
expect(signalMessageActions.supportsAction?.({ action: "react" })).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks reactions when action gate is disabled", async () => {
|
||||
const cfg = {
|
||||
channels: { signal: { account: "+15550001111", actions: { reactions: false } } },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
await expect(
|
||||
signalMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: { to: "+15550001111", messageId: "123", emoji: "✅" },
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
}),
|
||||
).rejects.toThrow(/actions\.reactions/);
|
||||
});
|
||||
|
||||
it("uses account-level actions when enabled", async () => {
|
||||
sendReactionSignal.mockClear();
|
||||
const cfg = {
|
||||
channels: {
|
||||
signal: {
|
||||
actions: { reactions: false },
|
||||
accounts: {
|
||||
work: { account: "+15550001111", actions: { reactions: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
await signalMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: { to: "+15550001111", messageId: "123", emoji: "👍" },
|
||||
cfg,
|
||||
accountId: "work",
|
||||
});
|
||||
|
||||
expect(sendReactionSignal).toHaveBeenCalledWith("+15550001111", 123, "👍", {
|
||||
accountId: "work",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes uuid recipients", async () => {
|
||||
sendReactionSignal.mockClear();
|
||||
const cfg = {
|
||||
channels: { signal: { account: "+15550001111" } },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
await signalMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: {
|
||||
recipient: "uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
messageId: "123",
|
||||
emoji: "🔥",
|
||||
},
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
});
|
||||
|
||||
expect(sendReactionSignal).toHaveBeenCalledWith(
|
||||
"123e4567-e89b-12d3-a456-426614174000",
|
||||
123,
|
||||
"🔥",
|
||||
{ accountId: undefined },
|
||||
);
|
||||
});
|
||||
|
||||
it("requires targetAuthor for group reactions", async () => {
|
||||
const cfg = {
|
||||
channels: { signal: { account: "+15550001111" } },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
await expect(
|
||||
signalMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: { to: "signal:group:group-id", messageId: "123", emoji: "✅" },
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
}),
|
||||
).rejects.toThrow(/targetAuthor/);
|
||||
});
|
||||
|
||||
it("passes groupId and targetAuthor for group reactions", async () => {
|
||||
sendReactionSignal.mockClear();
|
||||
const cfg = {
|
||||
channels: { signal: { account: "+15550001111" } },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
await signalMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: {
|
||||
to: "signal:group:group-id",
|
||||
targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
messageId: "123",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
});
|
||||
|
||||
expect(sendReactionSignal).toHaveBeenCalledWith("", 123, "✅", {
|
||||
accountId: undefined,
|
||||
groupId: "group-id",
|
||||
targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
targetAuthorUuid: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import { createActionGate, jsonResult, readStringParam } from "../../../agents/tools/common.js";
|
||||
import { listEnabledSignalAccounts, resolveSignalAccount } from "../../../signal/accounts.js";
|
||||
import { resolveSignalReactionLevel } from "../../../signal/reaction-level.js";
|
||||
import { sendReactionSignal, removeReactionSignal } from "../../../signal/send-reactions.js";
|
||||
import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../types.js";
|
||||
|
||||
const providerId = "signal";
|
||||
const GROUP_PREFIX = "group:";
|
||||
|
||||
function normalizeSignalReactionRecipient(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
const withoutSignal = trimmed.replace(/^signal:/i, "").trim();
|
||||
if (!withoutSignal) return withoutSignal;
|
||||
if (withoutSignal.toLowerCase().startsWith("uuid:")) {
|
||||
return withoutSignal.slice("uuid:".length).trim();
|
||||
}
|
||||
return withoutSignal;
|
||||
}
|
||||
|
||||
function resolveSignalReactionTarget(raw: string): { recipient?: string; groupId?: string } {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return {};
|
||||
const withoutSignal = trimmed.replace(/^signal:/i, "").trim();
|
||||
if (!withoutSignal) return {};
|
||||
if (withoutSignal.toLowerCase().startsWith(GROUP_PREFIX)) {
|
||||
const groupId = withoutSignal.slice(GROUP_PREFIX.length).trim();
|
||||
return groupId ? { groupId } : {};
|
||||
}
|
||||
return { recipient: normalizeSignalReactionRecipient(withoutSignal) };
|
||||
}
|
||||
|
||||
export const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
const accounts = listEnabledSignalAccounts(cfg);
|
||||
if (accounts.length === 0) return [];
|
||||
const configuredAccounts = accounts.filter((account) => account.configured);
|
||||
if (configuredAccounts.length === 0) return [];
|
||||
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
|
||||
const reactionsEnabled = configuredAccounts.some((account) =>
|
||||
createActionGate(account.config.actions)("reactions"),
|
||||
);
|
||||
if (reactionsEnabled) {
|
||||
actions.add("react");
|
||||
}
|
||||
|
||||
return Array.from(actions);
|
||||
},
|
||||
supportsAction: ({ action }) => action !== "send",
|
||||
|
||||
handleAction: async ({ action, params, cfg, accountId }) => {
|
||||
if (action === "send") {
|
||||
throw new Error("Send should be handled by outbound, not actions handler.");
|
||||
}
|
||||
|
||||
if (action === "react") {
|
||||
// Check reaction level first
|
||||
const reactionLevelInfo = resolveSignalReactionLevel({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
if (!reactionLevelInfo.agentReactionsEnabled) {
|
||||
throw new Error(
|
||||
`Signal agent reactions disabled (reactionLevel="${reactionLevelInfo.level}"). ` +
|
||||
`Set channels.signal.reactionLevel to "minimal" or "extensive" to enable.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Also check the action gate for backward compatibility
|
||||
const actionConfig = resolveSignalAccount({ cfg, accountId }).config.actions;
|
||||
const isActionEnabled = createActionGate(actionConfig);
|
||||
if (!isActionEnabled("reactions")) {
|
||||
throw new Error("Signal reactions are disabled via actions.reactions.");
|
||||
}
|
||||
|
||||
const recipientRaw =
|
||||
readStringParam(params, "recipient") ??
|
||||
readStringParam(params, "to", {
|
||||
required: true,
|
||||
label: "recipient (UUID, phone number, or group)",
|
||||
});
|
||||
const target = resolveSignalReactionTarget(recipientRaw);
|
||||
if (!target.recipient && !target.groupId) {
|
||||
throw new Error("recipient or group required");
|
||||
}
|
||||
|
||||
const messageId = readStringParam(params, "messageId", {
|
||||
required: true,
|
||||
label: "messageId (timestamp)",
|
||||
});
|
||||
const targetAuthor = readStringParam(params, "targetAuthor");
|
||||
const targetAuthorUuid = readStringParam(params, "targetAuthorUuid");
|
||||
if (target.groupId && !targetAuthor && !targetAuthorUuid) {
|
||||
throw new Error("targetAuthor or targetAuthorUuid required for group reactions.");
|
||||
}
|
||||
|
||||
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
|
||||
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
|
||||
|
||||
const timestamp = parseInt(messageId, 10);
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
throw new Error(`Invalid messageId: ${messageId}. Expected numeric timestamp.`);
|
||||
}
|
||||
|
||||
if (remove) {
|
||||
if (!emoji) throw new Error("Emoji required to remove reaction.");
|
||||
await removeReactionSignal(target.recipient ?? "", timestamp, emoji, {
|
||||
accountId: accountId ?? undefined,
|
||||
groupId: target.groupId,
|
||||
targetAuthor,
|
||||
targetAuthorUuid,
|
||||
});
|
||||
return jsonResult({ ok: true, removed: emoji });
|
||||
}
|
||||
|
||||
if (!emoji) throw new Error("Emoji required to add reaction.");
|
||||
await sendReactionSignal(target.recipient ?? "", timestamp, emoji, {
|
||||
accountId: accountId ?? undefined,
|
||||
groupId: target.groupId,
|
||||
targetAuthor,
|
||||
targetAuthorUuid,
|
||||
});
|
||||
return jsonResult({ ok: true, added: emoji });
|
||||
}
|
||||
|
||||
throw new Error(`Action ${action} not supported for ${providerId}.`);
|
||||
},
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./signal.js";
|
||||
|
||||
describe("signal target normalization", () => {
|
||||
it("normalizes uuid targets by stripping uuid:", () => {
|
||||
expect(normalizeSignalMessagingTarget("uuid:123E4567-E89B-12D3-A456-426614174000")).toBe(
|
||||
"123e4567-e89b-12d3-a456-426614174000",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes signal:uuid targets", () => {
|
||||
expect(normalizeSignalMessagingTarget("signal:uuid:123E4567-E89B-12D3-A456-426614174000")).toBe(
|
||||
"123e4567-e89b-12d3-a456-426614174000",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts uuid prefixes for target detection", () => {
|
||||
expect(looksLikeSignalTargetId("uuid:123e4567-e89b-12d3-a456-426614174000")).toBe(true);
|
||||
expect(looksLikeSignalTargetId("signal:uuid:123e4567-e89b-12d3-a456-426614174000")).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts compact UUIDs for target detection", () => {
|
||||
expect(looksLikeSignalTargetId("123e4567e89b12d3a456426614174000")).toBe(true);
|
||||
expect(looksLikeSignalTargetId("uuid:123e4567e89b12d3a456426614174000")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid uuid prefixes", () => {
|
||||
expect(looksLikeSignalTargetId("uuid:")).toBe(false);
|
||||
expect(looksLikeSignalTargetId("uuid:not-a-uuid")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -19,30 +19,12 @@ export function normalizeSignalMessagingTarget(raw: string): string | undefined
|
||||
const id = normalized.slice("u:".length).trim();
|
||||
return id ? `username:${id}`.toLowerCase() : undefined;
|
||||
}
|
||||
if (lower.startsWith("uuid:")) {
|
||||
const id = normalized.slice("uuid:".length).trim();
|
||||
return id ? id.toLowerCase() : undefined;
|
||||
}
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
// UUID pattern for signal-cli recipient IDs
|
||||
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const UUID_COMPACT_PATTERN = /^[0-9a-f]{32}$/i;
|
||||
|
||||
export function looksLikeSignalTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^(signal:)?(group:|username:|u:)/i.test(trimmed)) return true;
|
||||
if (/^(signal:)?uuid:/i.test(trimmed)) {
|
||||
const stripped = trimmed
|
||||
.replace(/^signal:/i, "")
|
||||
.replace(/^uuid:/i, "")
|
||||
.trim();
|
||||
if (!stripped) return false;
|
||||
return UUID_PATTERN.test(stripped) || UUID_COMPACT_PATTERN.test(stripped);
|
||||
}
|
||||
// Accept UUIDs (used by signal-cli for reactions)
|
||||
if (UUID_PATTERN.test(trimmed) || UUID_COMPACT_PATTERN.test(trimmed)) return true;
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import type { ChannelOutboundAdapter } from "../types.js";
|
||||
export const imessageOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? sendMessageIMessage;
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { ChannelOutboundAdapter } from "../types.js";
|
||||
export const signalOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendSignal ?? sendMessageSignal;
|
||||
|
||||
@@ -21,7 +21,6 @@ function parseThreadId(threadId?: string | number | null) {
|
||||
export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: markdownToTelegramHtmlChunks,
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
|
||||
const send = deps?.sendTelegram ?? sendMessageTelegram;
|
||||
|
||||
@@ -8,7 +8,6 @@ import { missingTargetError } from "../../../infra/outbound/target-errors.js";
|
||||
export const whatsappOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "gateway",
|
||||
chunker: chunkText,
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
pollMaxOptions: 12,
|
||||
resolveTarget: ({ to, allowFrom, mode }) => {
|
||||
|
||||
@@ -84,7 +84,6 @@ export type ChannelOutboundContext = {
|
||||
export type ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct" | "gateway" | "hybrid";
|
||||
chunker?: ((text: string, limit: number) => string[]) | null;
|
||||
chunkerMode?: "text" | "markdown";
|
||||
textChunkLimit?: number;
|
||||
pollMaxOptions?: number;
|
||||
resolveTarget?: (params: {
|
||||
|
||||
@@ -67,28 +67,6 @@ describe("cli program (smoke)", () => {
|
||||
expect(messageCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs message react with signal author fields", async () => {
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(
|
||||
[
|
||||
"message",
|
||||
"react",
|
||||
"--channel",
|
||||
"signal",
|
||||
"--target",
|
||||
"signal:group:abc123",
|
||||
"--message-id",
|
||||
"1737630212345",
|
||||
"--emoji",
|
||||
"✅",
|
||||
"--target-author-uuid",
|
||||
"123e4567-e89b-12d3-a456-426614174000",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(messageCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs status command", async () => {
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(["status"], { from: "user" });
|
||||
|
||||
@@ -13,8 +13,6 @@ export function registerMessageReactionsCommands(message: Command, helpers: Mess
|
||||
.option("--remove", "Remove reaction", false)
|
||||
.option("--participant <id>", "WhatsApp reaction participant")
|
||||
.option("--from-me", "WhatsApp reaction fromMe", false)
|
||||
.option("--target-author <id>", "Signal reaction target author (uuid or phone)")
|
||||
.option("--target-author-uuid <uuid>", "Signal reaction target author uuid")
|
||||
.action(async (opts) => {
|
||||
await helpers.runMessageAction("react", opts);
|
||||
});
|
||||
|
||||
+7
-6
@@ -11,7 +11,7 @@ import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||
import { formatUncaughtError } from "../infra/errors.js";
|
||||
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||
import { enableConsoleCapture } from "../logging.js";
|
||||
import { getPrimaryCommand } from "./argv.js";
|
||||
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||
import { tryRouteCli } from "./route.js";
|
||||
|
||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||
@@ -50,11 +50,12 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
});
|
||||
|
||||
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
|
||||
// Register the primary subcommand if one exists (for lazy-loading)
|
||||
const primary = getPrimaryCommand(parseArgv);
|
||||
if (primary) {
|
||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||
await registerSubCliByName(program, primary);
|
||||
if (hasHelpOrVersion(parseArgv)) {
|
||||
const primary = getPrimaryCommand(parseArgv);
|
||||
if (primary) {
|
||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||
await registerSubCliByName(program, primary);
|
||||
}
|
||||
}
|
||||
await program.parseAsync(parseArgv);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export type AuthChoiceGroupId =
|
||||
| "opencode-zen"
|
||||
| "minimax"
|
||||
| "synthetic"
|
||||
| "venice"
|
||||
| "qwen";
|
||||
|
||||
export type AuthChoiceGroup = {
|
||||
@@ -67,12 +66,6 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||
hint: "Anthropic-compatible (multi-model)",
|
||||
choices: ["synthetic-api-key"],
|
||||
},
|
||||
{
|
||||
value: "venice",
|
||||
label: "Venice AI",
|
||||
hint: "Privacy-focused (uncensored models)",
|
||||
choices: ["venice-api-key"],
|
||||
},
|
||||
{
|
||||
value: "google",
|
||||
label: "Google",
|
||||
@@ -197,11 +190,6 @@ export function buildAuthChoiceOptions(params: {
|
||||
options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" });
|
||||
options.push({ value: "kimi-code-api-key", label: "Kimi Code API key" });
|
||||
options.push({ value: "synthetic-api-key", label: "Synthetic API key" });
|
||||
options.push({
|
||||
value: "venice-api-key",
|
||||
label: "Venice AI API key",
|
||||
hint: "Privacy-focused inference (uncensored models)",
|
||||
});
|
||||
options.push({
|
||||
value: "github-copilot",
|
||||
label: "GitHub Copilot (GitHub device login)",
|
||||
|
||||
@@ -23,8 +23,6 @@ import {
|
||||
applyOpenrouterProviderConfig,
|
||||
applySyntheticConfig,
|
||||
applySyntheticProviderConfig,
|
||||
applyVeniceConfig,
|
||||
applyVeniceProviderConfig,
|
||||
applyVercelAiGatewayConfig,
|
||||
applyVercelAiGatewayProviderConfig,
|
||||
applyZaiConfig,
|
||||
@@ -32,7 +30,6 @@ import {
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
setGeminiApiKey,
|
||||
setKimiCodeApiKey,
|
||||
@@ -40,7 +37,6 @@ import {
|
||||
setOpencodeZenApiKey,
|
||||
setOpenrouterApiKey,
|
||||
setSyntheticApiKey,
|
||||
setVeniceApiKey,
|
||||
setVercelAiGatewayApiKey,
|
||||
setZaiApiKey,
|
||||
ZAI_DEFAULT_MODEL_REF,
|
||||
@@ -81,8 +77,6 @@ export async function applyAuthChoiceApiProviders(
|
||||
authChoice = "zai-api-key";
|
||||
} else if (params.opts.tokenProvider === "synthetic") {
|
||||
authChoice = "synthetic-api-key";
|
||||
} else if (params.opts.tokenProvider === "venice") {
|
||||
authChoice = "venice-api-key";
|
||||
} else if (params.opts.tokenProvider === "opencode") {
|
||||
authChoice = "opencode-zen";
|
||||
}
|
||||
@@ -463,65 +457,6 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (authChoice === "venice-api-key") {
|
||||
let hasCredential = false;
|
||||
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "venice") {
|
||||
await setVeniceApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Venice AI provides privacy-focused inference with uncensored models.",
|
||||
"Get your API key at: https://venice.ai/settings/api",
|
||||
"Supports 'private' (fully private) and 'anonymized' (proxy) modes.",
|
||||
].join("\n"),
|
||||
"Venice AI",
|
||||
);
|
||||
}
|
||||
|
||||
const envKey = resolveEnvApiKey("venice");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing VENICE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
await setVeniceApiKey(envKey.apiKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter Venice AI API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setVeniceApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "venice:default",
|
||||
provider: "venice",
|
||||
mode: "api_key",
|
||||
});
|
||||
{
|
||||
const applied = await applyDefaultModelChoice({
|
||||
config: nextConfig,
|
||||
setDefaultModel: params.setDefaultModel,
|
||||
defaultModel: VENICE_DEFAULT_MODEL_REF,
|
||||
applyDefaultConfig: applyVeniceConfig,
|
||||
applyProviderConfig: applyVeniceProviderConfig,
|
||||
noteDefault: VENICE_DEFAULT_MODEL_REF,
|
||||
noteAgentModel,
|
||||
prompter: params.prompter,
|
||||
});
|
||||
nextConfig = applied.config;
|
||||
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
||||
}
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (authChoice === "opencode-zen") {
|
||||
let hasCredential = false;
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") {
|
||||
|
||||
@@ -19,7 +19,6 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"google-gemini-cli": "google-gemini-cli",
|
||||
"zai-api-key": "zai",
|
||||
"synthetic-api-key": "synthetic",
|
||||
"venice-api-key": "venice",
|
||||
"github-copilot": "github-copilot",
|
||||
"copilot-proxy": "copilot-proxy",
|
||||
"minimax-cloud": "minimax",
|
||||
|
||||
@@ -4,12 +4,6 @@ import {
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
SYNTHETIC_MODEL_CATALOG,
|
||||
} from "../agents/synthetic-models.js";
|
||||
import {
|
||||
buildVeniceModelDefinition,
|
||||
VENICE_BASE_URL,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
VENICE_MODEL_CATALOG,
|
||||
} from "../agents/venice-models.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
@@ -336,81 +330,6 @@ export function applySyntheticConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Venice provider configuration without changing the default model.
|
||||
* Registers Venice models and sets up the provider, but preserves existing model selection.
|
||||
*/
|
||||
export function applyVeniceProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[VENICE_DEFAULT_MODEL_REF] = {
|
||||
...models[VENICE_DEFAULT_MODEL_REF],
|
||||
alias: models[VENICE_DEFAULT_MODEL_REF]?.alias ?? "Llama 3.3 70B",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers.venice;
|
||||
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||
const veniceModels = VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
const mergedModels = [
|
||||
...existingModels,
|
||||
...veniceModels.filter((model) => !existingModels.some((existing) => existing.id === model.id)),
|
||||
];
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
> as { apiKey?: string };
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim();
|
||||
providers.venice = {
|
||||
...existingProviderRest,
|
||||
baseUrl: VENICE_BASE_URL,
|
||||
api: "openai-completions",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : veniceModels,
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Venice provider configuration AND set Venice as the default model.
|
||||
* Use this when Venice is the primary provider choice during onboarding.
|
||||
*/
|
||||
export function applyVeniceConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const next = applyVeniceProviderConfig(cfg);
|
||||
const existingModel = next.agents?.defaults?.model;
|
||||
return {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...next.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: VENICE_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAuthProfileConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
params: {
|
||||
|
||||
@@ -99,19 +99,6 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function setVeniceApiKey(key: string, agentDir?: string) {
|
||||
// Write to resolved agent dir so gateway finds credentials on startup.
|
||||
upsertAuthProfile({
|
||||
profileId: "venice:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "venice",
|
||||
key,
|
||||
},
|
||||
agentDir: resolveAuthAgentDir(agentDir),
|
||||
});
|
||||
}
|
||||
|
||||
export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7";
|
||||
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
|
||||
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5";
|
||||
|
||||
@@ -2,7 +2,6 @@ export {
|
||||
SYNTHETIC_DEFAULT_MODEL_ID,
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
} from "../agents/synthetic-models.js";
|
||||
export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js";
|
||||
export {
|
||||
applyAuthProfileConfig,
|
||||
applyKimiCodeConfig,
|
||||
@@ -13,8 +12,6 @@ export {
|
||||
applyOpenrouterProviderConfig,
|
||||
applySyntheticConfig,
|
||||
applySyntheticProviderConfig,
|
||||
applyVeniceConfig,
|
||||
applyVeniceProviderConfig,
|
||||
applyVercelAiGatewayConfig,
|
||||
applyVercelAiGatewayProviderConfig,
|
||||
applyZaiConfig,
|
||||
@@ -42,7 +39,6 @@ export {
|
||||
setOpencodeZenApiKey,
|
||||
setOpenrouterApiKey,
|
||||
setSyntheticApiKey,
|
||||
setVeniceApiKey,
|
||||
setVercelAiGatewayApiKey,
|
||||
setZaiApiKey,
|
||||
writeOAuthCredentials,
|
||||
|
||||
@@ -16,7 +16,6 @@ export type AuthChoice =
|
||||
| "moonshot-api-key"
|
||||
| "kimi-code-api-key"
|
||||
| "synthetic-api-key"
|
||||
| "venice-api-key"
|
||||
| "codex-cli"
|
||||
| "apiKey"
|
||||
| "gemini-api-key"
|
||||
@@ -69,7 +68,6 @@ export type OnboardOptions = {
|
||||
zaiApiKey?: string;
|
||||
minimaxApiKey?: string;
|
||||
syntheticApiKey?: string;
|
||||
veniceApiKey?: string;
|
||||
opencodeZenApiKey?: string;
|
||||
gatewayPort?: number;
|
||||
gatewayBind?: GatewayBind;
|
||||
|
||||
@@ -108,8 +108,6 @@ export type DiscordAccountConfig = {
|
||||
groupPolicy?: GroupPolicy;
|
||||
/** Outbound text chunk size (chars). Default: 2000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
/** Disable block streaming for this account. */
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
|
||||
@@ -218,10 +218,4 @@ export type GatewayConfig = {
|
||||
tls?: GatewayTlsConfig;
|
||||
http?: GatewayHttpConfig;
|
||||
nodes?: GatewayNodesConfig;
|
||||
/**
|
||||
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection
|
||||
* arrives from one of these IPs, the Gateway trusts `x-forwarded-for` (or
|
||||
* `x-real-ip`) to determine the client IP for local pairing and HTTP checks.
|
||||
*/
|
||||
trustedProxies?: string[];
|
||||
};
|
||||
@@ -78,8 +78,6 @@ export type GoogleChatAccountConfig = {
|
||||
dms?: Record<string, DmConfig>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
|
||||
@@ -54,8 +54,6 @@ export type IMessageAccountConfig = {
|
||||
mediaMaxMb?: number;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
|
||||
@@ -72,8 +72,6 @@ export type MSTeamsConfig = {
|
||||
groupPolicy?: GroupPolicy;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
|
||||
export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist";
|
||||
export type SignalReactionLevel = "off" | "ack" | "minimal" | "extensive";
|
||||
|
||||
export type SignalAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
@@ -57,8 +56,6 @@ export type SignalAccountConfig = {
|
||||
dms?: Record<string, DmConfig>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
@@ -67,19 +64,6 @@ export type SignalAccountConfig = {
|
||||
reactionNotifications?: SignalReactionNotificationMode;
|
||||
/** Allowlist for reaction notifications when mode is allowlist. */
|
||||
reactionAllowlist?: Array<string | number>;
|
||||
/** Action toggles for message tool capabilities. */
|
||||
actions?: {
|
||||
/** Enable/disable sending reactions via message tool (default: true). */
|
||||
reactions?: boolean;
|
||||
};
|
||||
/**
|
||||
* Controls agent reaction behavior:
|
||||
* - "off": No reactions
|
||||
* - "ack": Only automatic ack reactions (👀 when processing)
|
||||
* - "minimal": Agent can react sparingly (default)
|
||||
* - "extensive": Agent can react liberally
|
||||
*/
|
||||
reactionLevel?: SignalReactionLevel;
|
||||
/** Heartbeat visibility settings for this channel. */
|
||||
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
||||
};
|
||||
|
||||
@@ -116,8 +116,6 @@ export type SlackAccountConfig = {
|
||||
/** Per-DM config overrides keyed by user ID. */
|
||||
dms?: Record<string, DmConfig>;
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
|
||||
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user