Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc09f1a8f6 | ||
|
|
bd10a981b3 |
No files matched your search
@@ -21,4 +21,3 @@ jobs:
|
||||
with:
|
||||
configuration-path: .github/labeler.yml
|
||||
repo-token: ${{ steps.app-token.outputs.token }}
|
||||
sync-labels: true
|
||||
@@ -8,7 +8,6 @@ Status: unreleased.
|
||||
### Changes
|
||||
- Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204.
|
||||
- Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281)
|
||||
- Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk.
|
||||
- Docs: tighten Fly private deployment steps. (#2289) Thanks @dguido.
|
||||
- Docs: add migration guide for moving to a new machine. (#2381)
|
||||
- Docs: add Northflank one-click deployment guide. (#2167) Thanks @AdeboyeDN.
|
||||
@@ -39,7 +38,6 @@ Status: unreleased.
|
||||
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
|
||||
- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon.
|
||||
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
||||
- Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99.
|
||||
- Security: use Windows ACLs for permission audits and fixes on Windows. (#1957)
|
||||
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
||||
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
||||
@@ -55,19 +53,13 @@ Status: unreleased.
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
### Fixes
|
||||
- Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.
|
||||
- TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.
|
||||
- Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
|
||||
- Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez.
|
||||
- CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo.
|
||||
- BlueBubbles: coalesce inbound URL link preview messages. (#1981) Thanks @tyler6204.
|
||||
- Cron: allow payloads containing "heartbeat" in event filter. (#2219) Thanks @dwfinkelstein.
|
||||
- CLI: avoid loading config for global help/version while registering plugin commands. (#2212) Thanks @dial481.
|
||||
- Agents: include memory.md when bootstrapping memory context. (#2318) Thanks @czekaj.
|
||||
- Agents: release session locks on process termination and cover more signals. (#2483) Thanks @janeexai.
|
||||
- Telegram: harden polling + retry behavior for transient network errors and Node 22 transport issues. (#2420) Thanks @techboss.
|
||||
- Telegram: wrap reasoning italics per line to avoid raw underscores. (#2181) Thanks @YuriNachos.
|
||||
- Telegram: log fetch/API errors in delivery to avoid unhandled rejections. (#2492) Thanks @altryne.
|
||||
- Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default.
|
||||
- Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers.
|
||||
- Build: align memory-core peer dependency with lockfile.
|
||||
|
||||
@@ -479,34 +479,34 @@ 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/plum-dawg"><img src="https://avatars.githubusercontent.com/u/5909950?v=4&s=48" width="48" height="48" alt="plum-dawg" title="plum-dawg"/></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/jaydenfyi"><img src="https://avatars.githubusercontent.com/u/213395523?v=4&s=48" width="48" height="48" alt="jaydenfyi" title="jaydenfyi"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/mousberg"><img src="https://avatars.githubusercontent.com/u/57605064?v=4&s=48" width="48" height="48" alt="mousberg" title="mousberg"/></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/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="hirefrank" title="hirefrank"/></a> <a href="https://github.com/joeynyc"><img src="https://avatars.githubusercontent.com/u/17919866?v=4&s=48" width="48" height="48" alt="joeynyc" title="joeynyc"/></a> <a href="https://github.com/orlyjamie"><img src="https://avatars.githubusercontent.com/u/6668807?v=4&s=48" width="48" height="48" alt="orlyjamie" title="orlyjamie"/></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/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/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/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/rohannagpal"><img src="https://avatars.githubusercontent.com/u/4009239?v=4&s=48" width="48" height="48" alt="rohannagpal" title="rohannagpal"/></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/f-trycua"><img src="https://avatars.githubusercontent.com/u/195596869?v=4&s=48" width="48" height="48" alt="f-trycua" title="f-trycua"/></a> <a href="https://github.com/benostein"><img src="https://avatars.githubusercontent.com/u/31802821?v=4&s=48" width="48" height="48" alt="benostein" title="benostein"/></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/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/shakkernerd"><img src="https://avatars.githubusercontent.com/u/165377636?v=4&s=48" width="48" height="48" alt="shakkernerd" title="shakkernerd"/></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/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="dominicnunez" title="dominicnunez"/></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/AdeboyeDN"><img src="https://avatars.githubusercontent.com/u/65312338?v=4&s=48" width="48" height="48" alt="AdeboyeDN" title="AdeboyeDN"/></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/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/rhuanssauro"><img src="https://avatars.githubusercontent.com/u/164682191?v=4&s=48" width="48" height="48" alt="rhuanssauro" title="rhuanssauro"/></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/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/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/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/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/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/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/uos-status"><img src="https://avatars.githubusercontent.com/u/255712580?v=4&s=48" width="48" height="48" alt="uos-status" title="uos-status"/></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/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/JonUleis"><img src="https://avatars.githubusercontent.com/u/7644941?v=4&s=48" width="48" height="48" alt="JonUleis" title="JonUleis"/></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/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/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/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/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/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/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/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/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/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=Joshua%20Mitchell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Joshua Mitchell" title="Joshua Mitchell"/></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/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></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/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></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/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/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/search?q=Pocket%20Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pocket Clawd" title="Pocket Clawd"/></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/Alex-Alaniz"><img src="https://avatars.githubusercontent.com/u/88956822?v=4&s=48" width="48" height="48" alt="Alex-Alaniz" title="Alex-Alaniz"/></a> <a href="https://github.com/alexstyl"><img src="https://avatars.githubusercontent.com/u/1665273?v=4&s=48" width="48" height="48" alt="alexstyl" title="alexstyl"/></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/chenyuan99"><img src="https://avatars.githubusercontent.com/u/25518100?v=4&s=48" width="48" height="48" alt="chenyuan99" title="chenyuan99"/></a> <a href="https://github.com/search?q=Clawdbot%20Maintainers"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawdbot Maintainers" title="Clawdbot Maintainers"/></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/David-Marsh-Photo"><img src="https://avatars.githubusercontent.com/u/228404527?v=4&s=48" width="48" height="48" alt="David-Marsh-Photo" title="David-Marsh-Photo"/></a> <a href="https://github.com/search?q=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></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/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/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></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/kentaro"><img src="https://avatars.githubusercontent.com/u/3458?v=4&s=48" width="48" height="48" alt="kentaro" title="kentaro"/></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/Kiwitwitter"><img src="https://avatars.githubusercontent.com/u/25277769?v=4&s=48" width="48" height="48" alt="Kiwitwitter" title="Kiwitwitter"/></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/louzhixian"><img src="https://avatars.githubusercontent.com/u/7994361?v=4&s=48" width="48" height="48" alt="louzhixian" title="louzhixian"/></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/mertcicekci0"><img src="https://avatars.githubusercontent.com/u/179321902?v=4&s=48" width="48" height="48" alt="mertcicekci0" title="mertcicekci0"/></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/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></a> <a href="https://github.com/ppamment"><img src="https://avatars.githubusercontent.com/u/2122919?v=4&s=48" width="48" height="48" alt="ppamment" title="ppamment"/></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/senoldogann"><img src="https://avatars.githubusercontent.com/u/45736551?v=4&s=48" width="48" height="48" alt="senoldogann" title="senoldogann"/></a> <a href="https://github.com/Seredeep"><img src="https://avatars.githubusercontent.com/u/22802816?v=4&s=48" width="48" height="48" alt="Seredeep" title="Seredeep"/></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/shiyuanhai"><img src="https://avatars.githubusercontent.com/u/1187370?v=4&s=48" width="48" height="48" alt="shiyuanhai" title="shiyuanhai"/></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/Suksham-sharma"><img src="https://avatars.githubusercontent.com/u/94667656?v=4&s=48" width="48" height="48" alt="Suksham-sharma" title="Suksham-sharma"/></a> <a href="https://github.com/search?q=techboss"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="techboss" title="techboss"/></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=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></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/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></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/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></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/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></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/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/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/shakkernerd"><img src="https://avatars.githubusercontent.com/u/165377636?v=4&s=48" width="48" height="48" alt="shakkernerd" title="shakkernerd"/></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/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/AdeboyeDN"><img src="https://avatars.githubusercontent.com/u/65312338?v=4&s=48" width="48" height="48" alt="AdeboyeDN" title="AdeboyeDN"/></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/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/rhuanssauro"><img src="https://avatars.githubusercontent.com/u/164682191?v=4&s=48" width="48" height="48" alt="rhuanssauro" title="rhuanssauro"/></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/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/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/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/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/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/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/uos-status"><img src="https://avatars.githubusercontent.com/u/255712580?v=4&s=48" width="48" height="48" alt="uos-status" title="uos-status"/></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/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/JonUleis"><img src="https://avatars.githubusercontent.com/u/7644941?v=4&s=48" width="48" height="48" alt="JonUleis" title="JonUleis"/></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/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/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/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/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/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/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/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/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/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=Joshua%20Mitchell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Joshua Mitchell" title="Joshua Mitchell"/></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/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></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/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></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/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/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/search?q=Pocket%20Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pocket Clawd" title="Pocket Clawd"/></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/Alex-Alaniz"><img src="https://avatars.githubusercontent.com/u/88956822?v=4&s=48" width="48" height="48" alt="Alex-Alaniz" title="Alex-Alaniz"/></a> <a href="https://github.com/alexstyl"><img src="https://avatars.githubusercontent.com/u/1665273?v=4&s=48" width="48" height="48" alt="alexstyl" title="alexstyl"/></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/search?q=Clawdbot%20Maintainers"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawdbot Maintainers" title="Clawdbot Maintainers"/></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=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></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/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/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></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/kentaro"><img src="https://avatars.githubusercontent.com/u/3458?v=4&s=48" width="48" height="48" alt="kentaro" title="kentaro"/></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/louzhixian"><img src="https://avatars.githubusercontent.com/u/7994361?v=4&s=48" width="48" height="48" alt="louzhixian" title="louzhixian"/></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/mertcicekci0"><img src="https://avatars.githubusercontent.com/u/179321902?v=4&s=48" width="48" height="48" alt="mertcicekci0" title="mertcicekci0"/></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/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></a> <a href="https://github.com/ppamment"><img src="https://avatars.githubusercontent.com/u/2122919?v=4&s=48" width="48" height="48" alt="ppamment" title="ppamment"/></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/senoldogann"><img src="https://avatars.githubusercontent.com/u/45736551?v=4&s=48" width="48" height="48" alt="senoldogann" title="senoldogann"/></a> <a href="https://github.com/Seredeep"><img src="https://avatars.githubusercontent.com/u/22802816?v=4&s=48" width="48" height="48" alt="Seredeep" title="Seredeep"/></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/shiyuanhai"><img src="https://avatars.githubusercontent.com/u/1187370?v=4&s=48" width="48" height="48" alt="shiyuanhai" title="shiyuanhai"/></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/Suksham-sharma"><img src="https://avatars.githubusercontent.com/u/94667656?v=4&s=48" width="48" height="48" alt="Suksham-sharma" title="Suksham-sharma"/></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=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></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/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></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/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></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/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></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>
|
||||
@@ -115,9 +115,6 @@ body::after {
|
||||
}
|
||||
|
||||
.shell {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding: 22px 16px 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ on any homeserver, so you need a Matrix account for the bot. Once it is logged i
|
||||
the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too,
|
||||
but it requires E2EE to be enabled.
|
||||
|
||||
Status: supported via plugin (@vector-im/matrix-bot-sdk). Direct messages, rooms, threads, media, reactions,
|
||||
Status: supported via plugin (matrix-bot-sdk). Direct messages, rooms, threads, media, reactions,
|
||||
polls (send + poll-start as text), location, and E2EE (with crypto support).
|
||||
|
||||
## Plugin required
|
||||
|
||||
@@ -345,6 +345,10 @@
|
||||
"source": "/auth-monitoring",
|
||||
"destination": "/automation/auth-monitoring"
|
||||
},
|
||||
{
|
||||
"source": "/scripts",
|
||||
"destination": "/scripts"
|
||||
},
|
||||
{
|
||||
"source": "/camera",
|
||||
"destination": "/nodes/camera"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
title: "Node.js + npm (PATH sanity)"
|
||||
summary: "Node.js + npm install sanity: versions, PATH, and global installs"
|
||||
read_when:
|
||||
- "You installed Clawdbot but `clawdbot` is “command not found”"
|
||||
- "You’re setting up Node.js/npm on a new machine"
|
||||
- "npm install -g ... fails with permissions or PATH issues"
|
||||
- You installed Clawdbot but `clawdbot` is “command not found”
|
||||
- You’re setting up Node.js/npm on a new machine
|
||||
- `npm install -g ...` fails with permissions or PATH issues
|
||||
---
|
||||
|
||||
# Node.js + npm (PATH sanity)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"dependencies": {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||
"markdown-it": "14.1.0",
|
||||
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
|
||||
"matrix-bot-sdk": "0.8.0",
|
||||
"music-metadata": "^11.10.6",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ export async function readMatrixMessages(
|
||||
: 20;
|
||||
const token = opts.before?.trim() || opts.after?.trim() || undefined;
|
||||
const dir = opts.after ? "f" : "b";
|
||||
// @vector-im/matrix-bot-sdk uses doRequest for room messages
|
||||
// matrix-bot-sdk uses doRequest for room messages
|
||||
const res = await client.doRequest(
|
||||
"GET",
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(resolvedRoom)}/messages`,
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function listMatrixReactions(
|
||||
typeof opts.limit === "number" && Number.isFinite(opts.limit)
|
||||
? Math.max(1, Math.floor(opts.limit))
|
||||
: 100;
|
||||
// @vector-im/matrix-bot-sdk uses doRequest for relations
|
||||
// matrix-bot-sdk uses doRequest for relations
|
||||
const res = await client.doRequest(
|
||||
"GET",
|
||||
`/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`,
|
||||
|
||||
@@ -9,9 +9,9 @@ export async function getMatrixMemberInfo(
|
||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
||||
try {
|
||||
const roomId = opts.roomId ? await resolveMatrixRoomId(client, opts.roomId) : undefined;
|
||||
// @vector-im/matrix-bot-sdk uses getUserProfile
|
||||
// matrix-bot-sdk uses getUserProfile
|
||||
const profile = await client.getUserProfile(userId);
|
||||
// Note: @vector-im/matrix-bot-sdk doesn't have getRoom().getMember() like matrix-js-sdk
|
||||
// Note: matrix-bot-sdk doesn't have getRoom().getMember() like matrix-js-sdk
|
||||
// We'd need to fetch room state separately if needed
|
||||
return {
|
||||
userId,
|
||||
@@ -36,7 +36,7 @@ export async function getMatrixRoomInfo(
|
||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
||||
try {
|
||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||
// @vector-im/matrix-bot-sdk uses getRoomState for state events
|
||||
// matrix-bot-sdk uses getRoomState for state events
|
||||
let name: string | null = null;
|
||||
let topic: string | null = null;
|
||||
let canonicalAlias: string | null = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import {
|
||||
EventType,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
export const MsgType = {
|
||||
Text: "m.text",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
let activeClient: MatrixClient | null = null;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
MatrixClient,
|
||||
SimpleFsStorageProvider,
|
||||
RustSdkCryptoStorageProvider,
|
||||
} from "@vector-im/matrix-bot-sdk";
|
||||
import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk";
|
||||
} from "matrix-bot-sdk";
|
||||
import type { IStorageProvider, ICryptoStorageProvider } from "matrix-bot-sdk";
|
||||
|
||||
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConsoleLogger, LogService } from "@vector-im/matrix-bot-sdk";
|
||||
import { ConsoleLogger, LogService } from "matrix-bot-sdk";
|
||||
|
||||
let matrixSdkLoggingConfigured = false;
|
||||
const matrixSdkBaseLogger = new ConsoleLogger();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LogService } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import { LogService } from "matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import { createMatrixClient } from "./create-client.js";
|
||||
@@ -157,7 +157,7 @@ export async function waitForMatrixSync(_params: {
|
||||
timeoutMs?: number;
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
// @vector-im/matrix-bot-sdk handles sync internally in start()
|
||||
// matrix-bot-sdk handles sync internally in start()
|
||||
// This is kept for API compatibility but is essentially a no-op now
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
||||
import type { RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
|
||||
const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk";
|
||||
const MATRIX_SDK_PACKAGE = "matrix-bot-sdk";
|
||||
|
||||
export function isMatrixSdkAvailable(): boolean {
|
||||
try {
|
||||
@@ -30,9 +30,9 @@ export async function ensureMatrixSdkInstalled(params: {
|
||||
if (isMatrixSdkAvailable()) return;
|
||||
const confirm = params.confirm;
|
||||
if (confirm) {
|
||||
const ok = await confirm("Matrix requires @vector-im/matrix-bot-sdk. Install now?");
|
||||
const ok = await confirm("Matrix requires matrix-bot-sdk. Install now?");
|
||||
if (!ok) {
|
||||
throw new Error("Matrix requires @vector-im/matrix-bot-sdk (install dependencies first).");
|
||||
throw new Error("Matrix requires matrix-bot-sdk (install dependencies first).");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,6 @@ export async function ensureMatrixSdkInstalled(params: {
|
||||
);
|
||||
}
|
||||
if (!isMatrixSdkAvailable()) {
|
||||
throw new Error("Matrix dependency install completed but @vector-im/matrix-bot-sdk is still missing.");
|
||||
throw new Error("Matrix dependency install completed but matrix-bot-sdk is still missing.");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import { AutojoinRoomsMixin } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
import { AutojoinRoomsMixin } from "matrix-bot-sdk";
|
||||
|
||||
import type { RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
type DirectMessageCheck = {
|
||||
roomId: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
|
||||
import type { MatrixAuth } from "../client.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { LocationMessageEventContent, MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import {
|
||||
createReplyPrefixContext,
|
||||
@@ -110,7 +110,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
try {
|
||||
const eventType = event.type;
|
||||
if (eventType === EventType.RoomMessageEncrypted) {
|
||||
// Encrypted messages are decrypted automatically by @vector-im/matrix-bot-sdk with crypto enabled
|
||||
// Encrypted messages are decrypted automatically by matrix-bot-sdk with crypto enabled
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
threadReplies,
|
||||
messageId,
|
||||
threadRootId,
|
||||
isThreadRoot: false, // @vector-im/matrix-bot-sdk doesn't have this info readily available
|
||||
isThreadRoot: false, // matrix-bot-sdk doesn't have this info readily available
|
||||
});
|
||||
|
||||
const route = core.channel.routing.resolveAgentRoute({
|
||||
|
||||
@@ -244,7 +244,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
});
|
||||
logVerboseMessage("matrix: client started");
|
||||
|
||||
// @vector-im/matrix-bot-sdk client is already started via resolveSharedMatrixClient
|
||||
// matrix-bot-sdk client is already started via resolveSharedMatrixClient
|
||||
logger.info(`matrix: logged in as ${auth.userId}`);
|
||||
|
||||
// If E2EE is enabled, trigger device verification
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LocationMessageEventContent } from "@vector-im/matrix-bot-sdk";
|
||||
import type { LocationMessageEventContent } from "matrix-bot-sdk";
|
||||
|
||||
import {
|
||||
formatLocationText,
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("downloadMatrixMedia", () => {
|
||||
const client = {
|
||||
crypto: { decryptMedia },
|
||||
mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
|
||||
} as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;
|
||||
} as unknown as import("matrix-bot-sdk").MatrixClient;
|
||||
|
||||
const file = {
|
||||
url: "mxc://example/file",
|
||||
@@ -70,7 +70,7 @@ describe("downloadMatrixMedia", () => {
|
||||
const client = {
|
||||
crypto: { decryptMedia },
|
||||
mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
|
||||
} as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;
|
||||
} as unknown as import("matrix-bot-sdk").MatrixClient;
|
||||
|
||||
const file = {
|
||||
url: "mxc://example/file",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
@@ -22,7 +22,7 @@ async function fetchMatrixMediaBuffer(params: {
|
||||
mxcUrl: string;
|
||||
maxBytes: number;
|
||||
}): Promise<{ buffer: Buffer; headerType?: string } | null> {
|
||||
// @vector-im/matrix-bot-sdk provides mxcToHttp helper
|
||||
// matrix-bot-sdk provides mxcToHttp helper
|
||||
const url = params.client.mxcToHttp(params.mxcUrl);
|
||||
if (!url) return null;
|
||||
|
||||
@@ -40,7 +40,7 @@ async function fetchMatrixMediaBuffer(params: {
|
||||
|
||||
/**
|
||||
* Download and decrypt encrypted media from a Matrix room.
|
||||
* Uses @vector-im/matrix-bot-sdk's decryptMedia which handles both download and decryption.
|
||||
* Uses matrix-bot-sdk's decryptMedia which handles both download and decryption.
|
||||
*/
|
||||
async function fetchEncryptedMediaBuffer(params: {
|
||||
client: MatrixClient;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import { sendMessageMatrix } from "../send.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
export type MatrixRoomInfo = {
|
||||
name?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Type for raw Matrix event from @vector-im/matrix-bot-sdk
|
||||
// Type for raw Matrix event from matrix-bot-sdk
|
||||
type MatrixRawEvent = {
|
||||
event_id: string;
|
||||
sender: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EncryptedFile, MessageEventContent } from "@vector-im/matrix-bot-sdk";
|
||||
import type { EncryptedFile, MessageEventContent } from "matrix-bot-sdk";
|
||||
|
||||
export const EventType = {
|
||||
RoomMessage: "m.room.message",
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function probeMatrix(params: {
|
||||
accessToken: params.accessToken,
|
||||
localTimeoutMs: params.timeoutMs,
|
||||
});
|
||||
// @vector-im/matrix-bot-sdk uses getUserId() which calls whoami internally
|
||||
// matrix-bot-sdk uses getUserId() which calls whoami internally
|
||||
const userId = await client.getUserId();
|
||||
result.ok = true;
|
||||
result.userId = userId ?? null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
import { setMatrixRuntime } from "../runtime.js";
|
||||
|
||||
vi.mock("@vector-im/matrix-bot-sdk", () => ({
|
||||
vi.mock("matrix-bot-sdk", () => ({
|
||||
ConsoleLogger: class {
|
||||
trace = vi.fn();
|
||||
debug = vi.fn();
|
||||
@@ -60,7 +60,7 @@ const makeClient = () => {
|
||||
sendMessage,
|
||||
uploadContent,
|
||||
getUserId: vi.fn().mockResolvedValue("@bot:example.org"),
|
||||
} as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;
|
||||
} as unknown as import("matrix-bot-sdk").MatrixClient;
|
||||
return { client, sendMessage, uploadContent };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import type { PollInput } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
@@ -72,7 +72,7 @@ export async function sendMessageMatrix(
|
||||
? buildThreadRelation(threadId, opts.replyToId)
|
||||
: buildReplyRelation(opts.replyToId);
|
||||
const sendContent = async (content: MatrixOutboundContent) => {
|
||||
// @vector-im/matrix-bot-sdk uses sendMessage differently
|
||||
// matrix-bot-sdk uses sendMessage differently
|
||||
const eventId = await client.sendMessage(roomId, content);
|
||||
return eventId;
|
||||
};
|
||||
@@ -172,7 +172,7 @@ export async function sendPollMatrix(
|
||||
const pollPayload = threadId
|
||||
? { ...pollContent, "m.relates_to": buildThreadRelation(threadId) }
|
||||
: pollContent;
|
||||
// @vector-im/matrix-bot-sdk sendEvent returns eventId string directly
|
||||
// matrix-bot-sdk sendEvent returns eventId string directly
|
||||
const eventId = await client.sendEvent(roomId, M_POLL_START, pollPayload);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import { getActiveMatrixClient } from "../active-client.js";
|
||||
@@ -57,7 +57,7 @@ export async function resolveMatrixClient(opts: {
|
||||
// Ignore crypto prep failures for one-off sends; normal sync will retry.
|
||||
}
|
||||
}
|
||||
// @vector-im/matrix-bot-sdk uses start() instead of startClient()
|
||||
// matrix-bot-sdk uses start() instead of startClient()
|
||||
await client.start();
|
||||
return { client, stopOnDone: true };
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
MatrixClient,
|
||||
TimedFileInfo,
|
||||
VideoFileInfo,
|
||||
} from "@vector-im/matrix-bot-sdk";
|
||||
} from "matrix-bot-sdk";
|
||||
import { parseBuffer, type IFileInfo } from "music-metadata";
|
||||
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
import { EventType } from "./types.js";
|
||||
|
||||
let resolveMatrixRoomId: typeof import("./targets.js").resolveMatrixRoomId;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import { EventType, type MatrixDirectAccountData } from "./types.js";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
TextualMessageEventContent,
|
||||
TimedFileInfo,
|
||||
VideoFileInfo,
|
||||
} from "@vector-im/matrix-bot-sdk";
|
||||
} from "matrix-bot-sdk";
|
||||
|
||||
// Message types
|
||||
export const MsgType = {
|
||||
@@ -85,7 +85,7 @@ export type MatrixSendResult = {
|
||||
};
|
||||
|
||||
export type MatrixSendOpts = {
|
||||
client?: import("@vector-im/matrix-bot-sdk").MatrixClient;
|
||||
client?: import("matrix-bot-sdk").MatrixClient;
|
||||
mediaUrl?: string;
|
||||
accountId?: string;
|
||||
replyToId?: string;
|
||||
|
||||
@@ -185,7 +185,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
`Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`,
|
||||
],
|
||||
selectionHint: !sdkReady
|
||||
? "install @vector-im/matrix-bot-sdk"
|
||||
? "install matrix-bot-sdk"
|
||||
: configured
|
||||
? "configured"
|
||||
: "needs auth",
|
||||
|
||||
@@ -53,7 +53,7 @@ export type MatrixConfig = {
|
||||
password?: string;
|
||||
/** Optional device name when logging in via password. */
|
||||
deviceName?: string;
|
||||
/** Initial sync limit for startup (default: @vector-im/matrix-bot-sdk default). */
|
||||
/** Initial sync limit for startup (default: matrix-bot-sdk default). */
|
||||
initialSyncLimit?: number;
|
||||
/** Enable end-to-end encryption (E2EE). Default: false. */
|
||||
encryption?: boolean;
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
]
|
||||
},
|
||||
"peerDependencies": {
|
||||
"clawdbot": ">=2026.1.24-3"
|
||||
"clawdbot": ">=2026.1.25"
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
}) {
|
||||
const core = getMSTeamsRuntime();
|
||||
const sendTypingIndicator = async () => {
|
||||
await params.context.sendActivity({ type: "typing" });
|
||||
await params.context.sendActivities([{ type: "typing" }]);
|
||||
};
|
||||
const typingCallbacks = createTypingCallbacks({
|
||||
start: sendTypingIndicator,
|
||||
@@ -70,38 +70,38 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: params.cfg,
|
||||
channel: "msteams",
|
||||
});
|
||||
const messages = renderReplyPayloadsToMessages([payload], {
|
||||
textChunkLimit: params.textLimit,
|
||||
chunkText: true,
|
||||
mediaMode: "split",
|
||||
tableMode,
|
||||
chunkMode,
|
||||
});
|
||||
const mediaMaxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb,
|
||||
});
|
||||
const ids = await sendMSTeamsMessages({
|
||||
replyStyle: params.replyStyle,
|
||||
adapter: params.adapter,
|
||||
appId: params.appId,
|
||||
conversationRef: params.conversationRef,
|
||||
context: params.context,
|
||||
messages,
|
||||
// Enable default retry/backoff for throttling/transient failures.
|
||||
retry: {},
|
||||
onRetry: (event) => {
|
||||
params.log.debug("retrying send", {
|
||||
replyStyle: params.replyStyle,
|
||||
...event,
|
||||
});
|
||||
},
|
||||
tokenProvider: params.tokenProvider,
|
||||
sharePointSiteId: params.sharePointSiteId,
|
||||
mediaMaxBytes,
|
||||
});
|
||||
if (ids.length > 0) params.onSentMessageIds?.(ids);
|
||||
});
|
||||
const messages = renderReplyPayloadsToMessages([payload], {
|
||||
textChunkLimit: params.textLimit,
|
||||
chunkText: true,
|
||||
mediaMode: "split",
|
||||
tableMode,
|
||||
chunkMode,
|
||||
});
|
||||
const mediaMaxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb,
|
||||
});
|
||||
const ids = await sendMSTeamsMessages({
|
||||
replyStyle: params.replyStyle,
|
||||
adapter: params.adapter,
|
||||
appId: params.appId,
|
||||
conversationRef: params.conversationRef,
|
||||
context: params.context,
|
||||
messages,
|
||||
// Enable default retry/backoff for throttling/transient failures.
|
||||
retry: {},
|
||||
onRetry: (event) => {
|
||||
params.log.debug("retrying send", {
|
||||
replyStyle: params.replyStyle,
|
||||
...event,
|
||||
});
|
||||
},
|
||||
tokenProvider: params.tokenProvider,
|
||||
sharePointSiteId: params.sharePointSiteId,
|
||||
mediaMaxBytes,
|
||||
});
|
||||
if (ids.length > 0) params.onSentMessageIds?.(ids);
|
||||
},
|
||||
onError: (err, info) => {
|
||||
const errMsg = formatUnknownError(err);
|
||||
|
||||
Generated
+39
-157
@@ -172,6 +172,13 @@ importers:
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas':
|
||||
specifier: ^0.1.88
|
||||
version: 0.1.88
|
||||
node-llama-cpp:
|
||||
specifier: 3.15.0
|
||||
version: 3.15.0(typescript@5.9.3)
|
||||
devDependencies:
|
||||
'@grammyjs/types':
|
||||
specifier: ^3.23.0
|
||||
@@ -254,13 +261,6 @@ importers:
|
||||
wireit:
|
||||
specifier: ^0.14.12
|
||||
version: 0.14.12
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas':
|
||||
specifier: ^0.1.88
|
||||
version: 0.1.88
|
||||
node-llama-cpp:
|
||||
specifier: 3.15.0
|
||||
version: 3.15.0(typescript@5.9.3)
|
||||
|
||||
extensions/bluebubbles: {}
|
||||
|
||||
@@ -335,12 +335,12 @@ importers:
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
'@vector-im/matrix-bot-sdk':
|
||||
specifier: 0.8.0-element.3
|
||||
version: 0.8.0-element.3
|
||||
markdown-it:
|
||||
specifier: 14.1.0
|
||||
version: 14.1.0
|
||||
matrix-bot-sdk:
|
||||
specifier: 0.8.0
|
||||
version: 0.8.0
|
||||
music-metadata:
|
||||
specifier: ^11.10.6
|
||||
version: 11.10.6
|
||||
@@ -357,8 +357,8 @@ importers:
|
||||
extensions/memory-core:
|
||||
dependencies:
|
||||
clawdbot:
|
||||
specifier: '>=2026.1.24-3'
|
||||
version: 2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3)
|
||||
specifier: '>=2026.1.25'
|
||||
version: link:../..
|
||||
|
||||
extensions/memory-lancedb:
|
||||
dependencies:
|
||||
@@ -1316,7 +1316,6 @@ packages:
|
||||
'@lancedb/lancedb@0.23.0':
|
||||
resolution: {integrity: sha512-aYrIoEG24AC+wILCL57Ius/Y4yU+xFHDPKLvmjzzN4byAjzeIGF0TC86S5RBt4Ji+dxS7yIWV5Q/gE5/fybIFQ==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [x64, arm64]
|
||||
os: [darwin, linux, win32]
|
||||
peerDependencies:
|
||||
apache-arrow: '>=15.0.0 <=18.1.0'
|
||||
@@ -2668,9 +2667,6 @@ packages:
|
||||
'@types/bun@1.3.6':
|
||||
resolution: {integrity: sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA==}
|
||||
|
||||
'@types/caseless@0.12.5':
|
||||
resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
@@ -2752,9 +2748,6 @@ packages:
|
||||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/request@2.48.13':
|
||||
resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==}
|
||||
|
||||
'@types/retry@0.12.0':
|
||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||
|
||||
@@ -2773,9 +2766,6 @@ packages:
|
||||
'@types/serve-static@2.2.0':
|
||||
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
||||
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
@@ -2832,10 +2822,6 @@ packages:
|
||||
'@urbit/http-api@3.0.0':
|
||||
resolution: {integrity: sha512-EmyPbWHWXhfYQ/9wWFcLT53VvCn8ct9ljd6QEe+UBjNPEhUPOFBLpDsDp3iPLQgg8ykSU8JMMHxp95LHCorExA==}
|
||||
|
||||
'@vector-im/matrix-bot-sdk@0.8.0-element.3':
|
||||
resolution: {integrity: sha512-2FFo/Kz2vTnOZDv59Q0s803LHf7KzuQ2EwOYYAtO0zUKJ8pV5CPsVC/IHyFb+Fsxl3R9XWFiX529yhslb4v9cQ==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
'@vitest/browser-playwright@4.0.18':
|
||||
resolution: {integrity: sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==}
|
||||
peerDependencies:
|
||||
@@ -3208,11 +3194,6 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
clawdbot@2026.1.24-3:
|
||||
resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3630,10 +3611,6 @@ packages:
|
||||
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
|
||||
engines: {node: '>= 0.12'}
|
||||
|
||||
form-data@2.5.5:
|
||||
resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==}
|
||||
engines: {node: '>= 0.12'}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -4258,6 +4235,10 @@ packages:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
matrix-bot-sdk@0.8.0:
|
||||
resolution: {integrity: sha512-sCY5UvZfsZhJdCjSc8wZhGhIHOe5cSFSILxx9Zp5a/NEXtmQ6W/bIhefIk4zFAZXetFwXsgvKh1960k1hG5WDw==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
@@ -8438,8 +8419,6 @@ snapshots:
|
||||
bun-types: 1.3.6
|
||||
optional: true
|
||||
|
||||
'@types/caseless@0.12.5': {}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
@@ -8532,13 +8511,6 @@ snapshots:
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/request@2.48.13':
|
||||
dependencies:
|
||||
'@types/caseless': 0.12.5
|
||||
'@types/node': 25.0.10
|
||||
'@types/tough-cookie': 4.0.5
|
||||
form-data: 2.5.5
|
||||
|
||||
'@types/retry@0.12.0': {}
|
||||
|
||||
'@types/retry@0.12.5': {}
|
||||
@@ -8563,8 +8535,6 @@ snapshots:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 25.0.10
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
@@ -8618,30 +8588,6 @@ snapshots:
|
||||
browser-or-node: 1.3.0
|
||||
core-js: 3.48.0
|
||||
|
||||
'@vector-im/matrix-bot-sdk@0.8.0-element.3':
|
||||
dependencies:
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0
|
||||
'@types/express': 4.17.25
|
||||
'@types/request': 2.48.13
|
||||
another-json: 0.2.0
|
||||
async-lock: 1.4.1
|
||||
chalk: 4.1.2
|
||||
express: 4.22.1
|
||||
glob-to-regexp: 0.4.1
|
||||
hash.js: 1.1.7
|
||||
html-to-text: 9.0.5
|
||||
htmlencode: 0.0.4
|
||||
lowdb: 1.0.0
|
||||
lru-cache: 10.4.3
|
||||
mkdirp: 3.0.1
|
||||
morgan: 1.10.1
|
||||
postgres: 3.4.8
|
||||
request: 2.88.2
|
||||
request-promise: 4.2.6(request@2.88.2)
|
||||
sanitize-html: 2.17.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/browser-playwright@4.0.18(playwright@1.58.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18)
|
||||
@@ -9092,84 +9038,6 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock': 3.975.0
|
||||
'@buape/carbon': 0.14.0(hono@4.11.4)
|
||||
'@clack/prompts': 0.11.0
|
||||
'@grammyjs/runner': 2.0.3(grammy@1.39.3)
|
||||
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3)
|
||||
'@homebridge/ciao': 1.3.4
|
||||
'@line/bot-sdk': 10.6.0
|
||||
'@lydell/node-pty': 1.2.0-beta.3
|
||||
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-tui': 0.49.3
|
||||
'@mozilla/readability': 0.6.0
|
||||
'@sinclair/typebox': 0.34.47
|
||||
'@slack/bolt': 4.6.0(@types/express@5.0.6)
|
||||
'@slack/web-api': 7.13.0
|
||||
'@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
|
||||
ajv: 8.17.1
|
||||
body-parser: 2.2.2
|
||||
chalk: 5.6.2
|
||||
chokidar: 5.0.0
|
||||
chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482)
|
||||
cli-highlight: 2.1.11
|
||||
commander: 14.0.2
|
||||
croner: 9.1.0
|
||||
detect-libc: 2.1.2
|
||||
discord-api-types: 0.38.37
|
||||
dotenv: 17.2.3
|
||||
express: 5.2.1
|
||||
file-type: 21.3.0
|
||||
grammy: 1.39.3
|
||||
hono: 4.11.4
|
||||
jiti: 2.6.1
|
||||
json5: 2.2.3
|
||||
jszip: 3.10.1
|
||||
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
|
||||
proper-lockfile: 4.1.2
|
||||
qrcode-terminal: 0.12.0
|
||||
sharp: 0.34.5
|
||||
sqlite-vec: 0.1.7-alpha.2
|
||||
tar: 7.5.4
|
||||
tslog: 4.10.2
|
||||
undici: 7.19.0
|
||||
ws: 8.19.0
|
||||
yaml: 2.8.2
|
||||
zod: 4.3.6
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.88
|
||||
node-llama-cpp: 3.15.0(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@discordjs/opus'
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- '@types/express'
|
||||
- audio-decode
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- canvas
|
||||
- debug
|
||||
- devtools-protocol
|
||||
- encoding
|
||||
- ffmpeg-static
|
||||
- jimp
|
||||
- link-preview-js
|
||||
- node-opus
|
||||
- opusscript
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
dependencies:
|
||||
restore-cursor: 5.1.0
|
||||
@@ -9650,15 +9518,6 @@ snapshots:
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@2.5.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
@@ -10338,6 +10197,29 @@ snapshots:
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
matrix-bot-sdk@0.8.0:
|
||||
dependencies:
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0
|
||||
'@types/express': 4.17.25
|
||||
another-json: 0.2.0
|
||||
async-lock: 1.4.1
|
||||
chalk: 4.1.2
|
||||
express: 4.22.1
|
||||
glob-to-regexp: 0.4.1
|
||||
hash.js: 1.1.7
|
||||
html-to-text: 9.0.5
|
||||
htmlencode: 0.0.4
|
||||
lowdb: 1.0.0
|
||||
lru-cache: 10.4.3
|
||||
mkdirp: 3.0.1
|
||||
morgan: 1.10.1
|
||||
postgres: 3.4.8
|
||||
request: 2.88.2
|
||||
request-promise: 4.2.6(request@2.88.2)
|
||||
sanitize-html: 2.17.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdurl@2.0.0: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveApiKeyForProfile } from "./oauth.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
|
||||
describe("resolveApiKeyForProfile", () => {
|
||||
let tmpDir: string;
|
||||
let mainAgentDir: string;
|
||||
let secondaryAgentDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "oauth-test-"));
|
||||
mainAgentDir = path.join(tmpDir, "agents", "main", "agent");
|
||||
secondaryAgentDir = path.join(tmpDir, "agents", "kids", "agent");
|
||||
await fs.promises.mkdir(mainAgentDir, { recursive: true });
|
||||
await fs.promises.mkdir(secondaryAgentDir, { recursive: true });
|
||||
|
||||
// Set env to use our temp dir
|
||||
process.env.CLAWDBOT_STATE_DIR = tmpDir;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
delete process.env.CLAWDBOT_STATE_DIR;
|
||||
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("falls back to main agent credentials when secondary agent token is expired and refresh fails", async () => {
|
||||
const profileId = "anthropic:claude-cli";
|
||||
const now = Date.now();
|
||||
const expiredTime = now - 60 * 60 * 1000; // 1 hour ago
|
||||
const freshTime = now + 60 * 60 * 1000; // 1 hour from now
|
||||
|
||||
// Write expired credentials for secondary agent
|
||||
const secondaryStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "expired-access-token",
|
||||
refresh: "expired-refresh-token",
|
||||
expires: expiredTime,
|
||||
},
|
||||
},
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(secondaryAgentDir, "auth-profiles.json"),
|
||||
JSON.stringify(secondaryStore),
|
||||
);
|
||||
|
||||
// Write fresh credentials for main agent
|
||||
const mainStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "fresh-access-token",
|
||||
refresh: "fresh-refresh-token",
|
||||
expires: freshTime,
|
||||
},
|
||||
},
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(mainAgentDir, "auth-profiles.json"),
|
||||
JSON.stringify(mainStore),
|
||||
);
|
||||
|
||||
// The secondary agent should fall back to main agent's credentials
|
||||
// when its own token refresh fails
|
||||
const result = await resolveApiKeyForProfile({
|
||||
store: secondaryStore,
|
||||
profileId,
|
||||
agentDir: secondaryAgentDir,
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.apiKey).toBe("fresh-access-token");
|
||||
expect(result?.provider).toBe("anthropic");
|
||||
|
||||
// Verify the credentials were copied to the secondary agent
|
||||
const updatedSecondaryStore = JSON.parse(
|
||||
await fs.promises.readFile(path.join(secondaryAgentDir, "auth-profiles.json"), "utf8"),
|
||||
) as AuthProfileStore;
|
||||
expect(updatedSecondaryStore.profiles[profileId]).toMatchObject({
|
||||
access: "fresh-access-token",
|
||||
expires: freshTime,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import lockfile from "proper-lockfile";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { refreshChutesTokens } from "../chutes-oauth.js";
|
||||
import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS } from "./constants.js";
|
||||
import { formatAuthDoctorHint } from "./doctor.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
||||
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
||||
@@ -196,32 +196,6 @@ export async function resolveApiKeyForProfile(params: {
|
||||
// keep original error
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if this is a secondary agent, try using the main agent's credentials
|
||||
if (params.agentDir) {
|
||||
try {
|
||||
const mainStore = ensureAuthProfileStore(undefined); // main agent (no agentDir)
|
||||
const mainCred = mainStore.profiles[profileId];
|
||||
if (mainCred?.type === "oauth" && Date.now() < mainCred.expires) {
|
||||
// Main agent has fresh credentials - copy them to this agent and use them
|
||||
refreshedStore.profiles[profileId] = { ...mainCred };
|
||||
saveAuthProfileStore(refreshedStore, params.agentDir);
|
||||
log.info("inherited fresh OAuth credentials from main agent", {
|
||||
profileId,
|
||||
agentDir: params.agentDir,
|
||||
expires: new Date(mainCred.expires).toISOString(),
|
||||
});
|
||||
return {
|
||||
apiKey: buildOAuthApiKey(mainCred.provider, mainCred),
|
||||
provider: mainCred.provider,
|
||||
email: mainCred.email,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// keep original error if main agent fallback also fails
|
||||
}
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const hint = formatAuthDoctorHint({
|
||||
cfg,
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { __testing, acquireSessionWriteLock } from "./session-write-lock.js";
|
||||
import { acquireSessionWriteLock } from "./session-write-lock.js";
|
||||
|
||||
describe("acquireSessionWriteLock", () => {
|
||||
it("reuses locks across symlinked session paths", async () => {
|
||||
@@ -31,132 +31,4 @@ describe("acquireSessionWriteLock", () => {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the lock file until the last release", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-"));
|
||||
try {
|
||||
const sessionFile = path.join(root, "sessions.json");
|
||||
const lockPath = `${sessionFile}.lock`;
|
||||
|
||||
const lockA = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 });
|
||||
const lockB = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 });
|
||||
|
||||
await expect(fs.access(lockPath)).resolves.toBeUndefined();
|
||||
await lockA.release();
|
||||
await expect(fs.access(lockPath)).resolves.toBeUndefined();
|
||||
await lockB.release();
|
||||
await expect(fs.access(lockPath)).rejects.toThrow();
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reclaims stale lock files", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-"));
|
||||
try {
|
||||
const sessionFile = path.join(root, "sessions.json");
|
||||
const lockPath = `${sessionFile}.lock`;
|
||||
await fs.writeFile(
|
||||
lockPath,
|
||||
JSON.stringify({ pid: 123456, createdAt: new Date(Date.now() - 60_000).toISOString() }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500, staleMs: 10 });
|
||||
const raw = await fs.readFile(lockPath, "utf8");
|
||||
const payload = JSON.parse(raw) as { pid: number };
|
||||
|
||||
expect(payload.pid).toBe(process.pid);
|
||||
await lock.release();
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("removes held locks on termination signals", async () => {
|
||||
const signals = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"] as const;
|
||||
for (const signal of signals) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-cleanup-"));
|
||||
try {
|
||||
const sessionFile = path.join(root, "sessions.json");
|
||||
const lockPath = `${sessionFile}.lock`;
|
||||
await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 });
|
||||
const keepAlive = () => {};
|
||||
if (signal === "SIGINT") {
|
||||
process.on(signal, keepAlive);
|
||||
}
|
||||
|
||||
__testing.handleTerminationSignal(signal);
|
||||
|
||||
await expect(fs.stat(lockPath)).rejects.toThrow();
|
||||
if (signal === "SIGINT") {
|
||||
process.off(signal, keepAlive);
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("registers cleanup for SIGQUIT and SIGABRT", () => {
|
||||
expect(__testing.cleanupSignals).toContain("SIGQUIT");
|
||||
expect(__testing.cleanupSignals).toContain("SIGABRT");
|
||||
});
|
||||
it("cleans up locks on SIGINT without removing other handlers", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-"));
|
||||
const originalKill = process.kill.bind(process);
|
||||
const killCalls: Array<NodeJS.Signals | undefined> = [];
|
||||
let otherHandlerCalled = false;
|
||||
|
||||
process.kill = ((pid: number, signal?: NodeJS.Signals) => {
|
||||
killCalls.push(signal);
|
||||
return true;
|
||||
}) as typeof process.kill;
|
||||
|
||||
const otherHandler = () => {
|
||||
otherHandlerCalled = true;
|
||||
};
|
||||
|
||||
process.on("SIGINT", otherHandler);
|
||||
|
||||
try {
|
||||
const sessionFile = path.join(root, "sessions.json");
|
||||
const lockPath = `${sessionFile}.lock`;
|
||||
await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 });
|
||||
|
||||
process.emit("SIGINT");
|
||||
|
||||
await expect(fs.access(lockPath)).rejects.toThrow();
|
||||
expect(otherHandlerCalled).toBe(true);
|
||||
expect(killCalls).toEqual([]);
|
||||
} finally {
|
||||
process.off("SIGINT", otherHandler);
|
||||
process.kill = originalKill;
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("cleans up locks on exit", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-"));
|
||||
try {
|
||||
const sessionFile = path.join(root, "sessions.json");
|
||||
const lockPath = `${sessionFile}.lock`;
|
||||
await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 });
|
||||
|
||||
process.emit("exit", 0);
|
||||
|
||||
await expect(fs.access(lockPath)).rejects.toThrow();
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("keeps other signal listeners registered", () => {
|
||||
const keepAlive = () => {};
|
||||
process.on("SIGINT", keepAlive);
|
||||
|
||||
__testing.handleTerminationSignal("SIGINT");
|
||||
|
||||
expect(process.listeners("SIGINT")).toContain(keepAlive);
|
||||
process.off("SIGINT", keepAlive);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -14,9 +13,6 @@ type HeldLock = {
|
||||
};
|
||||
|
||||
const HELD_LOCKS = new Map<string, HeldLock>();
|
||||
const CLEANUP_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"] as const;
|
||||
type CleanupSignal = (typeof CLEANUP_SIGNALS)[number];
|
||||
const cleanupHandlers = new Map<CleanupSignal, () => void>();
|
||||
|
||||
function isAlive(pid: number): boolean {
|
||||
if (!Number.isFinite(pid) || pid <= 0) return false;
|
||||
@@ -28,65 +24,6 @@ function isAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously release all held locks.
|
||||
* Used during process exit when async operations aren't reliable.
|
||||
*/
|
||||
function releaseAllLocksSync(): void {
|
||||
for (const [sessionFile, held] of HELD_LOCKS) {
|
||||
try {
|
||||
if (typeof held.handle.fd === "number") {
|
||||
fsSync.closeSync(held.handle.fd);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during cleanup - best effort
|
||||
}
|
||||
try {
|
||||
fsSync.rmSync(held.lockPath, { force: true });
|
||||
} catch {
|
||||
// Ignore errors during cleanup - best effort
|
||||
}
|
||||
HELD_LOCKS.delete(sessionFile);
|
||||
}
|
||||
}
|
||||
|
||||
let cleanupRegistered = false;
|
||||
|
||||
function handleTerminationSignal(signal: CleanupSignal): void {
|
||||
releaseAllLocksSync();
|
||||
const shouldReraise = process.listenerCount(signal) === 1;
|
||||
if (shouldReraise) {
|
||||
const handler = cleanupHandlers.get(signal);
|
||||
if (handler) process.off(signal, handler);
|
||||
try {
|
||||
process.kill(process.pid, signal);
|
||||
} catch {
|
||||
// Ignore errors during shutdown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerCleanupHandlers(): void {
|
||||
if (cleanupRegistered) return;
|
||||
cleanupRegistered = true;
|
||||
|
||||
// Cleanup on normal exit and process.exit() calls
|
||||
process.on("exit", () => {
|
||||
releaseAllLocksSync();
|
||||
});
|
||||
|
||||
// Handle termination signals
|
||||
for (const signal of CLEANUP_SIGNALS) {
|
||||
try {
|
||||
const handler = () => handleTerminationSignal(signal);
|
||||
cleanupHandlers.set(signal, handler);
|
||||
process.on(signal, handler);
|
||||
} catch {
|
||||
// Ignore unsupported signals on this platform.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readLockPayload(lockPath: string): Promise<LockFilePayload | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(lockPath, "utf8");
|
||||
@@ -106,7 +43,6 @@ export async function acquireSessionWriteLock(params: {
|
||||
}): Promise<{
|
||||
release: () => Promise<void>;
|
||||
}> {
|
||||
registerCleanupHandlers();
|
||||
const timeoutMs = params.timeoutMs ?? 10_000;
|
||||
const staleMs = params.staleMs ?? 30 * 60 * 1000;
|
||||
const sessionFile = path.resolve(params.sessionFile);
|
||||
@@ -180,9 +116,3 @@ export async function acquireSessionWriteLock(params: {
|
||||
const owner = payload?.pid ? `pid=${payload.pid}` : "unknown";
|
||||
throw new Error(`session file locked (timeout ${timeoutMs}ms): ${owner} ${lockPath}`);
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
cleanupSignals: [...CLEANUP_SIGNALS],
|
||||
handleTerminationSignal,
|
||||
releaseAllLocksSync,
|
||||
};
|
||||
@@ -181,44 +181,9 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||
defineChatCommand({
|
||||
key: "tts",
|
||||
nativeName: "tts",
|
||||
description: "Control text-to-speech (TTS).",
|
||||
description: "Configure text-to-speech.",
|
||||
textAlias: "/tts",
|
||||
args: [
|
||||
{
|
||||
name: "action",
|
||||
description: "TTS action",
|
||||
type: "string",
|
||||
choices: [
|
||||
{ value: "on", label: "On" },
|
||||
{ value: "off", label: "Off" },
|
||||
{ value: "status", label: "Status" },
|
||||
{ value: "provider", label: "Provider" },
|
||||
{ value: "limit", label: "Limit" },
|
||||
{ value: "summary", label: "Summary" },
|
||||
{ value: "audio", label: "Audio" },
|
||||
{ value: "help", label: "Help" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
description: "Provider, limit, or text",
|
||||
type: "string",
|
||||
captureRemaining: true,
|
||||
},
|
||||
],
|
||||
argsMenu: {
|
||||
arg: "action",
|
||||
title:
|
||||
"TTS Actions:\n" +
|
||||
"• On – Enable TTS for responses\n" +
|
||||
"• Off – Disable TTS\n" +
|
||||
"• Status – Show current settings\n" +
|
||||
"• Provider – Set voice provider (edge, elevenlabs, openai)\n" +
|
||||
"• Limit – Set max characters for TTS\n" +
|
||||
"• Summary – Toggle AI summary for long texts\n" +
|
||||
"• Audio – Generate TTS from custom text\n" +
|
||||
"• Help – Show usage guide",
|
||||
},
|
||||
acceptsArgs: true,
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "whoami",
|
||||
|
||||
@@ -229,12 +229,7 @@ describe("commands registry args", () => {
|
||||
|
||||
const menu = resolveCommandArgMenu({ command, args: undefined, cfg: {} as never });
|
||||
expect(menu?.arg.name).toBe("mode");
|
||||
expect(menu?.choices).toEqual([
|
||||
{ label: "off", value: "off" },
|
||||
{ label: "tokens", value: "tokens" },
|
||||
{ label: "full", value: "full" },
|
||||
{ label: "cost", value: "cost" },
|
||||
]);
|
||||
expect(menu?.choices).toEqual(["off", "tokens", "full", "cost"]);
|
||||
});
|
||||
|
||||
it("does not show menus when arg already provided", () => {
|
||||
@@ -289,10 +284,7 @@ describe("commands registry args", () => {
|
||||
|
||||
const menu = resolveCommandArgMenu({ command, args: undefined, cfg: {} as never });
|
||||
expect(menu?.arg.name).toBe("level");
|
||||
expect(menu?.choices).toEqual([
|
||||
{ label: "low", value: "low" },
|
||||
{ label: "high", value: "high" },
|
||||
]);
|
||||
expect(menu?.choices).toEqual(["low", "high"]);
|
||||
expect(seen?.commandKey).toBe("think");
|
||||
expect(seen?.argName).toBe("level");
|
||||
expect(seen?.provider).toBeTruthy();
|
||||
|
||||
@@ -255,41 +255,33 @@ function resolveDefaultCommandContext(cfg?: ClawdbotConfig): {
|
||||
};
|
||||
}
|
||||
|
||||
export type ResolvedCommandArgChoice = { value: string; label: string };
|
||||
|
||||
export function resolveCommandArgChoices(params: {
|
||||
command: ChatCommandDefinition;
|
||||
arg: CommandArgDefinition;
|
||||
cfg?: ClawdbotConfig;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}): ResolvedCommandArgChoice[] {
|
||||
}): string[] {
|
||||
const { command, arg, cfg } = params;
|
||||
if (!arg.choices) return [];
|
||||
const provided = arg.choices;
|
||||
const raw = Array.isArray(provided)
|
||||
? provided
|
||||
: (() => {
|
||||
const defaults = resolveDefaultCommandContext(cfg);
|
||||
const context: CommandArgChoiceContext = {
|
||||
cfg,
|
||||
provider: params.provider ?? defaults.provider,
|
||||
model: params.model ?? defaults.model,
|
||||
command,
|
||||
arg,
|
||||
};
|
||||
return provided(context);
|
||||
})();
|
||||
return raw.map((choice) =>
|
||||
typeof choice === "string" ? { value: choice, label: choice } : choice,
|
||||
);
|
||||
if (Array.isArray(provided)) return provided;
|
||||
const defaults = resolveDefaultCommandContext(cfg);
|
||||
const context: CommandArgChoiceContext = {
|
||||
cfg,
|
||||
provider: params.provider ?? defaults.provider,
|
||||
model: params.model ?? defaults.model,
|
||||
command,
|
||||
arg,
|
||||
};
|
||||
return provided(context);
|
||||
}
|
||||
|
||||
export function resolveCommandArgMenu(params: {
|
||||
command: ChatCommandDefinition;
|
||||
args?: CommandArgs;
|
||||
cfg?: ClawdbotConfig;
|
||||
}): { arg: CommandArgDefinition; choices: ResolvedCommandArgChoice[]; title?: string } | null {
|
||||
}): { arg: CommandArgDefinition; choices: string[]; title?: string } | null {
|
||||
const { command, args, cfg } = params;
|
||||
if (!command.args || !command.argsMenu) return null;
|
||||
if (command.argsParsing === "none") return null;
|
||||
|
||||
@@ -12,16 +12,14 @@ export type CommandArgChoiceContext = {
|
||||
arg: CommandArgDefinition;
|
||||
};
|
||||
|
||||
export type CommandArgChoice = string | { value: string; label: string };
|
||||
|
||||
export type CommandArgChoicesProvider = (context: CommandArgChoiceContext) => CommandArgChoice[];
|
||||
export type CommandArgChoicesProvider = (context: CommandArgChoiceContext) => string[];
|
||||
|
||||
export type CommandArgDefinition = {
|
||||
name: string;
|
||||
description: string;
|
||||
type: CommandArgType;
|
||||
required?: boolean;
|
||||
choices?: CommandArgChoice[] | CommandArgChoicesProvider;
|
||||
choices?: string[] | CommandArgChoicesProvider;
|
||||
captureRemaining?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,18 +6,20 @@ import {
|
||||
getTtsMaxLength,
|
||||
getTtsProvider,
|
||||
isSummarizationEnabled,
|
||||
isTtsEnabled,
|
||||
isTtsProviderConfigured,
|
||||
normalizeTtsAutoMode,
|
||||
resolveTtsAutoMode,
|
||||
resolveTtsApiKey,
|
||||
resolveTtsConfig,
|
||||
resolveTtsPrefsPath,
|
||||
resolveTtsProviderOrder,
|
||||
setLastTtsAttempt,
|
||||
setSummarizationEnabled,
|
||||
setTtsEnabled,
|
||||
setTtsMaxLength,
|
||||
setTtsProvider,
|
||||
textToSpeech,
|
||||
} from "../../tts/tts.js";
|
||||
import { updateSessionStore } from "../../config/sessions.js";
|
||||
|
||||
type ParsedTtsCommand = {
|
||||
action: string;
|
||||
@@ -38,27 +40,14 @@ function ttsUsage(): ReplyPayload {
|
||||
// Keep usage in one place so help/validation stays consistent.
|
||||
return {
|
||||
text:
|
||||
`🔊 **TTS (Text-to-Speech) Help**\n\n` +
|
||||
`**Commands:**\n` +
|
||||
`• /tts on — Enable automatic TTS for replies\n` +
|
||||
`• /tts off — Disable TTS\n` +
|
||||
`• /tts status — Show current settings\n` +
|
||||
`• /tts provider [name] — View/change provider\n` +
|
||||
`• /tts limit [number] — View/change text limit\n` +
|
||||
`• /tts summary [on|off] — View/change auto-summary\n` +
|
||||
`• /tts audio <text> — Generate audio from text\n\n` +
|
||||
`**Providers:**\n` +
|
||||
`• edge — Free, fast (default)\n` +
|
||||
`• openai — High quality (requires API key)\n` +
|
||||
`• elevenlabs — Premium voices (requires API key)\n\n` +
|
||||
`**Text Limit (default: 1500, max: 4096):**\n` +
|
||||
`When text exceeds the limit:\n` +
|
||||
`• Summary ON: AI summarizes, then generates audio\n` +
|
||||
`• Summary OFF: Truncates text, then generates audio\n\n` +
|
||||
`**Examples:**\n` +
|
||||
`/tts provider edge\n` +
|
||||
`/tts limit 2000\n` +
|
||||
`/tts audio Hello, this is a test!`,
|
||||
"⚙️ Usage: /tts <off|always|inbound|tagged|status|provider|limit|summary|audio> [value]" +
|
||||
"\nExamples:\n" +
|
||||
"/tts always\n" +
|
||||
"/tts provider openai\n" +
|
||||
"/tts provider edge\n" +
|
||||
"/tts limit 2000\n" +
|
||||
"/tts summary off\n" +
|
||||
"/tts audio Hello from Clawdbot",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,27 +72,35 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
return { shouldContinue: false, reply: ttsUsage() };
|
||||
}
|
||||
|
||||
if (action === "on") {
|
||||
setTtsEnabled(prefsPath, true);
|
||||
return { shouldContinue: false, reply: { text: "🔊 TTS enabled." } };
|
||||
}
|
||||
|
||||
if (action === "off") {
|
||||
setTtsEnabled(prefsPath, false);
|
||||
return { shouldContinue: false, reply: { text: "🔇 TTS disabled." } };
|
||||
const requestedAuto = normalizeTtsAutoMode(
|
||||
action === "on" ? "always" : action === "off" ? "off" : action,
|
||||
);
|
||||
if (requestedAuto) {
|
||||
const entry = params.sessionEntry;
|
||||
const sessionKey = params.sessionKey;
|
||||
const store = params.sessionStore;
|
||||
if (entry && store && sessionKey) {
|
||||
entry.ttsAuto = requestedAuto;
|
||||
entry.updatedAt = Date.now();
|
||||
store[sessionKey] = entry;
|
||||
if (params.storePath) {
|
||||
await updateSessionStore(params.storePath, (store) => {
|
||||
store[sessionKey] = entry;
|
||||
});
|
||||
}
|
||||
}
|
||||
const label = requestedAuto === "always" ? "enabled (always)" : requestedAuto;
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: requestedAuto === "off" ? "🔇 TTS disabled." : `🔊 TTS ${label}.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action === "audio") {
|
||||
if (!args.trim()) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
`🎤 Generate audio from text.\n\n` +
|
||||
`Usage: /tts audio <text>\n` +
|
||||
`Example: /tts audio Hello, this is a test!`,
|
||||
},
|
||||
};
|
||||
return { shouldContinue: false, reply: ttsUsage() };
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
@@ -149,6 +146,9 @@ 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 hasOpenAI = Boolean(resolveTtsApiKey(config, "openai"));
|
||||
const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs"));
|
||||
const hasEdge = isTtsProviderConfigured(config, "edge");
|
||||
@@ -158,6 +158,7 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
text:
|
||||
`🎙️ TTS provider\n` +
|
||||
`Primary: ${currentProvider}\n` +
|
||||
`Fallbacks: ${fallback.join(", ") || "none"}\n` +
|
||||
`OpenAI key: ${hasOpenAI ? "✅" : "❌"}\n` +
|
||||
`ElevenLabs key: ${hasElevenLabs ? "✅" : "❌"}\n` +
|
||||
`Edge enabled: ${hasEdge ? "✅" : "❌"}\n` +
|
||||
@@ -172,9 +173,18 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
}
|
||||
|
||||
setTtsProvider(prefsPath, requested);
|
||||
const fallback = resolveTtsProviderOrder(requested)
|
||||
.slice(1)
|
||||
.filter((provider) => isTtsProviderConfigured(config, provider));
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: `✅ TTS provider set to ${requested}.` },
|
||||
reply: {
|
||||
text:
|
||||
`✅ TTS provider set to ${requested} (fallbacks: ${fallback.join(", ") || "none"}).` +
|
||||
(requested === "edge"
|
||||
? "\nEnable Edge TTS in config: messages.tts.edge.enabled = true."
|
||||
: ""),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,22 +193,12 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
const currentLimit = getTtsMaxLength(prefsPath);
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
`📏 TTS limit: ${currentLimit} characters.\n\n` +
|
||||
`Text longer than this triggers summary (if enabled).\n` +
|
||||
`Range: 100-4096 chars (Telegram max).\n\n` +
|
||||
`To change: /tts limit <number>\n` +
|
||||
`Example: /tts limit 2000`,
|
||||
},
|
||||
reply: { text: `📏 TTS limit: ${currentLimit} characters.` },
|
||||
};
|
||||
}
|
||||
const next = Number.parseInt(args.trim(), 10);
|
||||
if (!Number.isFinite(next) || next < 100 || next > 4096) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "❌ Limit must be between 100 and 4096 characters." },
|
||||
};
|
||||
if (!Number.isFinite(next) || next < 100 || next > 10_000) {
|
||||
return { shouldContinue: false, reply: ttsUsage() };
|
||||
}
|
||||
setTtsMaxLength(prefsPath, next);
|
||||
return {
|
||||
@@ -210,17 +210,9 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
if (action === "summary") {
|
||||
if (!args.trim()) {
|
||||
const enabled = isSummarizationEnabled(prefsPath);
|
||||
const maxLen = getTtsMaxLength(prefsPath);
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
`📝 TTS auto-summary: ${enabled ? "on" : "off"}.\n\n` +
|
||||
`When text exceeds ${maxLen} chars:\n` +
|
||||
`• ON: summarizes text, then generates audio\n` +
|
||||
`• OFF: truncates text, then generates audio\n\n` +
|
||||
`To change: /tts summary on | off`,
|
||||
},
|
||||
reply: { text: `📝 TTS auto-summary: ${enabled ? "on" : "off"}.` },
|
||||
};
|
||||
}
|
||||
const requested = args.trim().toLowerCase();
|
||||
@@ -237,16 +229,27 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
}
|
||||
|
||||
if (action === "status") {
|
||||
const enabled = isTtsEnabled(config, prefsPath);
|
||||
const sessionAuto = params.sessionEntry?.ttsAuto;
|
||||
const autoMode = resolveTtsAutoMode({ config, prefsPath, sessionAuto });
|
||||
const enabled = autoMode !== "off";
|
||||
const provider = getTtsProvider(config, prefsPath);
|
||||
const hasKey = isTtsProviderConfigured(config, provider);
|
||||
const providerStatus =
|
||||
provider === "edge"
|
||||
? hasKey
|
||||
? "✅ enabled"
|
||||
: "❌ disabled"
|
||||
: hasKey
|
||||
? "✅ key"
|
||||
: "❌ no key";
|
||||
const maxLength = getTtsMaxLength(prefsPath);
|
||||
const summarize = isSummarizationEnabled(prefsPath);
|
||||
const last = getLastTtsAttempt();
|
||||
const autoLabel = sessionAuto ? `${autoMode} (session)` : autoMode;
|
||||
const lines = [
|
||||
"📊 TTS status",
|
||||
`State: ${enabled ? "✅ enabled" : "❌ disabled"}`,
|
||||
`Provider: ${provider} (${hasKey ? "✅ configured" : "❌ not configured"})`,
|
||||
`Auto: ${enabled ? autoLabel : "off"}`,
|
||||
`Provider: ${provider} (${providerStatus})`,
|
||||
`Text limit: ${maxLength} chars`,
|
||||
`Auto-summary: ${summarize ? "on" : "off"}`,
|
||||
];
|
||||
|
||||
@@ -420,17 +420,3 @@ describe("handleCommands subagents", () => {
|
||||
expect(result.reply?.text).toContain("Status: done");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleCommands /tts", () => {
|
||||
it("returns status for bare /tts on text command surfaces", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
messages: { tts: { prefsPath: path.join(testWorkspaceDir, "tts.json") } },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/tts", cfg);
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("TTS status");
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
||||
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
|
||||
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
|
||||
import { isRoutableChannel, routeReply } from "./route-reply.js";
|
||||
import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
|
||||
import { maybeApplyTtsToPayload, normalizeTtsAutoMode } from "../../tts/tts.js";
|
||||
|
||||
const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;
|
||||
const AUDIO_HEADER_RE = /^\[Audio\b/i;
|
||||
@@ -266,26 +266,12 @@ export async function dispatchReplyFromConfig(params: {
|
||||
return { queuedFinal, counts };
|
||||
}
|
||||
|
||||
// Track accumulated block text for TTS generation after streaming completes.
|
||||
// When block streaming succeeds, there's no final reply, so we need to generate
|
||||
// TTS audio separately from the accumulated block content.
|
||||
let accumulatedBlockText = "";
|
||||
let blockCount = 0;
|
||||
|
||||
const replyResult = await (params.replyResolver ?? getReplyFromConfig)(
|
||||
ctx,
|
||||
{
|
||||
...params.replyOptions,
|
||||
onBlockReply: (payload: ReplyPayload, context) => {
|
||||
const run = async () => {
|
||||
// Accumulate block text for TTS generation after streaming
|
||||
if (payload.text) {
|
||||
if (accumulatedBlockText.length > 0) {
|
||||
accumulatedBlockText += "\n";
|
||||
}
|
||||
accumulatedBlockText += payload.text;
|
||||
blockCount++;
|
||||
}
|
||||
const ttsPayload = await maybeApplyTtsToPayload({
|
||||
payload,
|
||||
cfg,
|
||||
@@ -341,62 +327,6 @@ export async function dispatchReplyFromConfig(params: {
|
||||
queuedFinal = dispatcher.sendFinalReply(ttsReply) || queuedFinal;
|
||||
}
|
||||
}
|
||||
|
||||
const ttsMode = resolveTtsConfig(cfg).mode ?? "final";
|
||||
// Generate TTS-only reply after block streaming completes (when there's no final reply).
|
||||
// This handles the case where block streaming succeeds and drops final payloads,
|
||||
// but we still want TTS audio to be generated from the accumulated block content.
|
||||
if (
|
||||
ttsMode === "final" &&
|
||||
replies.length === 0 &&
|
||||
blockCount > 0 &&
|
||||
accumulatedBlockText.trim()
|
||||
) {
|
||||
try {
|
||||
const ttsSyntheticReply = await maybeApplyTtsToPayload({
|
||||
payload: { text: accumulatedBlockText },
|
||||
cfg,
|
||||
channel: ttsChannel,
|
||||
kind: "final",
|
||||
inboundAudio,
|
||||
ttsAuto: sessionTtsAuto,
|
||||
});
|
||||
// Only send if TTS was actually applied (mediaUrl exists)
|
||||
if (ttsSyntheticReply.mediaUrl) {
|
||||
// Send TTS-only payload (no text, just audio) so it doesn't duplicate the block content
|
||||
const ttsOnlyPayload: ReplyPayload = {
|
||||
mediaUrl: ttsSyntheticReply.mediaUrl,
|
||||
audioAsVoice: ttsSyntheticReply.audioAsVoice,
|
||||
};
|
||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||
const result = await routeReply({
|
||||
payload: ttsOnlyPayload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
sessionKey: ctx.SessionKey,
|
||||
accountId: ctx.AccountId,
|
||||
threadId: ctx.MessageThreadId,
|
||||
cfg,
|
||||
});
|
||||
queuedFinal = result.ok || queuedFinal;
|
||||
if (result.ok) routedFinalCount += 1;
|
||||
if (!result.ok) {
|
||||
logVerbose(
|
||||
`dispatch-from-config: route-reply (tts-only) failed: ${result.error ?? "unknown error"}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const didQueue = dispatcher.sendFinalReply(ttsOnlyPayload);
|
||||
queuedFinal = didQueue || queuedFinal;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`dispatch-from-config: accumulated block TTS failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await dispatcher.waitForIdle();
|
||||
|
||||
const counts = dispatcher.getQueuedCounts();
|
||||
|
||||
@@ -80,20 +80,14 @@ async function promptTelegramAllowFrom(params: {
|
||||
if (!token) return null;
|
||||
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
result?: { id?: number | string };
|
||||
} | null;
|
||||
const id = data?.ok ? data?.result?.id : undefined;
|
||||
if (typeof id === "number" || typeof id === "string") return String(id);
|
||||
return null;
|
||||
} catch {
|
||||
// Network error during username lookup - return null to prompt user for numeric ID
|
||||
return null;
|
||||
}
|
||||
const res = await fetch(url);
|
||||
const data = (await res.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
result?: { id?: number | string };
|
||||
} | null;
|
||||
const id = data?.ok ? data?.result?.id : undefined;
|
||||
if (typeof id === "number" || typeof id === "string") return String(id);
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseInput = (value: string) =>
|
||||
|
||||
@@ -78,48 +78,6 @@ describe("argv helpers", () => {
|
||||
});
|
||||
expect(nodeArgv).toEqual(["node", "clawdbot", "status"]);
|
||||
|
||||
const versionedNodeArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["node-22", "clawdbot", "status"],
|
||||
});
|
||||
expect(versionedNodeArgv).toEqual(["node-22", "clawdbot", "status"]);
|
||||
|
||||
const versionedNodeWindowsArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["node-22.2.0.exe", "clawdbot", "status"],
|
||||
});
|
||||
expect(versionedNodeWindowsArgv).toEqual(["node-22.2.0.exe", "clawdbot", "status"]);
|
||||
|
||||
const versionedNodePatchlessArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["node-22.2", "clawdbot", "status"],
|
||||
});
|
||||
expect(versionedNodePatchlessArgv).toEqual(["node-22.2", "clawdbot", "status"]);
|
||||
|
||||
const versionedNodeWindowsPatchlessArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["node-22.2.exe", "clawdbot", "status"],
|
||||
});
|
||||
expect(versionedNodeWindowsPatchlessArgv).toEqual(["node-22.2.exe", "clawdbot", "status"]);
|
||||
|
||||
const versionedNodeWithPathArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["/usr/bin/node-22.2.0", "clawdbot", "status"],
|
||||
});
|
||||
expect(versionedNodeWithPathArgv).toEqual(["/usr/bin/node-22.2.0", "clawdbot", "status"]);
|
||||
|
||||
const nodejsArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["nodejs", "clawdbot", "status"],
|
||||
});
|
||||
expect(nodejsArgv).toEqual(["nodejs", "clawdbot", "status"]);
|
||||
|
||||
const nonVersionedNodeArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["node-dev", "clawdbot", "status"],
|
||||
});
|
||||
expect(nonVersionedNodeArgv).toEqual(["node", "clawdbot", "node-dev", "clawdbot", "status"]);
|
||||
|
||||
const directArgv = buildParseArgv({
|
||||
programName: "clawdbot",
|
||||
rawArgs: ["clawdbot", "status"],
|
||||
|
||||
+5
-17
@@ -96,27 +96,15 @@ export function buildParseArgv(params: {
|
||||
: baseArgv;
|
||||
const executable = (normalizedArgv[0]?.split(/[/\\]/).pop() ?? "").toLowerCase();
|
||||
const looksLikeNode =
|
||||
normalizedArgv.length >= 2 && (isNodeExecutable(executable) || isBunExecutable(executable));
|
||||
normalizedArgv.length >= 2 &&
|
||||
(executable === "node" ||
|
||||
executable === "node.exe" ||
|
||||
executable === "bun" ||
|
||||
executable === "bun.exe");
|
||||
if (looksLikeNode) return normalizedArgv;
|
||||
return ["node", programName || "clawdbot", ...normalizedArgv];
|
||||
}
|
||||
|
||||
const nodeExecutablePattern = /^node-\d+(?:\.\d+)*(?:\.exe)?$/;
|
||||
|
||||
function isNodeExecutable(executable: string): boolean {
|
||||
return (
|
||||
executable === "node" ||
|
||||
executable === "node.exe" ||
|
||||
executable === "nodejs" ||
|
||||
executable === "nodejs.exe" ||
|
||||
nodeExecutablePattern.test(executable)
|
||||
);
|
||||
}
|
||||
|
||||
function isBunExecutable(executable: string): boolean {
|
||||
return executable === "bun" || executable === "bun.exe";
|
||||
}
|
||||
|
||||
export function shouldMigrateStateFromPath(path: string[]): boolean {
|
||||
if (path.length === 0) return true;
|
||||
const [primary, secondary] = path;
|
||||
|
||||
+1
-10
@@ -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, hasHelpOrVersion } from "./argv.js";
|
||||
import { getPrimaryCommand } from "./argv.js";
|
||||
import { tryRouteCli } from "./route.js";
|
||||
|
||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||
@@ -56,15 +56,6 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||
await registerSubCliByName(program, primary);
|
||||
}
|
||||
|
||||
const shouldSkipPluginRegistration = !primary && hasHelpOrVersion(parseArgv);
|
||||
if (!shouldSkipPluginRegistration) {
|
||||
// Register plugin CLI commands before parsing
|
||||
const { registerPluginCliCommands } = await import("../plugins/cli.js");
|
||||
const { loadConfig } = await import("../config/config.js");
|
||||
registerPluginCliCommands(program, loadConfig());
|
||||
}
|
||||
|
||||
await program.parseAsync(parseArgv);
|
||||
}
|
||||
|
||||
|
||||
@@ -93,18 +93,16 @@ function buildDiscordCommandOptions(params: {
|
||||
typeof focused?.value === "string" ? focused.value.trim().toLowerCase() : "";
|
||||
const choices = resolveCommandArgChoices({ command, arg, cfg });
|
||||
const filtered = focusValue
|
||||
? choices.filter((choice) => choice.label.toLowerCase().includes(focusValue))
|
||||
? choices.filter((choice) => choice.toLowerCase().includes(focusValue))
|
||||
: choices;
|
||||
await interaction.respond(
|
||||
filtered.slice(0, 25).map((choice) => ({ name: choice.label, value: choice.value })),
|
||||
filtered.slice(0, 25).map((choice) => ({ name: choice, value: choice })),
|
||||
);
|
||||
}
|
||||
: undefined;
|
||||
const choices =
|
||||
resolvedChoices.length > 0 && !autocomplete
|
||||
? resolvedChoices
|
||||
.slice(0, 25)
|
||||
.map((choice) => ({ name: choice.label, value: choice.value }))
|
||||
? resolvedChoices.slice(0, 25).map((choice) => ({ name: choice, value: choice }))
|
||||
: undefined;
|
||||
return {
|
||||
name: arg.name,
|
||||
@@ -353,11 +351,7 @@ export function createDiscordCommandArgFallbackButton(params: DiscordCommandArgC
|
||||
|
||||
function buildDiscordCommandArgMenu(params: {
|
||||
command: ChatCommandDefinition;
|
||||
menu: {
|
||||
arg: CommandArgDefinition;
|
||||
choices: Array<{ value: string; label: string }>;
|
||||
title?: string;
|
||||
};
|
||||
menu: { arg: CommandArgDefinition; choices: string[]; title?: string };
|
||||
interaction: CommandInteraction;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
discordConfig: DiscordConfig;
|
||||
@@ -371,11 +365,11 @@ function buildDiscordCommandArgMenu(params: {
|
||||
const buttons = choices.map(
|
||||
(choice) =>
|
||||
new DiscordCommandArgButton({
|
||||
label: choice.label,
|
||||
label: choice,
|
||||
customId: buildDiscordCommandArgCustomId({
|
||||
command: commandLabel,
|
||||
arg: menu.arg.name,
|
||||
value: choice.value,
|
||||
value: choice,
|
||||
userId,
|
||||
}),
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
function readTerminalCss() {
|
||||
// This test is intentionally simple: it guards against regressions where the
|
||||
// docs header stops being sticky because sticky elements live inside an
|
||||
// overflow-clipped container.
|
||||
const path = join(process.cwd(), "docs", "assets", "terminal.css");
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
describe("docs terminal.css", () => {
|
||||
test("keeps the docs header sticky (shell is sticky)", () => {
|
||||
const css = readTerminalCss();
|
||||
expect(css).toMatch(/\.shell\s*\{[^}]*position:\s*sticky;[^}]*top:\s*0;[^}]*\}/s);
|
||||
});
|
||||
|
||||
test("does not rely on making body overflow visible", () => {
|
||||
const css = readTerminalCss();
|
||||
expect(css).not.toMatch(/body\s*\{[^}]*overflow-x:\s*visible;[^}]*\}/s);
|
||||
});
|
||||
|
||||
test("does not make the terminal frame overflow visible (can break layout)", () => {
|
||||
const css = readTerminalCss();
|
||||
expect(css).not.toMatch(/\.shell__frame\s*\{[^}]*overflow:\s*visible;[^}]*\}/s);
|
||||
});
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isAbortError, isTransientNetworkError } from "./unhandled-rejections.js";
|
||||
|
||||
describe("isAbortError", () => {
|
||||
it("returns true for error with name AbortError", () => {
|
||||
const error = new Error("aborted");
|
||||
error.name = "AbortError";
|
||||
expect(isAbortError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for error with "This operation was aborted" message', () => {
|
||||
const error = new Error("This operation was aborted");
|
||||
expect(isAbortError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for undici-style AbortError", () => {
|
||||
// Node's undici throws errors with this exact message
|
||||
const error = Object.assign(new Error("This operation was aborted"), { name: "AbortError" });
|
||||
expect(isAbortError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for object with AbortError name", () => {
|
||||
expect(isAbortError({ name: "AbortError", message: "test" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for regular errors", () => {
|
||||
expect(isAbortError(new Error("Something went wrong"))).toBe(false);
|
||||
expect(isAbortError(new TypeError("Cannot read property"))).toBe(false);
|
||||
expect(isAbortError(new RangeError("Invalid array length"))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for errors with similar but different messages", () => {
|
||||
expect(isAbortError(new Error("Operation aborted"))).toBe(false);
|
||||
expect(isAbortError(new Error("aborted"))).toBe(false);
|
||||
expect(isAbortError(new Error("Request was aborted"))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for null and undefined", () => {
|
||||
expect(isAbortError(null)).toBe(false);
|
||||
expect(isAbortError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for non-error values", () => {
|
||||
expect(isAbortError("string error")).toBe(false);
|
||||
expect(isAbortError(42)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for plain objects without AbortError name", () => {
|
||||
expect(isAbortError({ message: "plain object" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTransientNetworkError", () => {
|
||||
it("returns true for errors with transient network codes", () => {
|
||||
const codes = [
|
||||
"ECONNRESET",
|
||||
"ECONNREFUSED",
|
||||
"ENOTFOUND",
|
||||
"ETIMEDOUT",
|
||||
"ESOCKETTIMEDOUT",
|
||||
"ECONNABORTED",
|
||||
"EPIPE",
|
||||
"EHOSTUNREACH",
|
||||
"ENETUNREACH",
|
||||
"EAI_AGAIN",
|
||||
"UND_ERR_CONNECT_TIMEOUT",
|
||||
"UND_ERR_SOCKET",
|
||||
"UND_ERR_HEADERS_TIMEOUT",
|
||||
"UND_ERR_BODY_TIMEOUT",
|
||||
];
|
||||
|
||||
for (const code of codes) {
|
||||
const error = Object.assign(new Error("test"), { code });
|
||||
expect(isTransientNetworkError(error), `code: ${code}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns true for TypeError with "fetch failed" message', () => {
|
||||
const error = new TypeError("fetch failed");
|
||||
expect(isTransientNetworkError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for fetch failed with network cause", () => {
|
||||
const cause = Object.assign(new Error("getaddrinfo ENOTFOUND"), { code: "ENOTFOUND" });
|
||||
const error = Object.assign(new TypeError("fetch failed"), { cause });
|
||||
expect(isTransientNetworkError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for nested cause chain with network error", () => {
|
||||
const innerCause = Object.assign(new Error("connection reset"), { code: "ECONNRESET" });
|
||||
const outerCause = Object.assign(new Error("wrapper"), { cause: innerCause });
|
||||
const error = Object.assign(new TypeError("fetch failed"), { cause: outerCause });
|
||||
expect(isTransientNetworkError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for AggregateError containing network errors", () => {
|
||||
const networkError = Object.assign(new Error("timeout"), { code: "ETIMEDOUT" });
|
||||
const error = new AggregateError([networkError], "Multiple errors");
|
||||
expect(isTransientNetworkError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for regular errors without network codes", () => {
|
||||
expect(isTransientNetworkError(new Error("Something went wrong"))).toBe(false);
|
||||
expect(isTransientNetworkError(new TypeError("Cannot read property"))).toBe(false);
|
||||
expect(isTransientNetworkError(new RangeError("Invalid array length"))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for errors with non-network codes", () => {
|
||||
const error = Object.assign(new Error("test"), { code: "INVALID_CONFIG" });
|
||||
expect(isTransientNetworkError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for null and undefined", () => {
|
||||
expect(isTransientNetworkError(null)).toBe(false);
|
||||
expect(isTransientNetworkError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for non-error values", () => {
|
||||
expect(isTransientNetworkError("string error")).toBe(false);
|
||||
expect(isTransientNetworkError(42)).toBe(false);
|
||||
expect(isTransientNetworkError({ message: "plain object" })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for AggregateError with only non-network errors", () => {
|
||||
const error = new AggregateError([new Error("regular error")], "Multiple errors");
|
||||
expect(isTransientNetworkError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -6,83 +6,6 @@ type UnhandledRejectionHandler = (reason: unknown) => boolean;
|
||||
|
||||
const handlers = new Set<UnhandledRejectionHandler>();
|
||||
|
||||
/**
|
||||
* Checks if an error is an AbortError.
|
||||
* These are typically intentional cancellations (e.g., during shutdown) and shouldn't crash.
|
||||
*/
|
||||
export function isAbortError(err: unknown): boolean {
|
||||
if (!err || typeof err !== "object") return false;
|
||||
const name = "name" in err ? String(err.name) : "";
|
||||
if (name === "AbortError") return true;
|
||||
// Check for "This operation was aborted" message from Node's undici
|
||||
const message = "message" in err && typeof err.message === "string" ? err.message : "";
|
||||
if (message === "This operation was aborted") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Network error codes that indicate transient failures (shouldn't crash the gateway)
|
||||
const TRANSIENT_NETWORK_CODES = new Set([
|
||||
"ECONNRESET",
|
||||
"ECONNREFUSED",
|
||||
"ENOTFOUND",
|
||||
"ETIMEDOUT",
|
||||
"ESOCKETTIMEDOUT",
|
||||
"ECONNABORTED",
|
||||
"EPIPE",
|
||||
"EHOSTUNREACH",
|
||||
"ENETUNREACH",
|
||||
"EAI_AGAIN",
|
||||
"UND_ERR_CONNECT_TIMEOUT",
|
||||
"UND_ERR_SOCKET",
|
||||
"UND_ERR_HEADERS_TIMEOUT",
|
||||
"UND_ERR_BODY_TIMEOUT",
|
||||
]);
|
||||
|
||||
function getErrorCode(err: unknown): string | undefined {
|
||||
if (!err || typeof err !== "object") return undefined;
|
||||
const code = (err as { code?: unknown }).code;
|
||||
return typeof code === "string" ? code : undefined;
|
||||
}
|
||||
|
||||
function getErrorCause(err: unknown): unknown {
|
||||
if (!err || typeof err !== "object") return undefined;
|
||||
return (err as { cause?: unknown }).cause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an error is a transient network error that shouldn't crash the gateway.
|
||||
* These are typically temporary connectivity issues that will resolve on their own.
|
||||
*/
|
||||
export function isTransientNetworkError(err: unknown): boolean {
|
||||
if (!err) return false;
|
||||
|
||||
// Check the error itself
|
||||
const code = getErrorCode(err);
|
||||
if (code && TRANSIENT_NETWORK_CODES.has(code)) return true;
|
||||
|
||||
// "fetch failed" TypeError from undici (Node's native fetch)
|
||||
if (err instanceof TypeError && err.message === "fetch failed") {
|
||||
const cause = getErrorCause(err);
|
||||
// The cause often contains the actual network error
|
||||
if (cause) return isTransientNetworkError(cause);
|
||||
// Even without a cause, "fetch failed" is typically a network issue
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the cause chain recursively
|
||||
const cause = getErrorCause(err);
|
||||
if (cause && cause !== err) {
|
||||
return isTransientNetworkError(cause);
|
||||
}
|
||||
|
||||
// AggregateError may wrap multiple causes
|
||||
if (err instanceof AggregateError && err.errors?.length) {
|
||||
return err.errors.some((e) => isTransientNetworkError(e));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function registerUnhandledRejectionHandler(handler: UnhandledRejectionHandler): () => void {
|
||||
handlers.add(handler);
|
||||
return () => {
|
||||
@@ -90,6 +13,36 @@ export function registerUnhandledRejectionHandler(handler: UnhandledRejectionHan
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a recoverable/transient error that shouldn't crash the process.
|
||||
* These include network errors and abort signals during shutdown.
|
||||
*/
|
||||
function isRecoverableError(reason: unknown): boolean {
|
||||
if (!reason) return false;
|
||||
|
||||
// Check error name for AbortError
|
||||
if (reason instanceof Error && reason.name === "AbortError") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = reason instanceof Error ? reason.message : String(reason);
|
||||
const lowerMessage = message.toLowerCase();
|
||||
return (
|
||||
lowerMessage.includes("fetch failed") ||
|
||||
lowerMessage.includes("network request") ||
|
||||
lowerMessage.includes("econnrefused") ||
|
||||
lowerMessage.includes("econnreset") ||
|
||||
lowerMessage.includes("etimedout") ||
|
||||
lowerMessage.includes("socket hang up") ||
|
||||
lowerMessage.includes("enotfound") ||
|
||||
lowerMessage.includes("network error") ||
|
||||
lowerMessage.includes("getaddrinfo") ||
|
||||
lowerMessage.includes("client network socket disconnected") ||
|
||||
lowerMessage.includes("this operation was aborted") ||
|
||||
lowerMessage.includes("aborted")
|
||||
);
|
||||
}
|
||||
|
||||
export function isUnhandledRejectionHandled(reason: unknown): boolean {
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
@@ -108,17 +61,9 @@ export function installUnhandledRejectionHandler(): void {
|
||||
process.on("unhandledRejection", (reason, _promise) => {
|
||||
if (isUnhandledRejectionHandled(reason)) return;
|
||||
|
||||
// AbortError is typically an intentional cancellation (e.g., during shutdown)
|
||||
// Log it but don't crash - these are expected during graceful shutdown
|
||||
if (isAbortError(reason)) {
|
||||
console.warn("[clawdbot] Suppressed AbortError:", formatUncaughtError(reason));
|
||||
return;
|
||||
}
|
||||
|
||||
// Transient network errors (fetch failed, connection reset, etc.) shouldn't crash
|
||||
// These are temporary connectivity issues that will resolve on their own
|
||||
if (isTransientNetworkError(reason)) {
|
||||
console.error("[clawdbot] Network error (non-fatal):", formatUncaughtError(reason));
|
||||
// Don't crash on recoverable/transient errors - log them and continue
|
||||
if (isRecoverableError(reason)) {
|
||||
console.error("[clawdbot] Recoverable error (not crashing):", formatUncaughtError(reason));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -1,10 +1,10 @@
|
||||
import type { WebhookRequestBody } from "@line/bot-sdk";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import crypto from "node:crypto";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { danger, logVerbose } from "../globals.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { createLineBot } from "./bot.js";
|
||||
import { validateLineSignature } from "./signature.js";
|
||||
import { normalizePluginHttpPath } from "../plugins/http-path.js";
|
||||
import { registerPluginHttpRoute } from "../plugins/http-registry.js";
|
||||
import {
|
||||
@@ -85,6 +85,11 @@ export function getLineRuntimeState(accountId: string) {
|
||||
return runtimeState.get(`line:${accountId}`);
|
||||
}
|
||||
|
||||
function validateLineSignature(body: string, signature: string, channelSecret: string): boolean {
|
||||
const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
|
||||
return hash === signature;
|
||||
}
|
||||
|
||||
async function readRequestBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateLineSignature } from "./signature.js";
|
||||
|
||||
const sign = (body: string, secret: string) =>
|
||||
crypto.createHmac("SHA256", secret).update(body).digest("base64");
|
||||
|
||||
describe("validateLineSignature", () => {
|
||||
it("accepts valid signatures", () => {
|
||||
const secret = "secret";
|
||||
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
|
||||
|
||||
expect(validateLineSignature(rawBody, sign(rawBody, secret), secret)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects signatures computed with the wrong secret", () => {
|
||||
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
|
||||
|
||||
expect(validateLineSignature(rawBody, sign(rawBody, "wrong-secret"), "secret")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects signatures with a different length", () => {
|
||||
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
|
||||
|
||||
expect(validateLineSignature(rawBody, "short", "secret")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export function validateLineSignature(
|
||||
body: string,
|
||||
signature: string,
|
||||
channelSecret: string,
|
||||
): boolean {
|
||||
const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
|
||||
const hashBuffer = Buffer.from(hash);
|
||||
const signatureBuffer = Buffer.from(signature);
|
||||
|
||||
// Use constant-time comparison to prevent timing attacks.
|
||||
if (hashBuffer.length !== signatureBuffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return crypto.timingSafeEqual(hashBuffer, signatureBuffer);
|
||||
}
|
||||
@@ -70,41 +70,4 @@ describe("createLineWebhookMiddleware", () => {
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects webhooks with invalid signatures", async () => {
|
||||
const onEvents = vi.fn(async () => {});
|
||||
const secret = "secret";
|
||||
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
|
||||
const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents });
|
||||
|
||||
const req = {
|
||||
headers: { "x-line-signature": "invalid-signature" },
|
||||
body: rawBody,
|
||||
} as any;
|
||||
const res = createRes();
|
||||
|
||||
await middleware(req, res, {} as any);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects webhooks with signatures computed using wrong secret", async () => {
|
||||
const onEvents = vi.fn(async () => {});
|
||||
const correctSecret = "correct-secret";
|
||||
const wrongSecret = "wrong-secret";
|
||||
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
|
||||
const middleware = createLineWebhookMiddleware({ channelSecret: correctSecret, onEvents });
|
||||
|
||||
const req = {
|
||||
headers: { "x-line-signature": sign(rawBody, wrongSecret) },
|
||||
body: rawBody,
|
||||
} as any;
|
||||
const res = createRes();
|
||||
|
||||
await middleware(req, res, {} as any);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+7
-2
@@ -1,8 +1,8 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import crypto from "node:crypto";
|
||||
import type { WebhookRequestBody } from "@line/bot-sdk";
|
||||
import { logVerbose, danger } from "../globals.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { validateLineSignature } from "./signature.js";
|
||||
|
||||
export interface LineWebhookOptions {
|
||||
channelSecret: string;
|
||||
@@ -10,6 +10,11 @@ export interface LineWebhookOptions {
|
||||
runtime?: RuntimeEnv;
|
||||
}
|
||||
|
||||
function validateSignature(body: string, signature: string, channelSecret: string): boolean {
|
||||
const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
|
||||
return hash === signature;
|
||||
}
|
||||
|
||||
function readRawBody(req: Request): string | null {
|
||||
const rawBody =
|
||||
(req as { rawBody?: string | Buffer }).rawBody ??
|
||||
@@ -47,7 +52,7 @@ export function createLineWebhookMiddleware(options: LineWebhookOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateLineSignature(rawBody, signature, channelSecret)) {
|
||||
if (!validateSignature(rawBody, signature, channelSecret)) {
|
||||
logVerbose("line: webhook signature validation failed");
|
||||
res.status(401).json({ error: "Invalid signature" });
|
||||
return;
|
||||
|
||||
@@ -103,7 +103,7 @@ function buildSlackCommandArgMenuBlocks(params: {
|
||||
title: string;
|
||||
command: string;
|
||||
arg: string;
|
||||
choices: Array<{ value: string; label: string }>;
|
||||
choices: string[];
|
||||
userId: string;
|
||||
}) {
|
||||
const rows = chunkItems(params.choices, 5).map((choices) => ({
|
||||
@@ -111,11 +111,11 @@ function buildSlackCommandArgMenuBlocks(params: {
|
||||
elements: choices.map((choice) => ({
|
||||
type: "button",
|
||||
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
||||
text: { type: "plain_text", text: choice.label },
|
||||
text: { type: "plain_text", text: choice },
|
||||
value: encodeSlackCommandArgValue({
|
||||
command: params.command,
|
||||
arg: params.arg,
|
||||
value: choice.value,
|
||||
value: choice,
|
||||
userId: params.userId,
|
||||
}),
|
||||
})),
|
||||
|
||||
@@ -366,10 +366,10 @@ export const registerTelegramNativeCommands = ({
|
||||
rows.push(
|
||||
slice.map((choice) => {
|
||||
const args: CommandArgs = {
|
||||
values: { [menu.arg.name]: choice.value },
|
||||
values: { [menu.arg.name]: choice },
|
||||
};
|
||||
return {
|
||||
text: choice.label,
|
||||
text: choice,
|
||||
callback_data: buildCommandTextFromArgs(commandDefinition, args),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -25,24 +25,6 @@ import type { TelegramContext } from "./types.js";
|
||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||
const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/;
|
||||
|
||||
/**
|
||||
* Wraps a Telegram API call with error logging. Ensures network failures are
|
||||
* logged with context before propagating, preventing silent unhandled rejections.
|
||||
*/
|
||||
async function withMediaErrorHandler<T>(
|
||||
operation: string,
|
||||
runtime: RuntimeEnv,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
const errText = formatErrorMessage(err);
|
||||
runtime.error?.(danger(`telegram ${operation} failed: ${errText}`));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deliverReplies(params: {
|
||||
replies: ReplyPayload[];
|
||||
chatId: string;
|
||||
@@ -164,17 +146,17 @@ export async function deliverReplies(params: {
|
||||
mediaParams.message_thread_id = threadParams.message_thread_id;
|
||||
}
|
||||
if (isGif) {
|
||||
await withMediaErrorHandler("sendAnimation", runtime, () =>
|
||||
bot.api.sendAnimation(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
await bot.api.sendAnimation(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
} else if (kind === "image") {
|
||||
await withMediaErrorHandler("sendPhoto", runtime, () =>
|
||||
bot.api.sendPhoto(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
await bot.api.sendPhoto(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
} else if (kind === "video") {
|
||||
await withMediaErrorHandler("sendVideo", runtime, () =>
|
||||
bot.api.sendVideo(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
await bot.api.sendVideo(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
} else if (kind === "audio") {
|
||||
const { useVoice } = resolveTelegramVoiceSend({
|
||||
wantsVoice: reply.audioAsVoice === true, // default false (backward compatible)
|
||||
@@ -187,9 +169,9 @@ export async function deliverReplies(params: {
|
||||
// Switch typing indicator to record_voice before sending.
|
||||
await params.onVoiceRecording?.();
|
||||
try {
|
||||
await withMediaErrorHandler("sendVoice", runtime, () =>
|
||||
bot.api.sendVoice(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
await bot.api.sendVoice(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
} catch (voiceErr) {
|
||||
// Fall back to text if voice messages are forbidden in this chat.
|
||||
// This happens when the recipient has Telegram Premium privacy settings
|
||||
@@ -222,14 +204,14 @@ export async function deliverReplies(params: {
|
||||
}
|
||||
} else {
|
||||
// Audio file - displays with metadata (title, duration) - DEFAULT
|
||||
await withMediaErrorHandler("sendAudio", runtime, () =>
|
||||
bot.api.sendAudio(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
await bot.api.sendAudio(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await withMediaErrorHandler("sendDocument", runtime, () =>
|
||||
bot.api.sendDocument(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
await bot.api.sendDocument(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
}
|
||||
if (replyToId && !hasReplied) {
|
||||
hasReplied = true;
|
||||
|
||||
+26
-28
@@ -40,7 +40,7 @@ import { resolveModel } from "../agents/pi-embedded-runner/model.js";
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_TTS_MAX_LENGTH = 1500;
|
||||
const DEFAULT_TTS_SUMMARIZE = true;
|
||||
const DEFAULT_MAX_TEXT_LENGTH = 4096;
|
||||
const DEFAULT_MAX_TEXT_LENGTH = 4000;
|
||||
const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
|
||||
@@ -1386,34 +1386,32 @@ export async function maybeApplyTtsToPayload(params: {
|
||||
|
||||
if (textForAudio.length > maxLength) {
|
||||
if (!isSummarizationEnabled(prefsPath)) {
|
||||
// Truncate text when summarization is disabled
|
||||
logVerbose(
|
||||
`TTS: truncating long text (${textForAudio.length} > ${maxLength}), summarization disabled.`,
|
||||
`TTS: skipping long text (${textForAudio.length} > ${maxLength}), summarization disabled.`,
|
||||
);
|
||||
textForAudio = `${textForAudio.slice(0, maxLength - 3)}...`;
|
||||
} else {
|
||||
// Summarize text when enabled
|
||||
try {
|
||||
const summary = await summarizeText({
|
||||
text: textForAudio,
|
||||
targetLength: maxLength,
|
||||
cfg: params.cfg,
|
||||
config,
|
||||
timeoutMs: config.timeoutMs,
|
||||
});
|
||||
textForAudio = summary.summary;
|
||||
wasSummarized = true;
|
||||
if (textForAudio.length > config.maxTextLength) {
|
||||
logVerbose(
|
||||
`TTS: summary exceeded hard limit (${textForAudio.length} > ${config.maxTextLength}); truncating.`,
|
||||
);
|
||||
textForAudio = `${textForAudio.slice(0, config.maxTextLength - 3)}...`;
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logVerbose(`TTS: summarization failed, truncating instead: ${error.message}`);
|
||||
textForAudio = `${textForAudio.slice(0, maxLength - 3)}...`;
|
||||
return nextPayload;
|
||||
}
|
||||
|
||||
try {
|
||||
const summary = await summarizeText({
|
||||
text: textForAudio,
|
||||
targetLength: maxLength,
|
||||
cfg: params.cfg,
|
||||
config,
|
||||
timeoutMs: config.timeoutMs,
|
||||
});
|
||||
textForAudio = summary.summary;
|
||||
wasSummarized = true;
|
||||
if (textForAudio.length > config.maxTextLength) {
|
||||
logVerbose(
|
||||
`TTS: summary exceeded hard limit (${textForAudio.length} > ${config.maxTextLength}); truncating.`,
|
||||
);
|
||||
textForAudio = `${textForAudio.slice(0, config.maxTextLength - 3)}...`;
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logVerbose(`TTS: summarization failed: ${error.message}`);
|
||||
return nextPayload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1438,12 +1436,12 @@ export async function maybeApplyTtsToPayload(params: {
|
||||
|
||||
const channelId = resolveChannelId(params.channel);
|
||||
const shouldVoice = channelId === "telegram" && result.voiceCompatible === true;
|
||||
const finalPayload = {
|
||||
|
||||
return {
|
||||
...nextPayload,
|
||||
mediaUrl: result.audioPath,
|
||||
audioAsVoice: shouldVoice || params.payload.audioAsVoice,
|
||||
};
|
||||
return finalPayload;
|
||||
}
|
||||
|
||||
lastTtsAttempt = {
|
||||
|
||||
@@ -127,12 +127,6 @@ export function connectGateway(host: GatewayHost) {
|
||||
host.lastError = null;
|
||||
host.hello = hello;
|
||||
applySnapshot(host, hello);
|
||||
// Reset orphaned chat run state from before disconnect.
|
||||
// Any in-flight run's final event was lost during the disconnect window.
|
||||
host.chatRunId = null;
|
||||
(host as unknown as { chatStream: string | null }).chatStream = null;
|
||||
(host as unknown as { chatStreamStartedAt: number | null }).chatStreamStartedAt = null;
|
||||
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
||||
void loadAssistantIdentity(host as unknown as ClawdbotApp);
|
||||
void loadAgents(host as unknown as ClawdbotApp);
|
||||
void loadNodes(host as unknown as ClawdbotApp, { quiet: true });
|
||||
|
||||
Reference in new issue
Block a user