Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
557db51b8a | ||
|
|
41c98942d8 | ||
|
|
4c4ca12e08 | ||
|
|
92e794dc18 | ||
|
|
6375ee836f | ||
|
|
a6c97b5a48 | ||
|
|
5ea15ff7fe | ||
|
|
dd57483e5e | ||
|
|
3696aade09 | ||
|
|
426168a338 | ||
|
|
2f58d59f22 | ||
|
|
cbe19ad2f2 | ||
|
|
d57b88c7af | ||
|
|
ce89bc2b40 | ||
|
|
85b27fe5fe | ||
|
|
c147962434 | ||
|
|
72858a5311 | ||
|
|
21445cfc0a | ||
|
|
5ad203e47b | ||
|
|
3b53213b41 | ||
|
|
81c6ab0ec0 | ||
|
|
3ea887be5a | ||
|
|
c565de0f71 |
No files matched your search
+4
-1
@@ -8,10 +8,10 @@ Docs: https://docs.clawd.bot
|
||||
- Ollama: provider discovery + docs. (#1606) Thanks @abhaymundhara. https://docs.clawd.bot/providers/ollama
|
||||
|
||||
### Changes
|
||||
- Models: add Venice AI provider support (onboarding, non-interactive auth, docs). (#1666) Thanks @jonisjongithub. https://docs.clawd.bot/providers/venice
|
||||
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
|
||||
- Docs: add verbose installer troubleshooting guidance.
|
||||
- Docs: update Fly.io guide notes.
|
||||
- Tools: add link understanding with CLI preprocessing, SSRF guard, and structured link summaries. (#1637) Thanks @dasilva333.
|
||||
- Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.clawd.bot/bedrock
|
||||
- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/slash-commands
|
||||
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
|
||||
@@ -20,14 +20,17 @@ Docs: https://docs.clawd.bot
|
||||
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
||||
- Web UI: hide internal `message_id` hints in chat bubbles.
|
||||
- Heartbeat: normalize target identifiers for consistent routing.
|
||||
- TUI: reload history after gateway reconnect to restore session state. (#1663)
|
||||
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
|
||||
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
|
||||
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
||||
- Agents: use the active auth profile for auto-compaction recovery.
|
||||
- Models: default missing custom provider fields so minimal configs are accepted.
|
||||
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
||||
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
|
||||
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
|
||||
- Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
|
||||
- Google Chat: normalize space targets without double `spaces/` prefix.
|
||||
|
||||
## 2026.1.23-1
|
||||
|
||||
|
||||
@@ -477,30 +477,31 @@ Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and
|
||||
Thanks to all clawtributors:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
||||
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a>
|
||||
<a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a>
|
||||
<a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a>
|
||||
<a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a>
|
||||
<a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a>
|
||||
<a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a>
|
||||
<a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a>
|
||||
<a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a>
|
||||
<a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a>
|
||||
<a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a>
|
||||
<a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a>
|
||||
<a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a>
|
||||
<a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a>
|
||||
<a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a>
|
||||
<a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a>
|
||||
<a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
|
||||
<a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a>
|
||||
<a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a>
|
||||
<a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a>
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a>
|
||||
<a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a>
|
||||
<a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a>
|
||||
<a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a>
|
||||
<a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a>
|
||||
<a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a>
|
||||
<a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a>
|
||||
<a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a>
|
||||
<a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a>
|
||||
<a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a>
|
||||
<a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a>
|
||||
<a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a>
|
||||
<a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a>
|
||||
<a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a>
|
||||
<a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a>
|
||||
<a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a>
|
||||
<a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a>
|
||||
<a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a>
|
||||
<a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a> <a href="https://github.com/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a>
|
||||
<a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a>
|
||||
<a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a>
|
||||
<a href="https://github.com/dasilva333"><img src="https://avatars.githubusercontent.com/u/947827?v=4&s=48" width="48" height="48" alt="dasilva333" title="dasilva333"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a>
|
||||
<a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
|
||||
<a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a>
|
||||
<a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
|
||||
<a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a>
|
||||
<a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/sergical"><img src="https://avatars.githubusercontent.com/u/3760543?v=4&s=48" width="48" height="48" alt="sergical" title="sergical"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
|
||||
<a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a>
|
||||
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a>
|
||||
<a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||
|
||||
@@ -196,6 +196,7 @@ Provider options:
|
||||
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
|
||||
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`).
|
||||
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
|
||||
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on every newline and sends each line immediately during streaming.
|
||||
- `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8).
|
||||
- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
|
||||
- `channels.bluebubbles.dmHistoryLimit`: DM history limit.
|
||||
|
||||
@@ -295,6 +295,16 @@ Example (OpenAI‑compatible):
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- For custom providers, `reasoning`, `input`, `cost`, `contextWindow`, and `maxTokens` are optional.
|
||||
When omitted, Clawdbot defaults to:
|
||||
- `reasoning: false`
|
||||
- `input: ["text"]`
|
||||
- `cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }`
|
||||
- `contextWindow: 200000`
|
||||
- `maxTokens: 8192`
|
||||
- Recommended: set explicit values that match your proxy/model limits.
|
||||
|
||||
## CLI examples
|
||||
|
||||
```bash
|
||||
|
||||
+9
-1
@@ -780,6 +780,14 @@
|
||||
{
|
||||
"source": "/plugins",
|
||||
"destination": "/plugin"
|
||||
},
|
||||
{
|
||||
"source": "/install/railway",
|
||||
"destination": "/railway"
|
||||
},
|
||||
{
|
||||
"source": "/install/railway/",
|
||||
"destination": "/railway"
|
||||
}
|
||||
],
|
||||
"navigation": {
|
||||
@@ -818,6 +826,7 @@
|
||||
"install/ansible",
|
||||
"install/nix",
|
||||
"install/docker",
|
||||
"railway",
|
||||
"install/bun"
|
||||
]
|
||||
},
|
||||
@@ -1005,7 +1014,6 @@
|
||||
"plugins/zalouser",
|
||||
"tools/exec",
|
||||
"tools/web",
|
||||
"tools/link-understanding",
|
||||
"tools/apply-patch",
|
||||
"tools/elevated",
|
||||
"tools/browser",
|
||||
|
||||
@@ -2010,35 +2010,6 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
`tools.links` configures link understanding (URL preprocessing via CLI):
|
||||
- `tools.links.enabled`: enable/disable link understanding (default: enabled when models are configured).
|
||||
- `tools.links.maxLinks`: max URLs per message (default 3).
|
||||
- `tools.links.timeoutSeconds`: default CLI timeout (default 30).
|
||||
- `tools.links.scope`: optional gating rules (same shape as media understanding scope).
|
||||
- `tools.links.models`: ordered CLI entries (fallbacks in order).
|
||||
- `command`: executable to run (required).
|
||||
- `args`: templated args (supports `{{LinkUrl}}`).
|
||||
- `timeoutSeconds`: optional per-entry override.
|
||||
|
||||
Example:
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
enabled: true,
|
||||
maxLinks: 2,
|
||||
scope: {
|
||||
default: "deny",
|
||||
rules: [{ action: "allow", match: { chatType: "direct" } }]
|
||||
},
|
||||
models: [
|
||||
{ command: "link-understand", args: ["--url", "{{LinkUrl}}"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`agents.defaults.subagents` configures sub-agent defaults:
|
||||
- `model`: default model for spawned sub-agents (string or `{ primary, fallbacks }`). If omitted, sub-agents inherit the caller’s model unless overridden per agent or per call.
|
||||
- `maxConcurrent`: max concurrent sub-agent runs (default 1)
|
||||
@@ -3209,7 +3180,7 @@ clawdbot dns setup --apply
|
||||
|
||||
## Template variables
|
||||
|
||||
Template placeholders are expanded in `tools.media.*.models[].args`, `tools.media.models[].args`, and `tools.links.models[].args` (and any future templated argument fields).
|
||||
Template placeholders are expanded in `tools.media.*.models[].args` and `tools.media.models[].args` (and any future templated argument fields).
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
@@ -3224,7 +3195,6 @@ Template placeholders are expanded in `tools.media.*.models[].args`, `tools.medi
|
||||
| `{{MediaUrl}}` | Inbound media pseudo-URL (if present) |
|
||||
| `{{MediaPath}}` | Local media path (if downloaded) |
|
||||
| `{{MediaType}}` | Media type (image/audio/document/…) |
|
||||
| `{{LinkUrl}}` | URL currently being processed (link understanding only) |
|
||||
| `{{Transcript}}` | Audio transcript (when enabled) |
|
||||
| `{{Prompt}}` | Resolved media prompt for CLI entries |
|
||||
| `{{MaxChars}}` | Resolved max output chars for CLI entries |
|
||||
|
||||
@@ -31,6 +31,24 @@ See also: [Health checks](/gateway/health) and [Logging](/logging).
|
||||
|
||||
## Common Issues
|
||||
|
||||
### No API key found for provider "anthropic"
|
||||
|
||||
This means the **agent’s auth store is empty** or missing Anthropic credentials.
|
||||
Auth is **per agent**, so a new agent won’t inherit the main agent’s keys.
|
||||
|
||||
Fix options:
|
||||
- Re-run onboarding and choose **Anthropic** for that agent.
|
||||
- Or paste a setup-token on the **gateway host**:
|
||||
```bash
|
||||
clawdbot models auth setup-token --provider anthropic
|
||||
```
|
||||
- Or copy `auth-profiles.json` from the main agent dir to the new agent dir.
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
clawdbot models status
|
||||
```
|
||||
|
||||
### OAuth token refresh failed (Anthropic Claude subscription)
|
||||
|
||||
This means the stored Anthropic OAuth token expired and the refresh failed.
|
||||
|
||||
@@ -26,6 +26,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [The docs didn’t answer my question — how do I get a better answer?](#the-docs-didnt-answer-my-question--how-do-i-get-a-better-answer)
|
||||
- [How do I install Clawdbot on Linux?](#how-do-i-install-clawdbot-on-linux)
|
||||
- [How do I install Clawdbot on a VPS?](#how-do-i-install-clawdbot-on-a-vps)
|
||||
- [Where are the cloud/VPS install guides?](#where-are-the-cloudvps-install-guides)
|
||||
- [Can I ask Clawd to update itself?](#can-i-ask-clawd-to-update-itself)
|
||||
- [What does the onboarding wizard actually do?](#what-does-the-onboarding-wizard-actually-do)
|
||||
- [Do I need a Claude or OpenAI subscription to run this?](#do-i-need-a-claude-or-openai-subscription-to-run-this)
|
||||
@@ -437,6 +438,27 @@ Any Linux VPS works. Install on the server, then use SSH/Tailscale to reach the
|
||||
Guides: [exe.dev](/platforms/exe-dev), [Hetzner](/platforms/hetzner), [Fly.io](/platforms/fly).
|
||||
Remote access: [Gateway remote](/gateway/remote).
|
||||
|
||||
### Where are the cloud/VPS install guides?
|
||||
|
||||
We keep a **hosting hub** with the common providers. Pick one and follow the guide:
|
||||
|
||||
- [VPS hosting](/vps) (all providers in one place)
|
||||
- [Railway](/railway) (one‑click, browser‑based setup)
|
||||
- [Fly.io](/platforms/fly)
|
||||
- [Hetzner](/platforms/hetzner)
|
||||
- [exe.dev](/platforms/exe-dev)
|
||||
|
||||
How it works in the cloud: the **Gateway runs on the server**, and you access it
|
||||
from your laptop/phone via the Control UI (or Tailscale/SSH). Your state + workspace
|
||||
live on the server, so treat the host as the source of truth and back it up.
|
||||
|
||||
You can pair **nodes** (Mac/iOS/Android/headless) to that cloud Gateway to access
|
||||
local screen/camera/canvas or run commands on your laptop while keeping the
|
||||
Gateway in the cloud.
|
||||
|
||||
Hub: [Platforms](/platforms). Remote access: [Gateway remote](/gateway/remote).
|
||||
Nodes: [Nodes](/nodes), [Nodes CLI](/cli/nodes).
|
||||
|
||||
### Can I ask Clawd to update itself?
|
||||
|
||||
Short answer: **possible, not recommended**. The update flow can restart the
|
||||
|
||||
@@ -23,6 +23,8 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
||||
|
||||
## VPS & hosting
|
||||
|
||||
- VPS hub: [VPS hosting](/vps)
|
||||
- Railway (one-click): [Railway](/railway)
|
||||
- Fly.io: [Fly.io](/platforms/fly)
|
||||
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
||||
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
||||
|
||||
@@ -10,7 +10,13 @@ This flow lets the macOS app act as a full remote control for a Clawdbot gateway
|
||||
|
||||
## Modes
|
||||
- **Local (this Mac)**: Everything runs on the laptop. No SSH involved.
|
||||
- **Remote over SSH**: Clawdbot commands are executed on the remote host. The mac app opens an SSH connection with `-o BatchMode` plus your chosen identity/key.
|
||||
- **Remote over SSH (default)**: Clawdbot commands are executed on the remote host. The mac app opens an SSH connection with `-o BatchMode` plus your chosen identity/key and a local port-forward.
|
||||
- **Remote direct (ws/wss)**: No SSH tunnel. The mac app connects to the gateway URL directly (for example, via Tailscale Serve or a public HTTPS reverse proxy).
|
||||
|
||||
## Remote transports
|
||||
Remote mode supports two transports:
|
||||
- **SSH tunnel** (default): Uses `ssh -N -L ...` to forward the gateway port to localhost. The gateway will see the node’s IP as `127.0.0.1` because the tunnel is loopback.
|
||||
- **Direct (ws/wss)**: Connects straight to the gateway URL. The gateway sees the real client IP.
|
||||
|
||||
## Prereqs on the remote host
|
||||
1) Install Node + pnpm and build/install the Clawdbot CLI (`pnpm install && pnpm build && pnpm link --global`).
|
||||
@@ -20,16 +26,19 @@ This flow lets the macOS app act as a full remote control for a Clawdbot gateway
|
||||
## macOS app setup
|
||||
1) Open *Settings → General*.
|
||||
2) Under **Clawdbot runs**, pick **Remote over SSH** and set:
|
||||
- **Transport**: **SSH tunnel** or **Direct (ws/wss)**.
|
||||
- **SSH target**: `user@host` (optional `:port`).
|
||||
- If the gateway is on the same LAN and advertises Bonjour, pick it from the discovered list to auto-fill this field.
|
||||
- **Gateway URL** (Direct only): `wss://gateway.example.ts.net` (or `ws://...` for local/LAN).
|
||||
- **Identity file** (advanced): path to your key.
|
||||
- **Project root** (advanced): remote checkout path used for commands.
|
||||
- **CLI path** (advanced): optional path to a runnable `clawdbot` entrypoint/binary (auto-filled when advertised).
|
||||
3) Hit **Test remote**. Success indicates the remote `clawdbot status --json` runs correctly. Failures usually mean PATH/CLI issues; exit 127 means the CLI isn’t found remotely.
|
||||
4) Health checks and Web Chat will now run through this SSH tunnel automatically.
|
||||
|
||||
## Web Chat over SSH
|
||||
- Web Chat connects to the gateway over the forwarded WebSocket control port (default 18789).
|
||||
## Web Chat
|
||||
- **SSH tunnel**: Web Chat connects to the gateway over the forwarded WebSocket control port (default 18789).
|
||||
- **Direct (ws/wss)**: Web Chat connects straight to the configured gateway URL.
|
||||
- There is no separate WebChat HTTP server anymore.
|
||||
|
||||
## Permissions
|
||||
@@ -49,6 +58,7 @@ This flow lets the macOS app act as a full remote control for a Clawdbot gateway
|
||||
- **exit 127 / not found**: `clawdbot` isn’t on PATH for non-login shells. Add it to `/etc/paths`, your shell rc, or symlink into `/usr/local/bin`/`/opt/homebrew/bin`.
|
||||
- **Health probe failed**: check SSH reachability, PATH, and that Baileys is logged in (`clawdbot status --json`).
|
||||
- **Web Chat stuck**: confirm the gateway is running on the remote host and the forwarded port matches the gateway WS port; the UI requires a healthy WS connection.
|
||||
- **Node IP shows 127.0.0.1**: expected with the SSH tunnel. Switch **Transport** to **Direct (ws/wss)** if you want the gateway to see the real client IP.
|
||||
- **Voice Wake**: trigger phrases are forwarded automatically in remote mode; no separate forwarder is needed.
|
||||
|
||||
## Notification sounds
|
||||
|
||||
@@ -180,6 +180,9 @@ components can talk to a remote Gateway as if it were on localhost.
|
||||
or restarts it if needed.
|
||||
- **SSH shape:** `ssh -N -L <local>:127.0.0.1:<remote>` with BatchMode +
|
||||
ExitOnForwardFailure + keepalive options.
|
||||
- **IP reporting:** the SSH tunnel uses loopback, so the gateway will see the node
|
||||
IP as `127.0.0.1`. Use **Direct (ws/wss)** transport if you want the real client
|
||||
IP to appear (see [macOS remote access](/platforms/mac/remote)).
|
||||
|
||||
For setup steps, see [macOS remote access](/platforms/mac/remote). For protocol
|
||||
details, see [Gateway protocol](/gateway/protocol).
|
||||
|
||||
@@ -105,3 +105,26 @@ clawdbot onboard --auth-choice claude-cli
|
||||
accepts both OAuth and setup-token credentials. Older configs using `"token"` are
|
||||
auto-migrated on load.
|
||||
- Auth details + reuse rules are in [/concepts/oauth](/concepts/oauth).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**401 errors / token suddenly invalid**
|
||||
- Claude subscription auth can expire or be revoked. Re-run `claude setup-token`
|
||||
and paste it into the **gateway host**.
|
||||
- If the Claude CLI login lives on a different machine, use
|
||||
`clawdbot models auth paste-token --provider anthropic` on the gateway host.
|
||||
|
||||
**No API key found for provider "anthropic"**
|
||||
- Auth is **per agent**. New agents don’t inherit the main agent’s keys.
|
||||
- Re-run onboarding for that agent, or paste a setup-token / API key on the
|
||||
gateway host, then verify with `clawdbot models status`.
|
||||
|
||||
**No credentials found for profile `anthropic:default` or `anthropic:claude-cli`**
|
||||
- Run `clawdbot models status` to see which auth profile is active.
|
||||
- Re-run onboarding, or paste a setup-token / API key for that profile.
|
||||
|
||||
**No available auth profile (all in cooldown/unavailable)**
|
||||
- Check `clawdbot models status --json` for `auth.unusableProfiles`.
|
||||
- Add another Anthropic profile or wait for cooldown.
|
||||
|
||||
More: [/gateway/troubleshooting](/gateway/troubleshooting) and [/help/faq](/help/faq).
|
||||
@@ -35,6 +35,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
||||
- [Z.AI](/providers/zai)
|
||||
- [GLM models](/providers/glm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Venice AI (privacy-focused)](/providers/venice)
|
||||
- [Ollama (local models)](/providers/ollama)
|
||||
|
||||
## Transcription providers
|
||||
|
||||
@@ -215,5 +215,5 @@ ollama serve
|
||||
## See Also
|
||||
|
||||
- [Model Providers](/concepts/model-providers) - Overview of all providers
|
||||
- [Model Selection](/agents/model-selection) - How to choose models
|
||||
- [Configuration](/configuration) - Full config reference
|
||||
- [Model Selection](/concepts/models) - How to choose models
|
||||
- [Configuration](/gateway/configuration) - Full config reference
|
||||
@@ -0,0 +1,252 @@
|
||||
---
|
||||
summary: "Use Venice AI privacy-focused models in Clawdbot"
|
||||
read_when:
|
||||
- You want privacy-focused inference in Clawdbot
|
||||
- You want Venice AI setup guidance
|
||||
---
|
||||
# Venice AI Provider
|
||||
|
||||
Venice AI provides privacy-focused AI inference with support for uncensored models and access to major proprietary models through their anonymized proxy. All inference is private by default—no training on your data, no logging.
|
||||
|
||||
## Privacy Modes
|
||||
|
||||
Venice offers two privacy levels — understanding this is key to choosing your model:
|
||||
|
||||
| Mode | Description | Models |
|
||||
|------|-------------|--------|
|
||||
| **Private** | Fully private. Prompts/responses are **never stored or logged**. Ephemeral. | Llama, Qwen, DeepSeek, Venice Uncensored, etc. |
|
||||
| **Anonymized** | Proxied through Venice with metadata stripped. The underlying provider (OpenAI, Anthropic) sees anonymized requests. | Claude, GPT, Gemini, Grok, Kimi, MiniMax |
|
||||
|
||||
## Features
|
||||
|
||||
- **Privacy-focused**: Choose between "private" (fully private) and "anonymized" (proxied) modes
|
||||
- **Uncensored models**: Access to models without content restrictions
|
||||
- **Major model access**: Use Claude, GPT-5.2, Gemini, Grok via Venice's anonymized proxy
|
||||
- **OpenAI-compatible API**: Standard `/v1` endpoints for easy integration
|
||||
- **Streaming**: ✅ Supported on all models
|
||||
- **Function calling**: ✅ Supported on select models (check model capabilities)
|
||||
- **Vision**: ✅ Supported on models with vision capability
|
||||
- **No hard rate limits**: Fair-use throttling may apply for extreme usage
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Get API Key
|
||||
|
||||
1. Sign up at [venice.ai](https://venice.ai)
|
||||
2. Go to **Settings → API Keys → Create new key**
|
||||
3. Copy your API key (format: `vapi_xxxxxxxxxxxx`)
|
||||
|
||||
### 2. Configure Clawdbot
|
||||
|
||||
**Option A: Environment Variable**
|
||||
|
||||
```bash
|
||||
export VENICE_API_KEY="vapi_xxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
**Option B: Interactive Setup (Recommended)**
|
||||
|
||||
```bash
|
||||
clawdbot onboard --auth-choice venice-api-key
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Prompt for your API key (or use existing `VENICE_API_KEY`)
|
||||
2. Show all available Venice models
|
||||
3. Let you pick your default model
|
||||
4. Configure the provider automatically
|
||||
|
||||
**Option C: Non-interactive**
|
||||
|
||||
```bash
|
||||
clawdbot onboard --non-interactive \
|
||||
--auth-choice venice-api-key \
|
||||
--venice-api-key "vapi_xxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
### 3. Verify Setup
|
||||
|
||||
```bash
|
||||
clawdbot chat --model venice/llama-3.3-70b "Hello, are you working?"
|
||||
```
|
||||
|
||||
## Model Selection
|
||||
|
||||
After setup, Clawdbot shows all available Venice models. Pick based on your needs:
|
||||
|
||||
- **Privacy**: Choose "private" models for fully private inference
|
||||
- **Capability**: Choose "anonymized" models to access Claude, GPT, Gemini via Venice's proxy
|
||||
|
||||
Change your default model anytime:
|
||||
|
||||
```bash
|
||||
clawdbot models set venice/claude-opus-45
|
||||
clawdbot models set venice/llama-3.3-70b
|
||||
```
|
||||
|
||||
List all available models:
|
||||
|
||||
```bash
|
||||
clawdbot models list | grep venice
|
||||
```
|
||||
|
||||
## Configure via `clawdbot configure`
|
||||
|
||||
1. Run `clawdbot configure`
|
||||
2. Select **Model/auth**
|
||||
3. Choose **Venice AI**
|
||||
|
||||
## Which Model Should I Use?
|
||||
|
||||
| Use Case | Recommended Model | Why |
|
||||
|----------|-------------------|-----|
|
||||
| **General chat** | `llama-3.3-70b` | Good all-around, fully private |
|
||||
| **Privacy + Claude quality** | `claude-opus-45` | Best reasoning via anonymized proxy |
|
||||
| **Coding** | `qwen3-coder-480b-a35b-instruct` | Code-optimized, 262k context |
|
||||
| **Vision tasks** | `qwen3-vl-235b-a22b` | Best private vision model |
|
||||
| **Uncensored** | `venice-uncensored` | No content restrictions |
|
||||
| **Fast + cheap** | `qwen3-4b` | Lightweight, still capable |
|
||||
| **Complex reasoning** | `deepseek-v3.2` | Strong reasoning, private |
|
||||
|
||||
## Available Models (25 Total)
|
||||
|
||||
### Private Models (15) — Fully Private, No Logging
|
||||
|
||||
| Model ID | Name | Context (tokens) | Features |
|
||||
|----------|------|------------------|----------|
|
||||
| `llama-3.3-70b` | Llama 3.3 70B | 131k | General |
|
||||
| `llama-3.2-3b` | Llama 3.2 3B | 131k | Fast, lightweight |
|
||||
| `hermes-3-llama-3.1-405b` | Hermes 3 Llama 3.1 405B | 131k | Complex tasks |
|
||||
| `qwen3-235b-a22b-thinking-2507` | Qwen3 235B Thinking | 131k | Reasoning |
|
||||
| `qwen3-235b-a22b-instruct-2507` | Qwen3 235B Instruct | 131k | General |
|
||||
| `qwen3-coder-480b-a35b-instruct` | Qwen3 Coder 480B | 262k | Code |
|
||||
| `qwen3-next-80b` | Qwen3 Next 80B | 262k | General |
|
||||
| `qwen3-vl-235b-a22b` | Qwen3 VL 235B | 262k | Vision |
|
||||
| `qwen3-4b` | Venice Small (Qwen3 4B) | 32k | Fast, reasoning |
|
||||
| `deepseek-v3.2` | DeepSeek V3.2 | 163k | Reasoning |
|
||||
| `venice-uncensored` | Venice Uncensored | 32k | Uncensored |
|
||||
| `mistral-31-24b` | Venice Medium (Mistral) | 131k | Vision |
|
||||
| `google-gemma-3-27b-it` | Gemma 3 27B Instruct | 202k | Vision |
|
||||
| `openai-gpt-oss-120b` | OpenAI GPT OSS 120B | 131k | General |
|
||||
| `zai-org-glm-4.7` | GLM 4.7 | 202k | Reasoning, multilingual |
|
||||
|
||||
### Anonymized Models (10) — Via Venice Proxy
|
||||
|
||||
| Model ID | Original | Context (tokens) | Features |
|
||||
|----------|----------|------------------|----------|
|
||||
| `claude-opus-45` | Claude Opus 4.5 | 202k | Reasoning, vision |
|
||||
| `claude-sonnet-45` | Claude Sonnet 4.5 | 202k | Reasoning, vision |
|
||||
| `openai-gpt-52` | GPT-5.2 | 262k | Reasoning |
|
||||
| `openai-gpt-52-codex` | GPT-5.2 Codex | 262k | Reasoning, vision |
|
||||
| `gemini-3-pro-preview` | Gemini 3 Pro | 202k | Reasoning, vision |
|
||||
| `gemini-3-flash-preview` | Gemini 3 Flash | 262k | Reasoning, vision |
|
||||
| `grok-41-fast` | Grok 4.1 Fast | 262k | Reasoning, vision |
|
||||
| `grok-code-fast-1` | Grok Code Fast 1 | 262k | Reasoning, code |
|
||||
| `kimi-k2-thinking` | Kimi K2 Thinking | 262k | Reasoning |
|
||||
| `minimax-m21` | MiniMax M2.1 | 202k | Reasoning |
|
||||
|
||||
## Model Discovery
|
||||
|
||||
Clawdbot automatically discovers models from the Venice API when `VENICE_API_KEY` is set. If the API is unreachable, it falls back to a static catalog.
|
||||
|
||||
The `/models` endpoint is public (no auth needed for listing), but inference requires a valid API key.
|
||||
|
||||
## Streaming & Tool Support
|
||||
|
||||
| Feature | Support |
|
||||
|---------|---------|
|
||||
| **Streaming** | ✅ All models |
|
||||
| **Function calling** | ✅ Most models (check `supportsFunctionCalling` in API) |
|
||||
| **Vision/Images** | ✅ Models marked with "Vision" feature |
|
||||
| **JSON mode** | ✅ Supported via `response_format` |
|
||||
|
||||
## Pricing
|
||||
|
||||
Venice uses a credit-based system. Check [venice.ai/pricing](https://venice.ai/pricing) for current rates:
|
||||
|
||||
- **Private models**: Generally lower cost
|
||||
- **Anonymized models**: Similar to direct API pricing + small Venice fee
|
||||
|
||||
## Comparison: Venice vs Direct API
|
||||
|
||||
| Aspect | Venice (Anonymized) | Direct API |
|
||||
|--------|---------------------|------------|
|
||||
| **Privacy** | Metadata stripped, anonymized | Your account linked |
|
||||
| **Latency** | +10-50ms (proxy) | Direct |
|
||||
| **Features** | Most features supported | Full features |
|
||||
| **Billing** | Venice credits | Provider billing |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Use default private model
|
||||
clawdbot chat --model venice/llama-3.3-70b
|
||||
|
||||
# Use Claude via Venice (anonymized)
|
||||
clawdbot chat --model venice/claude-opus-45
|
||||
|
||||
# Use uncensored model
|
||||
clawdbot chat --model venice/venice-uncensored
|
||||
|
||||
# Use vision model with image
|
||||
clawdbot chat --model venice/qwen3-vl-235b-a22b
|
||||
|
||||
# Use coding model
|
||||
clawdbot chat --model venice/qwen3-coder-480b-a35b-instruct
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### API key not recognized
|
||||
|
||||
```bash
|
||||
echo $VENICE_API_KEY
|
||||
clawdbot models list | grep venice
|
||||
```
|
||||
|
||||
Ensure the key starts with `vapi_`.
|
||||
|
||||
### Model not available
|
||||
|
||||
The Venice model catalog updates dynamically. Run `clawdbot models list` to see currently available models. Some models may be temporarily offline.
|
||||
|
||||
### Connection issues
|
||||
|
||||
Venice API is at `https://api.venice.ai/api/v1`. Ensure your network allows HTTPS connections.
|
||||
|
||||
## Config file example
|
||||
|
||||
```json5
|
||||
{
|
||||
env: { VENICE_API_KEY: "vapi_..." },
|
||||
agents: { defaults: { model: { primary: "venice/llama-3.3-70b" } } },
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
venice: {
|
||||
baseUrl: "https://api.venice.ai/api/v1",
|
||||
apiKey: "${VENICE_API_KEY}",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "llama-3.3-70b",
|
||||
name: "Llama 3.3 70B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Venice AI](https://venice.ai)
|
||||
- [API Documentation](https://docs.venice.ai)
|
||||
- [Pricing](https://venice.ai/pricing)
|
||||
- [Status](https://status.venice.ai)
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Deploy on Railway
|
||||
---
|
||||
|
||||
Deploy Clawdbot on Railway with a one-click template and finish setup in your browser.
|
||||
This is the easiest “no terminal on the server” path: Railway runs the Gateway for you,
|
||||
and you configure everything via the `/setup` web wizard.
|
||||
|
||||
## Quick checklist (new users)
|
||||
|
||||
1) Click **Deploy on Railway** (below).
|
||||
2) Add a **Volume** mounted at `/data`.
|
||||
3) Set the required **Variables** (at least `SETUP_PASSWORD`).
|
||||
4) Enable **HTTP Proxy** on port `8080`.
|
||||
5) Open `https://<your-railway-domain>/setup` and finish the wizard.
|
||||
|
||||
## One-click deploy
|
||||
|
||||
<a href="https://railway.com/deploy/clawdbot-railway-template" target="_blank" rel="noreferrer">Deploy on Railway</a>
|
||||
|
||||
After deploy, find your public URL in **Railway → your service → Settings → Domains**.
|
||||
|
||||
Railway will either:
|
||||
- give you a generated domain (often `https://<something>.up.railway.app`), or
|
||||
- use your custom domain if you attached one.
|
||||
|
||||
Then open:
|
||||
|
||||
- `https://<your-railway-domain>/setup` — setup wizard (password protected)
|
||||
- `https://<your-railway-domain>/clawdbot` — Control UI
|
||||
|
||||
## What you get
|
||||
|
||||
- Hosted Clawdbot Gateway + Control UI
|
||||
- Web setup wizard at `/setup` (no terminal commands)
|
||||
- Persistent storage via Railway Volume (`/data`) so config/credentials/workspace survive redeploys
|
||||
- Backup export at `/setup/export` to migrate off Railway later
|
||||
|
||||
## Required Railway settings
|
||||
|
||||
### Public Networking
|
||||
|
||||
Enable **HTTP Proxy** for the service.
|
||||
|
||||
- Port: `8080`
|
||||
|
||||
### Volume (required)
|
||||
|
||||
Attach a volume mounted at:
|
||||
|
||||
- `/data`
|
||||
|
||||
### Variables
|
||||
|
||||
Set these variables on the service:
|
||||
|
||||
- `SETUP_PASSWORD` (required)
|
||||
- `CLAWDBOT_STATE_DIR=/data/.clawdbot` (recommended)
|
||||
- `CLAWDBOT_WORKSPACE_DIR=/data/workspace` (recommended)
|
||||
- `CLAWDBOT_GATEWAY_TOKEN` (recommended; treat as an admin secret)
|
||||
|
||||
## Setup flow
|
||||
|
||||
1) Visit `https://<your-railway-domain>/setup` and enter your `SETUP_PASSWORD`.
|
||||
2) Choose a model/auth provider and paste your key.
|
||||
3) (Optional) Add Telegram/Discord/Slack tokens.
|
||||
4) Click **Run setup**.
|
||||
|
||||
If Telegram DMs are set to pairing, the setup wizard can approve the pairing code.
|
||||
|
||||
## Getting chat tokens
|
||||
|
||||
### Telegram bot token
|
||||
|
||||
1) Message `@BotFather` in Telegram
|
||||
2) Run `/newbot`
|
||||
3) Copy the token (looks like `123456789:AA...`)
|
||||
4) Paste it into `/setup`
|
||||
|
||||
### Discord bot token
|
||||
|
||||
1) Go to https://discord.com/developers/applications
|
||||
2) **New Application** → choose a name
|
||||
3) **Bot** → **Add Bot**
|
||||
4) Copy the **Bot Token** and paste into `/setup`
|
||||
5) Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`)
|
||||
|
||||
## Backups & migration
|
||||
|
||||
Download a backup at:
|
||||
|
||||
- `https://<your-railway-domain>/setup/export`
|
||||
|
||||
This exports your Clawdbot state + workspace so you can migrate to another host without losing config or memory.
|
||||
@@ -232,13 +232,6 @@ Notes:
|
||||
- See [Web tools](/tools/web) for setup.
|
||||
- See [Firecrawl](/tools/firecrawl) for the optional anti-bot fallback.
|
||||
|
||||
### Link understanding (inbound)
|
||||
Preprocess inbound messages by running a CLI on detected URLs and appending the
|
||||
results to the message body before the agent sees it. This is **not** an agent
|
||||
tool; it runs in the inbound auto-reply pipeline.
|
||||
|
||||
See [Link understanding](/tools/link-understanding) for setup and configuration.
|
||||
|
||||
### `browser`
|
||||
Control the dedicated clawd browser.
|
||||
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
---
|
||||
summary: "Run a CLI on URLs in inbound messages and inject results before the agent replies"
|
||||
read_when:
|
||||
- You want to summarize or enrich URLs in inbound messages
|
||||
- You need to enable or scope link understanding
|
||||
- You want to wire a custom CLI to preprocess links
|
||||
---
|
||||
|
||||
# Link understanding
|
||||
|
||||
Link understanding is the **inbound URL enrichment pipeline**: Clawdbot detects
|
||||
links in a message, runs your **local CLI** to interpret them, and injects the
|
||||
results into the message body **before the agent sees it**. The goal is a
|
||||
predictable, structured prompt envelope without adding a browsing tool.
|
||||
|
||||
This is **not** an agent tool. It is a preprocessing step in the auto-reply
|
||||
pipeline, shared across all channels (including webchat).
|
||||
|
||||
## What it enables
|
||||
|
||||
- Summarize or extract data from links (docs, tickets, dashboards, runbooks).
|
||||
- Inject structured context (titles, owners, status, timestamps, key facts).
|
||||
- Normalize or transform link payloads (HTML → text, JSON → bullets).
|
||||
- Gate link processing by channel, chat type, or session key.
|
||||
- Apply SSRF protections before your CLI runs.
|
||||
|
||||
## When it runs (pipeline order)
|
||||
|
||||
Link understanding runs after inbound normalization and media understanding,
|
||||
but before the agent executes the reply run:
|
||||
|
||||
1) Inbound message is normalized and routed.
|
||||
2) Media understanding may rewrite the body (images/audio/video).
|
||||
3) Link understanding detects URLs in the current command/body and appends results.
|
||||
4) The agent receives the final body and command parsing uses the updated text.
|
||||
|
||||
## How it works
|
||||
|
||||
1) Clawdbot scans the inbound message for **bare** `http/https` URLs.
|
||||
2) It dedupes links in order and caps them at `tools.links.maxLinks` (default 3).
|
||||
3) For each link, it runs your configured CLI entries **in order** until one returns non-empty stdout.
|
||||
4) It appends the resulting outputs to the message body (structured envelope).
|
||||
5) The agent sees the original text plus the appended link outputs.
|
||||
|
||||
No requests are made by Clawdbot itself; **your CLI does the fetching/parsing**.
|
||||
|
||||
## URL detection rules
|
||||
|
||||
- Only **bare URLs** are extracted (e.g. `https://example.com`).
|
||||
- Markdown links are ignored (`[label](https://example.com)` is stripped).
|
||||
- Links are deduped (first occurrence wins).
|
||||
- Only `http` and `https` are allowed.
|
||||
- Local and private hosts are blocked (see **Security** below).
|
||||
|
||||
## Configuration
|
||||
|
||||
Link understanding is configured under `tools.links`:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
enabled: true,
|
||||
maxLinks: 3,
|
||||
timeoutSeconds: 30,
|
||||
scope: {
|
||||
default: "allow",
|
||||
rules: [{ action: "deny", match: { chatType: "group" } }]
|
||||
},
|
||||
models: [
|
||||
{
|
||||
command: "link-understand",
|
||||
args: ["--url", "{{LinkUrl}}", "--format", "markdown"],
|
||||
timeoutSeconds: 20
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `tools.links` fields
|
||||
|
||||
- `enabled`: enable/disable link understanding (default: enabled when models are configured).
|
||||
- `maxLinks`: max URLs processed per message (default 3).
|
||||
- `timeoutSeconds`: default timeout for CLI runs (default 30).
|
||||
- `scope`: optional gating rules (same structure as media understanding scope).
|
||||
- `models`: ordered list of CLI entries (fallbacks in order).
|
||||
|
||||
### Model entries (`tools.links.models[]`)
|
||||
|
||||
Each entry is a CLI command:
|
||||
|
||||
- `type`: optional, only `"cli"` is supported (default).
|
||||
- `command`: executable to run (required).
|
||||
- `args`: CLI args (templated; see **Template variables**).
|
||||
- `timeoutSeconds`: optional override for this entry.
|
||||
|
||||
The first entry that returns **non-empty stdout** wins. If a command fails or
|
||||
returns empty output, Clawdbot tries the next entry.
|
||||
|
||||
## Performance + ordering
|
||||
|
||||
- Links are processed **sequentially** (per message, in order).
|
||||
- Each CLI entry has a timeout (per entry or default).
|
||||
- Failure to run a CLI does **not** abort the agent run; it just tries the next entry.
|
||||
- `maxLinks` is the primary cap for latency. Keep it low for chatty channels.
|
||||
|
||||
## Template variables
|
||||
|
||||
`tools.links.models[].args` supports the standard template variables plus `{{LinkUrl}}`.
|
||||
See [Template variables](/gateway/configuration#template-variables) for the full list.
|
||||
|
||||
Link-specific variable:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `{{LinkUrl}}` | URL currently being processed |
|
||||
|
||||
## Output format
|
||||
|
||||
Each link is wrapped in a predictable envelope and appended to the message body:
|
||||
|
||||
```
|
||||
<original message>
|
||||
|
||||
[Link]
|
||||
URL: https://example.com
|
||||
Source: link-understand
|
||||
Summary:
|
||||
<cli output>
|
||||
```
|
||||
|
||||
When multiple links are present, the header is numbered (`[Link 1/2]`, `[Link 2/2]`).
|
||||
`Source` is the CLI `command` that produced the output.
|
||||
|
||||
### Output best practices
|
||||
|
||||
- Keep summaries short and stable (avoid prompt bloat).
|
||||
- Prefer a compact, consistent structure (title → bullets → status).
|
||||
- Avoid surrounding fences unless you want the agent to treat it as literal text.
|
||||
|
||||
|
||||
## Scope gating
|
||||
|
||||
`tools.links.scope` uses the same rules as media understanding:
|
||||
|
||||
- `default`: `"allow"` or `"deny"`.
|
||||
- `rules[]`: first match wins.
|
||||
- `match.channel` (surface/channel)
|
||||
- `match.chatType` (`direct`, `group`, `channel`)
|
||||
- `match.keyPrefix` (session key prefix)
|
||||
|
||||
Example (only allow direct messages):
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
scope: {
|
||||
default: "deny",
|
||||
rules: [{ action: "allow", match: { chatType: "direct" } }]
|
||||
},
|
||||
models: [{ command: "link-understand", args: ["--url", "{{LinkUrl}}"] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security (SSRF guard)
|
||||
|
||||
Clawdbot blocks local/private hosts before invoking your CLI. The guard rejects:
|
||||
|
||||
- `localhost`, `*.localhost`, `*.local`, `*.internal`
|
||||
- Loopback addresses (IPv4 + IPv6)
|
||||
- Private and link-local IP ranges (IPv4 + IPv6)
|
||||
- `metadata.google.internal`
|
||||
- Addresses that resolve to private/internal IPs
|
||||
|
||||
If a URL is blocked, it is **skipped** and your CLI is not invoked for it.
|
||||
URLs that fail DNS resolution are also skipped.
|
||||
|
||||
## Decision tracking
|
||||
|
||||
Each run records a decision payload (useful in logs/debugging) in `ctx.LinkUnderstandingDecisions`:
|
||||
|
||||
```json5
|
||||
{
|
||||
outcome: "success" | "skipped" | "disabled" | "scope-deny" | "no-links",
|
||||
urls: [
|
||||
{
|
||||
url: "https://example.com",
|
||||
attempts: [
|
||||
{ type: "cli", command: "link-understand", outcome: "success" }
|
||||
],
|
||||
chosen: { type: "cli", command: "link-understand", outcome: "success" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Custom usage patterns
|
||||
|
||||
### Ticket summarizer (single link)
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
maxLinks: 1,
|
||||
models: [
|
||||
{
|
||||
command: "ticket-summary",
|
||||
args: ["--url", "{{LinkUrl}}", "--format", "brief"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Per-channel allowlist (only Discord + Slack)
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
scope: {
|
||||
default: "deny",
|
||||
rules: [
|
||||
{ action: "allow", match: { channel: "discord" } },
|
||||
{ action: "allow", match: { channel: "slack" } }
|
||||
]
|
||||
},
|
||||
models: [{ command: "link-understand", args: ["--url", "{{LinkUrl}}"] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Per-agent override (support agent only)
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "support",
|
||||
tools: {
|
||||
links: {
|
||||
enabled: true,
|
||||
maxLinks: 2,
|
||||
models: [{ command: "link-understand", args: ["--url", "{{LinkUrl}}"] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Markdown link behavior (ignored)
|
||||
|
||||
Message:
|
||||
```
|
||||
Please check [our docs](https://docs.example.com) and https://status.example.com
|
||||
```
|
||||
|
||||
Only `https://status.example.com` is processed (markdown links are stripped).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Nothing happens:** ensure `tools.links.models` is set and `enabled` is not `false`.
|
||||
- **No links detected:** only bare URLs are extracted; markdown links are ignored.
|
||||
- **Output missing:** your CLI returned empty stdout; try logging or return a default line.
|
||||
- **Wrong channel scope:** check `tools.links.scope` rules and `match.chatType`.
|
||||
- **SSRF blocked:** local/private URLs are skipped by design.
|
||||
|
||||
Enable verbose logs to see CLI execution and scope decisions:
|
||||
|
||||
```bash
|
||||
clawdbot gateway run --verbose
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal CLI runner
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
models: [{ command: "link-understand", args: ["--url", "{{LinkUrl}}"] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Limit to 1 link, custom timeout
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
maxLinks: 1,
|
||||
timeoutSeconds: 10,
|
||||
models: [{ command: "link-understand", args: ["--url", "{{LinkUrl}}"] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fallback chain (first non-empty output wins)
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
links: {
|
||||
models: [
|
||||
{ command: "link-understand", args: ["--url", "{{LinkUrl}}"] },
|
||||
{ command: "link-backup", args: ["{{LinkUrl}}"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
summary: "VPS hosting hub for Clawdbot (Railway/Fly/Hetzner/exe.dev)"
|
||||
read_when:
|
||||
- You want to run the Gateway in the cloud
|
||||
- You need a quick map of VPS/hosting guides
|
||||
---
|
||||
# VPS hosting
|
||||
|
||||
This hub links to the supported VPS/hosting guides and explains how cloud
|
||||
deployments work at a high level.
|
||||
|
||||
## Pick a provider
|
||||
|
||||
- **Railway** (one‑click + browser setup): [Railway](/railway)
|
||||
- **Fly.io**: [Fly.io](/platforms/fly)
|
||||
- **Hetzner (Docker)**: [Hetzner](/platforms/hetzner)
|
||||
- **exe.dev** (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
||||
- **AWS (EC2/Lightsail/free tier)**: works well too. Video guide:
|
||||
https://x.com/techfrenAJ/status/2014934471095812547
|
||||
|
||||
## How cloud setups work
|
||||
|
||||
- The **Gateway runs on the VPS** and owns state + workspace.
|
||||
- You connect from your laptop/phone via the **Control UI** or **Tailscale/SSH**.
|
||||
- Treat the VPS as the source of truth and **back up** the state + workspace.
|
||||
|
||||
Remote access: [Gateway remote](/gateway/remote)
|
||||
Platforms hub: [Platforms](/platforms)
|
||||
|
||||
## Using nodes with a VPS
|
||||
|
||||
You can keep the Gateway in the cloud and pair **nodes** on your local devices
|
||||
(Mac/iOS/Android/headless). Nodes provide local screen/camera/canvas and `system.run`
|
||||
capabilities while the Gateway stays in the cloud.
|
||||
|
||||
Docs: [Nodes](/nodes), [Nodes CLI](/cli/nodes)
|
||||
@@ -47,7 +47,8 @@ function mergeBlueBubblesAccountConfig(
|
||||
};
|
||||
const { accounts: _ignored, ...rest } = base;
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
return { ...rest, ...account };
|
||||
const chunkMode = account.chunkMode ?? rest.chunkMode ?? "length";
|
||||
return { ...rest, ...account, chunkMode };
|
||||
}
|
||||
|
||||
export function resolveBlueBubblesAccount(params: {
|
||||
|
||||
@@ -38,6 +38,7 @@ const bluebubblesAccountSchema = z.object({
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
mediaMaxMb: z.number().int().positive().optional(),
|
||||
sendReadReceipts: z.boolean().optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
|
||||
@@ -1851,16 +1851,21 @@ async function processMessage(
|
||||
account.config.textChunkLimit && account.config.textChunkLimit > 0
|
||||
? account.config.textChunkLimit
|
||||
: DEFAULT_TEXT_LIMIT;
|
||||
const chunkMode = account.config.chunkMode ?? "length";
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
const chunks =
|
||||
chunkMode === "newline"
|
||||
? core.channel.text.chunkTextWithMode(text, textLimit, chunkMode)
|
||||
: core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
if (!chunks.length) return;
|
||||
for (const chunk of chunks) {
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
@@ -1869,6 +1874,17 @@ async function processMessage(
|
||||
maybeEnqueueOutboundMessageId(result.messageId, chunk);
|
||||
sentMessage = true;
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
// In newline mode, restart typing after each chunk if more chunks remain
|
||||
// Small delay allows the Apple API to finish clearing the typing state from message send
|
||||
if (chunkMode === "newline" && i < chunks.length - 1 && chatGuidForActions) {
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
sendBlueBubblesTyping(chatGuidForActions, true, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
}).catch(() => {
|
||||
// Ignore typing errors
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onReplyStart: async () => {
|
||||
|
||||
@@ -38,6 +38,8 @@ export type BlueBubblesAccountConfig = {
|
||||
dms?: Record<string, unknown>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "newline" (default) splits on every newline; "length" splits by size. */
|
||||
chunkMode?: "length" | "newline";
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: Record<string, unknown>;
|
||||
|
||||
@@ -6,8 +6,8 @@ export function normalizeGoogleChatTarget(raw?: string | null): string | undefin
|
||||
if (!trimmed) return undefined;
|
||||
const withoutPrefix = trimmed.replace(/^(googlechat|google-chat|gchat):/i, "");
|
||||
const normalized = withoutPrefix
|
||||
.replace(/^user:/i, "users/")
|
||||
.replace(/^space:/i, "spaces/");
|
||||
.replace(/^user:(users\/)?/i, "users/")
|
||||
.replace(/^space:(spaces\/)?/i, "spaces/");
|
||||
if (isGoogleChatUserTarget(normalized)) {
|
||||
const suffix = normalized.slice("users/".length);
|
||||
return suffix.includes("@") ? `users/${suffix.toLowerCase()}` : normalized;
|
||||
|
||||
Generated
+625
-180
File diff suppressed because it is too large.
Load diff
@@ -282,6 +282,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
||||
"kimi-code": "KIMICODE_API_KEY",
|
||||
minimax: "MINIMAX_API_KEY",
|
||||
synthetic: "SYNTHETIC_API_KEY",
|
||||
venice: "VENICE_API_KEY",
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
opencode: "OPENCODE_API_KEY",
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SYNTHETIC_BASE_URL,
|
||||
SYNTHETIC_MODEL_CATALOG,
|
||||
} from "./synthetic-models.js";
|
||||
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
|
||||
|
||||
type ModelsConfig = NonNullable<ClawdbotConfig["models"]>;
|
||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||
@@ -340,6 +341,15 @@ function buildSyntheticProvider(): ProviderConfig {
|
||||
};
|
||||
}
|
||||
|
||||
async function buildVeniceProvider(): Promise<ProviderConfig> {
|
||||
const models = await discoverVeniceModels();
|
||||
return {
|
||||
baseUrl: VENICE_BASE_URL,
|
||||
api: "openai-completions",
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildOllamaProvider(): Promise<ProviderConfig> {
|
||||
const models = await discoverOllamaModels();
|
||||
return {
|
||||
@@ -385,6 +395,13 @@ export async function resolveImplicitProviders(params: {
|
||||
providers.synthetic = { ...buildSyntheticProvider(), apiKey: syntheticKey };
|
||||
}
|
||||
|
||||
const veniceKey =
|
||||
resolveEnvApiKeyVarName("venice") ??
|
||||
resolveApiKeyFromProfiles({ provider: "venice", store: authStore });
|
||||
if (veniceKey) {
|
||||
providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey };
|
||||
}
|
||||
|
||||
const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal");
|
||||
if (qwenProfiles.length > 0) {
|
||||
providers["qwen-portal"] = {
|
||||
|
||||
@@ -51,12 +51,14 @@ describe("models-config", () => {
|
||||
const previousMinimax = process.env.MINIMAX_API_KEY;
|
||||
const previousMoonshot = process.env.MOONSHOT_API_KEY;
|
||||
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
||||
const previousVenice = process.env.VENICE_API_KEY;
|
||||
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||
delete process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MOONSHOT_API_KEY;
|
||||
delete process.env.SYNTHETIC_API_KEY;
|
||||
delete process.env.VENICE_API_KEY;
|
||||
|
||||
try {
|
||||
vi.resetModules();
|
||||
@@ -85,6 +87,8 @@ describe("models-config", () => {
|
||||
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
||||
if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY;
|
||||
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
||||
if (previousVenice === undefined) delete process.env.VENICE_API_KEY;
|
||||
else process.env.VENICE_API_KEY = previousVenice;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -172,4 +176,42 @@ describe("models-config", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("adds venice provider when VENICE_API_KEY is set", async () => {
|
||||
await withTempHome(async () => {
|
||||
vi.resetModules();
|
||||
const prevKey = process.env.VENICE_API_KEY;
|
||||
const prevVitest = process.env.VITEST;
|
||||
process.env.VENICE_API_KEY = "vapi-venice-test";
|
||||
process.env.VITEST = "1";
|
||||
try {
|
||||
const { ensureClawdbotModelsJson } = await import("./models-config.js");
|
||||
const { resolveClawdbotAgentDir } = await import("./agent-paths.js");
|
||||
|
||||
await ensureClawdbotModelsJson({});
|
||||
|
||||
const modelPath = path.join(resolveClawdbotAgentDir(), "models.json");
|
||||
const raw = await fs.readFile(modelPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
providers: Record<
|
||||
string,
|
||||
{
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
models?: Array<{ id: string }>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
expect(parsed.providers.venice?.baseUrl).toBe("https://api.venice.ai/api/v1");
|
||||
expect(parsed.providers.venice?.apiKey).toBe("VENICE_API_KEY");
|
||||
const ids = parsed.providers.venice?.models?.map((model) => model.id);
|
||||
expect(ids).toContain("llama-3.3-70b");
|
||||
} finally {
|
||||
if (prevKey === undefined) delete process.env.VENICE_API_KEY;
|
||||
else process.env.VENICE_API_KEY = prevKey;
|
||||
if (prevVitest === undefined) delete process.env.VITEST;
|
||||
else process.env.VITEST = prevVitest;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,393 @@
|
||||
import type { ModelDefinitionConfig } from "../config/types.js";
|
||||
|
||||
export const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
|
||||
export const VENICE_DEFAULT_MODEL_ID = "llama-3.3-70b";
|
||||
export const VENICE_DEFAULT_MODEL_REF = `venice/${VENICE_DEFAULT_MODEL_ID}`;
|
||||
|
||||
// Venice uses credit-based pricing, not per-token costs.
|
||||
// Set to 0 as costs vary by model and account type.
|
||||
export const VENICE_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete catalog of Venice AI models.
|
||||
*
|
||||
* Venice provides two privacy modes:
|
||||
* - "private": Fully private inference, no logging, ephemeral
|
||||
* - "anonymized": Proxied through Venice with metadata stripped (for proprietary models)
|
||||
*
|
||||
* Note: The `privacy` field is included for documentation purposes but is not
|
||||
* propagated to ModelDefinitionConfig as it's not part of the core model schema.
|
||||
* Privacy mode is determined by the model itself, not configurable at runtime.
|
||||
*
|
||||
* This catalog serves as a fallback when the Venice API is unreachable.
|
||||
*/
|
||||
export const VENICE_MODEL_CATALOG = [
|
||||
// ============================================
|
||||
// PRIVATE MODELS (Fully private, no logging)
|
||||
// ============================================
|
||||
|
||||
// Llama models
|
||||
{
|
||||
id: "llama-3.3-70b",
|
||||
name: "Llama 3.3 70B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "llama-3.2-3b",
|
||||
name: "Llama 3.2 3B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "hermes-3-llama-3.1-405b",
|
||||
name: "Hermes 3 Llama 3.1 405B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// Qwen models
|
||||
{
|
||||
id: "qwen3-235b-a22b-thinking-2507",
|
||||
name: "Qwen3 235B Thinking",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-235b-a22b-instruct-2507",
|
||||
name: "Qwen3 235B Instruct",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-coder-480b-a35b-instruct",
|
||||
name: "Qwen3 Coder 480B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-next-80b",
|
||||
name: "Qwen3 Next 80B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-vl-235b-a22b",
|
||||
name: "Qwen3 VL 235B (Vision)",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "qwen3-4b",
|
||||
name: "Venice Small (Qwen3 4B)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 32768,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// DeepSeek
|
||||
{
|
||||
id: "deepseek-v3.2",
|
||||
name: "DeepSeek V3.2",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 163840,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// Venice-specific models
|
||||
{
|
||||
id: "venice-uncensored",
|
||||
name: "Venice Uncensored (Dolphin-Mistral)",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 32768,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "mistral-31-24b",
|
||||
name: "Venice Medium (Mistral)",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// Other private models
|
||||
{
|
||||
id: "google-gemma-3-27b-it",
|
||||
name: "Google Gemma 3 27B Instruct",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "openai-gpt-oss-120b",
|
||||
name: "OpenAI GPT OSS 120B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
{
|
||||
id: "zai-org-glm-4.7",
|
||||
name: "GLM 4.7",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "private",
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// ANONYMIZED MODELS (Proxied through Venice)
|
||||
// These are proprietary models accessed via Venice's proxy
|
||||
// ============================================
|
||||
|
||||
// Anthropic (via Venice)
|
||||
{
|
||||
id: "claude-opus-45",
|
||||
name: "Claude Opus 4.5 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-45",
|
||||
name: "Claude Sonnet 4.5 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// OpenAI (via Venice)
|
||||
{
|
||||
id: "openai-gpt-52",
|
||||
name: "GPT-5.2 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "openai-gpt-52-codex",
|
||||
name: "GPT-5.2 Codex (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// Google (via Venice)
|
||||
{
|
||||
id: "gemini-3-pro-preview",
|
||||
name: "Gemini 3 Pro (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "gemini-3-flash-preview",
|
||||
name: "Gemini 3 Flash (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// xAI (via Venice)
|
||||
{
|
||||
id: "grok-41-fast",
|
||||
name: "Grok 4.1 Fast (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "grok-code-fast-1",
|
||||
name: "Grok Code Fast 1 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
|
||||
// Other anonymized models
|
||||
{
|
||||
id: "kimi-k2-thinking",
|
||||
name: "Kimi K2 Thinking (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
{
|
||||
id: "minimax-m21",
|
||||
name: "MiniMax M2.1 (via Venice)",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 8192,
|
||||
privacy: "anonymized",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type VeniceCatalogEntry = (typeof VENICE_MODEL_CATALOG)[number];
|
||||
|
||||
/**
|
||||
* Build a ModelDefinitionConfig from a Venice catalog entry.
|
||||
*
|
||||
* Note: The `privacy` field from the catalog is not included in the output
|
||||
* as ModelDefinitionConfig doesn't support custom metadata fields. Privacy
|
||||
* mode is inherent to each model and documented in the catalog/docs.
|
||||
*/
|
||||
export function buildVeniceModelDefinition(entry: VeniceCatalogEntry): ModelDefinitionConfig {
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
reasoning: entry.reasoning,
|
||||
input: [...entry.input],
|
||||
cost: VENICE_DEFAULT_COST,
|
||||
contextWindow: entry.contextWindow,
|
||||
maxTokens: entry.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
// Venice API response types
|
||||
interface VeniceModelSpec {
|
||||
name: string;
|
||||
privacy: "private" | "anonymized";
|
||||
availableContextTokens: number;
|
||||
capabilities: {
|
||||
supportsReasoning: boolean;
|
||||
supportsVision: boolean;
|
||||
supportsFunctionCalling: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface VeniceModel {
|
||||
id: string;
|
||||
model_spec: VeniceModelSpec;
|
||||
}
|
||||
|
||||
interface VeniceModelsResponse {
|
||||
data: VeniceModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover models from Venice API with fallback to static catalog.
|
||||
* The /models endpoint is public and doesn't require authentication.
|
||||
*/
|
||||
export async function discoverVeniceModels(): Promise<ModelDefinitionConfig[]> {
|
||||
// Skip API discovery in test environment
|
||||
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${VENICE_BASE_URL}/models`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`[venice-models] Failed to discover models: HTTP ${response.status}, using static catalog`,
|
||||
);
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as VeniceModelsResponse;
|
||||
if (!Array.isArray(data.data) || data.data.length === 0) {
|
||||
console.warn("[venice-models] No models found from API, using static catalog");
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
|
||||
// Merge discovered models with catalog metadata
|
||||
const catalogById = new Map<string, VeniceCatalogEntry>(
|
||||
VENICE_MODEL_CATALOG.map((m) => [m.id, m]),
|
||||
);
|
||||
const models: ModelDefinitionConfig[] = [];
|
||||
|
||||
for (const apiModel of data.data) {
|
||||
const catalogEntry = catalogById.get(apiModel.id);
|
||||
if (catalogEntry) {
|
||||
// Use catalog metadata for known models
|
||||
models.push(buildVeniceModelDefinition(catalogEntry));
|
||||
} else {
|
||||
// Create definition for newly discovered models not in catalog
|
||||
const isReasoning =
|
||||
apiModel.model_spec.capabilities.supportsReasoning ||
|
||||
apiModel.id.toLowerCase().includes("thinking") ||
|
||||
apiModel.id.toLowerCase().includes("reason") ||
|
||||
apiModel.id.toLowerCase().includes("r1");
|
||||
|
||||
const hasVision = apiModel.model_spec.capabilities.supportsVision;
|
||||
|
||||
models.push({
|
||||
id: apiModel.id,
|
||||
name: apiModel.model_spec.name || apiModel.id,
|
||||
reasoning: isReasoning,
|
||||
input: hasVision ? ["text", "image"] : ["text"],
|
||||
cost: VENICE_DEFAULT_COST,
|
||||
contextWindow: apiModel.model_spec.availableContextTokens || 128000,
|
||||
maxTokens: 8192,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return models.length > 0 ? models : VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
} catch (error) {
|
||||
console.warn(`[venice-models] Discovery failed: ${String(error)}, using static catalog`);
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { chunkMarkdownText, chunkText, resolveTextChunkLimit } from "./chunk.js";
|
||||
import {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
resolveTextChunkLimit,
|
||||
} from "./chunk.js";
|
||||
|
||||
function expectFencesBalanced(chunks: string[]) {
|
||||
for (const chunk of chunks) {
|
||||
@@ -231,3 +238,95 @@ describe("chunkMarkdownText", () => {
|
||||
expect(chunks.join("")).toBe(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkByNewline", () => {
|
||||
it("splits text on newlines", () => {
|
||||
const text = "Line one\nLine two\nLine three";
|
||||
const chunks = chunkByNewline(text, 1000);
|
||||
expect(chunks).toEqual(["Line one", "Line two", "Line three"]);
|
||||
});
|
||||
|
||||
it("filters empty lines", () => {
|
||||
const text = "Line one\n\n\nLine two\n\nLine three";
|
||||
const chunks = chunkByNewline(text, 1000);
|
||||
expect(chunks).toEqual(["Line one", "Line two", "Line three"]);
|
||||
});
|
||||
|
||||
it("trims whitespace from lines", () => {
|
||||
const text = " Line one \n Line two ";
|
||||
const chunks = chunkByNewline(text, 1000);
|
||||
expect(chunks).toEqual(["Line one", "Line two"]);
|
||||
});
|
||||
|
||||
it("falls back to length-based for long lines", () => {
|
||||
const text = "Short line\n" + "a".repeat(50) + "\nAnother short";
|
||||
const chunks = chunkByNewline(text, 20);
|
||||
expect(chunks[0]).toBe("Short line");
|
||||
// Long line gets split into multiple chunks
|
||||
expect(chunks[1].length).toBe(20);
|
||||
expect(chunks[2].length).toBe(20);
|
||||
expect(chunks[3].length).toBe(10);
|
||||
expect(chunks[4]).toBe("Another short");
|
||||
});
|
||||
|
||||
it("returns empty array for empty input", () => {
|
||||
expect(chunkByNewline("", 100)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array for whitespace-only input", () => {
|
||||
expect(chunkByNewline(" \n\n ", 100)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkTextWithMode", () => {
|
||||
it("uses length-based chunking for length mode", () => {
|
||||
const text = "Line one\nLine two";
|
||||
const chunks = chunkTextWithMode(text, 1000, "length");
|
||||
expect(chunks).toEqual(["Line one\nLine two"]);
|
||||
});
|
||||
|
||||
it("uses newline-based chunking for newline mode", () => {
|
||||
const text = "Line one\nLine two";
|
||||
const chunks = chunkTextWithMode(text, 1000, "newline");
|
||||
expect(chunks).toEqual(["Line one", "Line two"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChunkMode", () => {
|
||||
it("returns length as default", () => {
|
||||
expect(resolveChunkMode(undefined, "telegram")).toBe("length");
|
||||
expect(resolveChunkMode({}, "discord")).toBe("length");
|
||||
expect(resolveChunkMode(undefined, "bluebubbles")).toBe("length");
|
||||
});
|
||||
|
||||
it("returns length for internal channel", () => {
|
||||
const cfg = { channels: { bluebubbles: { chunkMode: "newline" as const } } };
|
||||
expect(resolveChunkMode(cfg, "__internal__")).toBe("length");
|
||||
});
|
||||
|
||||
it("supports provider-level overrides for bluebubbles", () => {
|
||||
const cfg = { channels: { bluebubbles: { chunkMode: "newline" as const } } };
|
||||
expect(resolveChunkMode(cfg, "bluebubbles")).toBe("newline");
|
||||
expect(resolveChunkMode(cfg, "discord")).toBe("length");
|
||||
});
|
||||
|
||||
it("supports account-level overrides for bluebubbles", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
chunkMode: "length" as const,
|
||||
accounts: {
|
||||
primary: { chunkMode: "newline" as const },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(resolveChunkMode(cfg, "bluebubbles", "primary")).toBe("newline");
|
||||
expect(resolveChunkMode(cfg, "bluebubbles", "other")).toBe("length");
|
||||
});
|
||||
|
||||
it("ignores chunkMode for non-bluebubbles providers", () => {
|
||||
const cfg = { channels: { ["telegram" as string]: { chunkMode: "newline" as const } } };
|
||||
expect(resolveChunkMode(cfg, "telegram")).toBe("length");
|
||||
});
|
||||
});
|
||||
+83
-1
@@ -10,11 +10,20 @@ import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
|
||||
export type TextChunkProvider = ChannelId | typeof INTERNAL_MESSAGE_CHANNEL;
|
||||
|
||||
/**
|
||||
* Chunking mode for outbound messages:
|
||||
* - "length": Split only when exceeding textChunkLimit (default)
|
||||
* - "newline": Split on every newline, with fallback to length-based for long lines
|
||||
*/
|
||||
export type ChunkMode = "length" | "newline";
|
||||
|
||||
const DEFAULT_CHUNK_LIMIT = 4000;
|
||||
const DEFAULT_CHUNK_MODE: ChunkMode = "length";
|
||||
|
||||
type ProviderChunkConfig = {
|
||||
textChunkLimit?: number;
|
||||
accounts?: Record<string, { textChunkLimit?: number }>;
|
||||
chunkMode?: ChunkMode;
|
||||
accounts?: Record<string, { textChunkLimit?: number; chunkMode?: ChunkMode }>;
|
||||
};
|
||||
|
||||
function resolveChunkLimitForProvider(
|
||||
@@ -63,6 +72,79 @@ export function resolveTextChunkLimit(
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolveChunkModeForProvider(
|
||||
cfgSection: ProviderChunkConfig | undefined,
|
||||
accountId?: string | null,
|
||||
): ChunkMode | undefined {
|
||||
if (!cfgSection) return undefined;
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
const accounts = cfgSection.accounts;
|
||||
if (accounts && typeof accounts === "object") {
|
||||
const direct = accounts[normalizedAccountId];
|
||||
if (direct?.chunkMode) {
|
||||
return direct.chunkMode;
|
||||
}
|
||||
const matchKey = Object.keys(accounts).find(
|
||||
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
|
||||
);
|
||||
const match = matchKey ? accounts[matchKey] : undefined;
|
||||
if (match?.chunkMode) {
|
||||
return match.chunkMode;
|
||||
}
|
||||
}
|
||||
return cfgSection.chunkMode;
|
||||
}
|
||||
|
||||
export function resolveChunkMode(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
provider?: TextChunkProvider,
|
||||
accountId?: string | null,
|
||||
): ChunkMode {
|
||||
if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return DEFAULT_CHUNK_MODE;
|
||||
// Chunk mode is only supported for BlueBubbles.
|
||||
if (provider !== "bluebubbles") return DEFAULT_CHUNK_MODE;
|
||||
const channelsConfig = cfg?.channels as Record<string, unknown> | undefined;
|
||||
const providerConfig = (channelsConfig?.[provider] ??
|
||||
(cfg as Record<string, unknown> | undefined)?.[provider]) as ProviderChunkConfig | undefined;
|
||||
const mode = resolveChunkModeForProvider(providerConfig, accountId);
|
||||
return mode ?? DEFAULT_CHUNK_MODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split text on newlines, filtering empty lines.
|
||||
* Lines exceeding maxLineLength are further split using length-based chunking.
|
||||
*/
|
||||
export function chunkByNewline(text: string, maxLineLength: number): string[] {
|
||||
if (!text) return [];
|
||||
const lines = text.split("\n");
|
||||
const chunks: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue; // skip empty lines
|
||||
|
||||
if (trimmed.length <= maxLineLength) {
|
||||
chunks.push(trimmed);
|
||||
} else {
|
||||
// Long line: fall back to length-based chunking
|
||||
const subChunks = chunkText(trimmed, maxLineLength);
|
||||
chunks.push(...subChunks);
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified chunking function that dispatches based on mode.
|
||||
*/
|
||||
export function chunkTextWithMode(text: string, limit: number, mode: ChunkMode): string[] {
|
||||
if (mode === "newline") {
|
||||
return chunkByNewline(text, limit);
|
||||
}
|
||||
return chunkText(text, limit);
|
||||
}
|
||||
|
||||
export function chunkText(text: string, limit: number): string[] {
|
||||
if (!text) return [];
|
||||
if (limit <= 0) return [text];
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
listDeliverableMessageChannels,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||
import { resolveChunkMode, resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||
|
||||
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
||||
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
||||
@@ -68,6 +68,17 @@ export function resolveBlockStreamingChunking(
|
||||
fallbackLimit: providerChunkLimit,
|
||||
});
|
||||
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
|
||||
|
||||
// BlueBubbles-only: if chunkMode is "newline", use newline-based streaming
|
||||
const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId);
|
||||
if (channelChunkMode === "newline") {
|
||||
// For newline mode: use very low minChars to flush quickly on newlines
|
||||
const minChars = Math.max(1, Math.floor(chunkCfg?.minChars ?? 1));
|
||||
const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? textLimit));
|
||||
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
||||
return { minChars, maxChars, breakPreference: "newline" };
|
||||
}
|
||||
|
||||
const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX));
|
||||
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
||||
const minFallback = DEFAULT_BLOCK_STREAM_MIN;
|
||||
@@ -91,6 +102,13 @@ export function resolveBlockStreamingCoalescing(
|
||||
},
|
||||
): BlockStreamingCoalescing | undefined {
|
||||
const providerKey = normalizeChunkProvider(provider);
|
||||
|
||||
// BlueBubbles-only: when chunkMode is "newline", disable coalescing to send each line immediately
|
||||
const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId);
|
||||
if (channelChunkMode === "newline") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
|
||||
const providerChunkLimit = providerId
|
||||
? getChannelDock(providerId)?.outbound?.textChunkLimit
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
MediaUnderstandingDecision,
|
||||
MediaUnderstandingOutput,
|
||||
} from "../media-understanding/types.js";
|
||||
import type { LinkUnderstandingDecision } from "../link-understanding/types.js";
|
||||
|
||||
/** Valid message channels for routing. */
|
||||
export type OriginatingChannelType = ChannelId | InternalMessageChannel;
|
||||
@@ -73,7 +72,6 @@ export type MsgContext = {
|
||||
MediaUnderstanding?: MediaUnderstandingOutput[];
|
||||
MediaUnderstandingDecisions?: MediaUnderstandingDecision[];
|
||||
LinkUnderstanding?: string[];
|
||||
LinkUnderstandingDecisions?: LinkUnderstandingDecision[];
|
||||
Prompt?: string;
|
||||
MaxChars?: number;
|
||||
ChatType?: string;
|
||||
|
||||
@@ -164,6 +164,17 @@ export function resolveGoogleChatGroupRequireMention(params: GroupMentionParams)
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveGoogleChatGroupToolPolicy(
|
||||
params: GroupMentionParams,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
return resolveChannelGroupToolsPolicy({
|
||||
cfg: params.cfg,
|
||||
channel: "googlechat",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSlackGroupRequireMention(params: GroupMentionParams): boolean {
|
||||
const account = resolveSlackAccount({
|
||||
cfg: params.cfg,
|
||||
@@ -240,17 +251,6 @@ export function resolveIMessageGroupToolPolicy(
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveGoogleChatGroupToolPolicy(
|
||||
params: GroupMentionParams,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
return resolveChannelGroupToolsPolicy({
|
||||
cfg: params.cfg,
|
||||
channel: "googlechat",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveDiscordGroupToolPolicy(
|
||||
params: GroupMentionParams,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
|
||||
@@ -154,6 +154,12 @@ describe("cli program (smoke)", () => {
|
||||
key: "sk-synthetic-test",
|
||||
field: "syntheticApiKey",
|
||||
},
|
||||
{
|
||||
authChoice: "venice-api-key",
|
||||
flag: "--venice-api-key",
|
||||
key: "vapi-venice-test",
|
||||
field: "veniceApiKey",
|
||||
},
|
||||
{
|
||||
authChoice: "zai-api-key",
|
||||
flag: "--zai-api-key",
|
||||
|
||||
@@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||
.option(
|
||||
"--auth-choice <choice>",
|
||||
"Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||
"Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||
)
|
||||
.option(
|
||||
"--token-provider <id>",
|
||||
@@ -74,6 +74,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
.option("--zai-api-key <key>", "Z.AI API key")
|
||||
.option("--minimax-api-key <key>", "MiniMax API key")
|
||||
.option("--synthetic-api-key <key>", "Synthetic API key")
|
||||
.option("--venice-api-key <key>", "Venice API key")
|
||||
.option("--opencode-zen-api-key <key>", "OpenCode Zen API key")
|
||||
.option("--gateway-port <port>", "Gateway port")
|
||||
.option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom")
|
||||
@@ -123,6 +124,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
zaiApiKey: opts.zaiApiKey as string | undefined,
|
||||
minimaxApiKey: opts.minimaxApiKey as string | undefined,
|
||||
syntheticApiKey: opts.syntheticApiKey as string | undefined,
|
||||
veniceApiKey: opts.veniceApiKey as string | undefined,
|
||||
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
|
||||
gatewayPort:
|
||||
typeof gatewayPort === "number" && Number.isFinite(gatewayPort)
|
||||
|
||||
@@ -127,6 +127,18 @@ describe("buildAuthChoiceOptions", () => {
|
||||
expect(options.some((opt) => opt.value === "synthetic-api-key")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes Venice auth choice", () => {
|
||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||
const options = buildAuthChoiceOptions({
|
||||
store,
|
||||
includeSkip: false,
|
||||
includeClaudeCliIfMissing: true,
|
||||
platform: "darwin",
|
||||
});
|
||||
|
||||
expect(options.some((opt) => opt.value === "venice-api-key")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes Chutes OAuth auth choice", () => {
|
||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||
const options = buildAuthChoiceOptions({
|
||||
|
||||
@@ -21,6 +21,7 @@ export type AuthChoiceGroupId =
|
||||
| "opencode-zen"
|
||||
| "minimax"
|
||||
| "synthetic"
|
||||
| "venice"
|
||||
| "qwen";
|
||||
|
||||
export type AuthChoiceGroup = {
|
||||
@@ -108,6 +109,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||
hint: "API key",
|
||||
choices: ["opencode-zen"],
|
||||
},
|
||||
{
|
||||
value: "venice",
|
||||
label: "Venice AI",
|
||||
hint: "Privacy-focused (uncensored models)",
|
||||
choices: ["venice-api-key"],
|
||||
},
|
||||
];
|
||||
|
||||
function formatOAuthHint(expires?: number, opts?: { allowStale?: boolean }): string {
|
||||
@@ -220,6 +227,11 @@ export function buildAuthChoiceOptions(params: {
|
||||
label: "OpenCode Zen (multi-model proxy)",
|
||||
hint: "Claude, GPT, Gemini via opencode.ai/zen",
|
||||
});
|
||||
options.push({
|
||||
value: "venice-api-key",
|
||||
label: "Venice AI API key",
|
||||
hint: "Privacy-focused inference (uncensored models)",
|
||||
});
|
||||
options.push({ value: "minimax-api", label: "MiniMax M2.1" });
|
||||
options.push({
|
||||
value: "minimax-api-lightning",
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
applyOpenrouterProviderConfig,
|
||||
applySyntheticConfig,
|
||||
applySyntheticProviderConfig,
|
||||
applyVeniceConfig,
|
||||
applyVeniceProviderConfig,
|
||||
applyVercelAiGatewayConfig,
|
||||
applyVercelAiGatewayProviderConfig,
|
||||
applyZaiConfig,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
setGeminiApiKey,
|
||||
setKimiCodeApiKey,
|
||||
@@ -37,6 +40,7 @@ import {
|
||||
setOpencodeZenApiKey,
|
||||
setOpenrouterApiKey,
|
||||
setSyntheticApiKey,
|
||||
setVeniceApiKey,
|
||||
setVercelAiGatewayApiKey,
|
||||
setZaiApiKey,
|
||||
ZAI_DEFAULT_MODEL_REF,
|
||||
@@ -77,6 +81,8 @@ export async function applyAuthChoiceApiProviders(
|
||||
authChoice = "zai-api-key";
|
||||
} else if (params.opts.tokenProvider === "synthetic") {
|
||||
authChoice = "synthetic-api-key";
|
||||
} else if (params.opts.tokenProvider === "venice") {
|
||||
authChoice = "venice-api-key";
|
||||
} else if (params.opts.tokenProvider === "opencode") {
|
||||
authChoice = "opencode-zen";
|
||||
}
|
||||
@@ -457,6 +463,65 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (authChoice === "venice-api-key") {
|
||||
let hasCredential = false;
|
||||
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "venice") {
|
||||
await setVeniceApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Venice AI provides privacy-focused inference with uncensored models.",
|
||||
"Get your API key at: https://venice.ai/settings/api",
|
||||
"Supports 'private' (fully private) and 'anonymized' (proxy) modes.",
|
||||
].join("\n"),
|
||||
"Venice AI",
|
||||
);
|
||||
}
|
||||
|
||||
const envKey = resolveEnvApiKey("venice");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing VENICE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
await setVeniceApiKey(envKey.apiKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter Venice AI API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setVeniceApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "venice:default",
|
||||
provider: "venice",
|
||||
mode: "api_key",
|
||||
});
|
||||
{
|
||||
const applied = await applyDefaultModelChoice({
|
||||
config: nextConfig,
|
||||
setDefaultModel: params.setDefaultModel,
|
||||
defaultModel: VENICE_DEFAULT_MODEL_REF,
|
||||
applyDefaultConfig: applyVeniceConfig,
|
||||
applyProviderConfig: applyVeniceProviderConfig,
|
||||
noteDefault: VENICE_DEFAULT_MODEL_REF,
|
||||
noteAgentModel,
|
||||
prompter: params.prompter,
|
||||
});
|
||||
nextConfig = applied.config;
|
||||
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
||||
}
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (authChoice === "opencode-zen") {
|
||||
let hasCredential = false;
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") {
|
||||
|
||||
@@ -19,6 +19,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"google-gemini-cli": "google-gemini-cli",
|
||||
"zai-api-key": "zai",
|
||||
"synthetic-api-key": "synthetic",
|
||||
"venice-api-key": "venice",
|
||||
"github-copilot": "github-copilot",
|
||||
"copilot-proxy": "copilot-proxy",
|
||||
"minimax-cloud": "minimax",
|
||||
|
||||
@@ -33,6 +33,7 @@ describe("applyAuthChoice", () => {
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
|
||||
const previousAiGatewayKey = process.env.AI_GATEWAY_API_KEY;
|
||||
const previousVeniceKey = process.env.VENICE_API_KEY;
|
||||
const previousSshTty = process.env.SSH_TTY;
|
||||
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
|
||||
let tempStateDir: string | null = null;
|
||||
@@ -69,6 +70,11 @@ describe("applyAuthChoice", () => {
|
||||
} else {
|
||||
process.env.AI_GATEWAY_API_KEY = previousAiGatewayKey;
|
||||
}
|
||||
if (previousVeniceKey === undefined) {
|
||||
delete process.env.VENICE_API_KEY;
|
||||
} else {
|
||||
process.env.VENICE_API_KEY = previousVeniceKey;
|
||||
}
|
||||
if (previousSshTty === undefined) {
|
||||
delete process.env.SSH_TTY;
|
||||
} else {
|
||||
@@ -187,6 +193,59 @@ describe("applyAuthChoice", () => {
|
||||
expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test");
|
||||
});
|
||||
|
||||
it("prompts and writes Venice API key when selecting venice-api-key", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const text = vi.fn().mockResolvedValue("vapi-venice-test");
|
||||
const select: WizardPrompter["select"] = vi.fn(
|
||||
async (params) => params.options[0]?.value as never,
|
||||
);
|
||||
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select,
|
||||
multiselect,
|
||||
text,
|
||||
confirm: vi.fn(async () => false),
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "venice-api-key",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Enter Venice AI API key" }),
|
||||
);
|
||||
expect(result.config.auth?.profiles?.["venice:default"]).toMatchObject({
|
||||
provider: "venice",
|
||||
mode: "api_key",
|
||||
});
|
||||
|
||||
const authProfilePath = authProfilePathFor(requireAgentDir());
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["venice:default"]?.key).toBe("vapi-venice-test");
|
||||
});
|
||||
|
||||
it("sets default model when selecting github-copilot", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
|
||||
@@ -134,6 +134,7 @@ export async function modelsStatusCommand(
|
||||
"zai",
|
||||
"mistral",
|
||||
"synthetic",
|
||||
"venice",
|
||||
];
|
||||
for (const provider of envProbeProviders) {
|
||||
if (resolveEnvApiKey(provider)) providersFromEnv.add(provider);
|
||||
|
||||
@@ -4,6 +4,12 @@ import {
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
SYNTHETIC_MODEL_CATALOG,
|
||||
} from "../agents/synthetic-models.js";
|
||||
import {
|
||||
buildVeniceModelDefinition,
|
||||
VENICE_BASE_URL,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
VENICE_MODEL_CATALOG,
|
||||
} from "../agents/venice-models.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
@@ -330,6 +336,83 @@ export function applySyntheticConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Venice provider configuration without changing the default model.
|
||||
* Registers Venice models and sets up the provider, but preserves existing model selection.
|
||||
*/
|
||||
export function applyVeniceProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[VENICE_DEFAULT_MODEL_REF] = {
|
||||
...models[VENICE_DEFAULT_MODEL_REF],
|
||||
alias: models[VENICE_DEFAULT_MODEL_REF]?.alias ?? "Llama 3.3 70B",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers.venice;
|
||||
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||
const veniceModels = VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
const mergedModels = [
|
||||
...existingModels,
|
||||
...veniceModels.filter(
|
||||
(model) => !existingModels.some((existing) => existing.id === model.id),
|
||||
),
|
||||
];
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
> as { apiKey?: string };
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim();
|
||||
providers.venice = {
|
||||
...existingProviderRest,
|
||||
baseUrl: VENICE_BASE_URL,
|
||||
api: "openai-completions",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : veniceModels,
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Venice provider configuration AND set Venice as the default model.
|
||||
* Use this when Venice is the primary provider choice during onboarding.
|
||||
*/
|
||||
export function applyVeniceConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const next = applyVeniceProviderConfig(cfg);
|
||||
const existingModel = next.agents?.defaults?.model;
|
||||
return {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...next.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: VENICE_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAuthProfileConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
params: {
|
||||
|
||||
@@ -99,6 +99,19 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function setVeniceApiKey(key: string, agentDir?: string) {
|
||||
// Write to resolved agent dir so gateway finds credentials on startup.
|
||||
upsertAuthProfile({
|
||||
profileId: "venice:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "venice",
|
||||
key,
|
||||
},
|
||||
agentDir: resolveAuthAgentDir(agentDir),
|
||||
});
|
||||
}
|
||||
|
||||
export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7";
|
||||
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
|
||||
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5";
|
||||
|
||||
@@ -15,9 +15,13 @@ import {
|
||||
applyOpenrouterProviderConfig,
|
||||
applySyntheticConfig,
|
||||
applySyntheticProviderConfig,
|
||||
applyVeniceConfig,
|
||||
applyVeniceProviderConfig,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
SYNTHETIC_DEFAULT_MODEL_ID,
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
VENICE_DEFAULT_MODEL_ID,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
setMinimaxApiKey,
|
||||
writeOAuthCredentials,
|
||||
} from "./onboard-auth.js";
|
||||
@@ -343,6 +347,52 @@ describe("applySyntheticConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyVeniceConfig", () => {
|
||||
it("adds venice provider with correct settings", () => {
|
||||
const cfg = applyVeniceConfig({});
|
||||
expect(cfg.models?.providers?.venice).toMatchObject({
|
||||
baseUrl: "https://api.venice.ai/api/v1",
|
||||
api: "openai-completions",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets correct primary model", () => {
|
||||
const cfg = applyVeniceConfig({});
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe(VENICE_DEFAULT_MODEL_REF);
|
||||
});
|
||||
|
||||
it("merges existing venice provider models", () => {
|
||||
const cfg = applyVeniceProviderConfig({
|
||||
models: {
|
||||
providers: {
|
||||
venice: {
|
||||
baseUrl: "https://old.example.com",
|
||||
apiKey: "old-key",
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
id: "old-model",
|
||||
name: "Old",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.models?.providers?.venice?.baseUrl).toBe("https://api.venice.ai/api/v1");
|
||||
expect(cfg.models?.providers?.venice?.api).toBe("openai-completions");
|
||||
expect(cfg.models?.providers?.venice?.apiKey).toBe("old-key");
|
||||
const ids = cfg.models?.providers?.venice?.models.map((m) => m.id);
|
||||
expect(ids).toContain("old-model");
|
||||
expect(ids).toContain(VENICE_DEFAULT_MODEL_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyOpencodeZenProviderConfig", () => {
|
||||
it("adds allowlist entry for the default model", () => {
|
||||
const cfg = applyOpencodeZenProviderConfig({});
|
||||
|
||||
@@ -2,6 +2,10 @@ export {
|
||||
SYNTHETIC_DEFAULT_MODEL_ID,
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
} from "../agents/synthetic-models.js";
|
||||
export {
|
||||
VENICE_DEFAULT_MODEL_ID,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
} from "../agents/venice-models.js";
|
||||
export {
|
||||
applyAuthProfileConfig,
|
||||
applyKimiCodeConfig,
|
||||
@@ -12,6 +16,8 @@ export {
|
||||
applyOpenrouterProviderConfig,
|
||||
applySyntheticConfig,
|
||||
applySyntheticProviderConfig,
|
||||
applyVeniceConfig,
|
||||
applyVeniceProviderConfig,
|
||||
applyVercelAiGatewayConfig,
|
||||
applyVercelAiGatewayProviderConfig,
|
||||
applyZaiConfig,
|
||||
@@ -39,6 +45,7 @@ export {
|
||||
setOpencodeZenApiKey,
|
||||
setOpenrouterApiKey,
|
||||
setSyntheticApiKey,
|
||||
setVeniceApiKey,
|
||||
setVercelAiGatewayApiKey,
|
||||
setZaiApiKey,
|
||||
writeOAuthCredentials,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
applyOpencodeZenConfig,
|
||||
applyOpenrouterConfig,
|
||||
applySyntheticConfig,
|
||||
applyVeniceConfig,
|
||||
applyVercelAiGatewayConfig,
|
||||
applyZaiConfig,
|
||||
setAnthropicApiKey,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
setOpencodeZenApiKey,
|
||||
setOpenrouterApiKey,
|
||||
setSyntheticApiKey,
|
||||
setVeniceApiKey,
|
||||
setVercelAiGatewayApiKey,
|
||||
setZaiApiKey,
|
||||
} from "../../onboard-auth.js";
|
||||
@@ -272,6 +274,25 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
return applySyntheticConfig(nextConfig);
|
||||
}
|
||||
|
||||
if (authChoice === "venice-api-key") {
|
||||
const resolved = await resolveNonInteractiveApiKey({
|
||||
provider: "venice",
|
||||
cfg: baseConfig,
|
||||
flagValue: opts.veniceApiKey,
|
||||
flagName: "--venice-api-key",
|
||||
envVar: "VENICE_API_KEY",
|
||||
runtime,
|
||||
});
|
||||
if (!resolved) return null;
|
||||
if (resolved.source !== "profile") await setVeniceApiKey(resolved.key);
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "venice:default",
|
||||
provider: "venice",
|
||||
mode: "api_key",
|
||||
});
|
||||
return applyVeniceConfig(nextConfig);
|
||||
}
|
||||
|
||||
if (
|
||||
authChoice === "minimax-cloud" ||
|
||||
authChoice === "minimax-api" ||
|
||||
|
||||
@@ -16,6 +16,7 @@ export type AuthChoice =
|
||||
| "moonshot-api-key"
|
||||
| "kimi-code-api-key"
|
||||
| "synthetic-api-key"
|
||||
| "venice-api-key"
|
||||
| "codex-cli"
|
||||
| "apiKey"
|
||||
| "gemini-api-key"
|
||||
@@ -68,6 +69,7 @@ export type OnboardOptions = {
|
||||
zaiApiKey?: string;
|
||||
minimaxApiKey?: string;
|
||||
syntheticApiKey?: string;
|
||||
veniceApiKey?: string;
|
||||
opencodeZenApiKey?: string;
|
||||
gatewayPort?: number;
|
||||
gatewayBind?: GatewayBind;
|
||||
|
||||
+102
-7
@@ -1,7 +1,9 @@
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||
import { parseModelRef } from "../agents/model-selection.js";
|
||||
import { resolveTalkApiKey } from "./talk.js";
|
||||
import type { ClawdbotConfig } from "./types.js";
|
||||
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
|
||||
import type { ModelDefinitionConfig } from "./types.models.js";
|
||||
|
||||
type WarnState = { warned: boolean };
|
||||
|
||||
@@ -23,6 +25,34 @@ const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = {
|
||||
"gemini-flash": "google/gemini-3-flash-preview",
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL_COST: ModelDefinitionConfig["cost"] = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
const DEFAULT_MODEL_INPUT: ModelDefinitionConfig["input"] = ["text"];
|
||||
const DEFAULT_MODEL_MAX_TOKENS = 8192;
|
||||
|
||||
type ModelDefinitionLike = Partial<ModelDefinitionConfig> &
|
||||
Pick<ModelDefinitionConfig, "id" | "name">;
|
||||
|
||||
function isPositiveNumber(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
function resolveModelCost(
|
||||
raw?: Partial<ModelDefinitionConfig["cost"]>,
|
||||
): ModelDefinitionConfig["cost"] {
|
||||
return {
|
||||
input: typeof raw?.input === "number" ? raw.input : DEFAULT_MODEL_COST.input,
|
||||
output: typeof raw?.output === "number" ? raw.output : DEFAULT_MODEL_COST.output,
|
||||
cacheRead: typeof raw?.cacheRead === "number" ? raw.cacheRead : DEFAULT_MODEL_COST.cacheRead,
|
||||
cacheWrite:
|
||||
typeof raw?.cacheWrite === "number" ? raw.cacheWrite : DEFAULT_MODEL_COST.cacheWrite,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAnthropicDefaultAuthMode(cfg: ClawdbotConfig): AnthropicAuthDefaultsMode | null {
|
||||
const profiles = cfg.auth?.profiles ?? {};
|
||||
const anthropicProfiles = Object.entries(profiles).filter(
|
||||
@@ -114,12 +144,77 @@ export function applyTalkApiKey(config: ClawdbotConfig): ClawdbotConfig {
|
||||
}
|
||||
|
||||
export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const existingAgent = cfg.agents?.defaults;
|
||||
if (!existingAgent) return cfg;
|
||||
const existingModels = existingAgent.models ?? {};
|
||||
if (Object.keys(existingModels).length === 0) return cfg;
|
||||
|
||||
let mutated = false;
|
||||
let nextCfg = cfg;
|
||||
|
||||
const providerConfig = nextCfg.models?.providers;
|
||||
if (providerConfig) {
|
||||
const nextProviders = { ...providerConfig };
|
||||
for (const [providerId, provider] of Object.entries(providerConfig)) {
|
||||
const models = provider.models;
|
||||
if (!Array.isArray(models) || models.length === 0) continue;
|
||||
let providerMutated = false;
|
||||
const nextModels = models.map((model) => {
|
||||
const raw = model as ModelDefinitionLike;
|
||||
let modelMutated = false;
|
||||
|
||||
const reasoning = typeof raw.reasoning === "boolean" ? raw.reasoning : false;
|
||||
if (raw.reasoning !== reasoning) modelMutated = true;
|
||||
|
||||
const input = raw.input ?? [...DEFAULT_MODEL_INPUT];
|
||||
if (raw.input === undefined) modelMutated = true;
|
||||
|
||||
const cost = resolveModelCost(raw.cost);
|
||||
const costMutated =
|
||||
!raw.cost ||
|
||||
raw.cost.input !== cost.input ||
|
||||
raw.cost.output !== cost.output ||
|
||||
raw.cost.cacheRead !== cost.cacheRead ||
|
||||
raw.cost.cacheWrite !== cost.cacheWrite;
|
||||
if (costMutated) modelMutated = true;
|
||||
|
||||
const contextWindow = isPositiveNumber(raw.contextWindow)
|
||||
? raw.contextWindow
|
||||
: DEFAULT_CONTEXT_TOKENS;
|
||||
if (raw.contextWindow !== contextWindow) modelMutated = true;
|
||||
|
||||
const defaultMaxTokens = Math.min(DEFAULT_MODEL_MAX_TOKENS, contextWindow);
|
||||
const maxTokens = isPositiveNumber(raw.maxTokens) ? raw.maxTokens : defaultMaxTokens;
|
||||
if (raw.maxTokens !== maxTokens) modelMutated = true;
|
||||
|
||||
if (!modelMutated) return model;
|
||||
providerMutated = true;
|
||||
return {
|
||||
...raw,
|
||||
reasoning,
|
||||
input,
|
||||
cost,
|
||||
contextWindow,
|
||||
maxTokens,
|
||||
} as ModelDefinitionConfig;
|
||||
});
|
||||
|
||||
if (!providerMutated) continue;
|
||||
nextProviders[providerId] = { ...provider, models: nextModels };
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (mutated) {
|
||||
nextCfg = {
|
||||
...nextCfg,
|
||||
models: {
|
||||
...nextCfg.models,
|
||||
providers: nextProviders,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const existingAgent = nextCfg.agents?.defaults;
|
||||
if (!existingAgent) return mutated ? nextCfg : cfg;
|
||||
const existingModels = existingAgent.models ?? {};
|
||||
if (Object.keys(existingModels).length === 0) return mutated ? nextCfg : cfg;
|
||||
|
||||
const nextModels: Record<string, { alias?: string }> = {
|
||||
...existingModels,
|
||||
};
|
||||
@@ -135,9 +230,9 @@ export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
if (!mutated) return cfg;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
...nextCfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
...nextCfg.agents,
|
||||
defaults: { ...existingAgent, models: nextModels },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||
import { applyModelDefaults } from "./defaults.js";
|
||||
import type { ClawdbotConfig } from "./types.js";
|
||||
|
||||
@@ -55,4 +56,28 @@ describe("applyModelDefaults", () => {
|
||||
"gemini-flash",
|
||||
);
|
||||
});
|
||||
|
||||
it("fills missing model provider defaults", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
myproxy: {
|
||||
baseUrl: "https://proxy.example/v1",
|
||||
apiKey: "sk-test",
|
||||
api: "openai-completions",
|
||||
models: [{ id: "gpt-5.2", name: "GPT-5.2" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ClawdbotConfig;
|
||||
|
||||
const next = applyModelDefaults(cfg);
|
||||
const model = next.models?.providers?.myproxy?.models?.[0];
|
||||
|
||||
expect(model?.reasoning).toBe(false);
|
||||
expect(model?.input).toEqual(["text"]);
|
||||
expect(model?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
||||
expect(model?.contextWindow).toBe(DEFAULT_CONTEXT_TOKENS);
|
||||
expect(model?.maxTokens).toBe(8192);
|
||||
});
|
||||
});
|
||||
@@ -28,18 +28,19 @@ export const ModelDefinitionSchema = z
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
api: ModelApiSchema.optional(),
|
||||
reasoning: z.boolean(),
|
||||
input: z.array(z.union([z.literal("text"), z.literal("image")])),
|
||||
reasoning: z.boolean().optional(),
|
||||
input: z.array(z.union([z.literal("text"), z.literal("image")])).optional(),
|
||||
cost: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cacheRead: z.number(),
|
||||
cacheWrite: z.number(),
|
||||
input: z.number().optional(),
|
||||
output: z.number().optional(),
|
||||
cacheRead: z.number().optional(),
|
||||
cacheWrite: z.number().optional(),
|
||||
})
|
||||
.strict(),
|
||||
contextWindow: z.number().positive(),
|
||||
maxTokens: z.number().positive(),
|
||||
.strict()
|
||||
.optional(),
|
||||
contextWindow: z.number().positive().optional(),
|
||||
maxTokens: z.number().positive().optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
compat: ModelCompatSchema,
|
||||
})
|
||||
|
||||
@@ -633,6 +633,7 @@ export const BlueBubblesAccountSchemaBase = z
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
mediaMaxMb: z.number().int().positive().optional(),
|
||||
sendReadReceipts: z.boolean().optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
|
||||
@@ -212,7 +212,8 @@ export async function deliverOutboundPayloads(params: {
|
||||
results.push(await handler.sendText(text));
|
||||
return;
|
||||
}
|
||||
for (const chunk of handler.chunker(text, textLimit)) {
|
||||
const chunks = handler.chunker(text, textLimit);
|
||||
for (const chunk of chunks) {
|
||||
throwIfAborted(abortSignal);
|
||||
results.push(await handler.sendText(chunk));
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import { runLinkUnderstanding } from "./runner.js";
|
||||
|
||||
vi.mock("./runner.js", () => ({
|
||||
runLinkUnderstanding: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("applyLinkUnderstanding", () => {
|
||||
it("keeps command parsing bodies unchanged when link output is applied", async () => {
|
||||
const mockedRunLinkUnderstanding = vi.mocked(runLinkUnderstanding);
|
||||
mockedRunLinkUnderstanding.mockResolvedValue({
|
||||
urls: ["https://example.com"],
|
||||
outputs: [{ url: "https://example.com", text: "Summary", source: "link-cli" }],
|
||||
decisions: [],
|
||||
});
|
||||
|
||||
const { applyLinkUnderstanding } = await import("./apply.js");
|
||||
const ctx: MsgContext = {
|
||||
Body: "check https://example.com",
|
||||
RawBody: "raw override",
|
||||
CommandBody: "/think low check https://example.com",
|
||||
};
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: { links: { models: [{ command: "link-cli" }] } },
|
||||
};
|
||||
|
||||
await applyLinkUnderstanding({ ctx, cfg });
|
||||
|
||||
expect(ctx.Body).toContain("[Link]");
|
||||
expect(ctx.CommandBody).toBe("/think low check https://example.com");
|
||||
expect(ctx.RawBody).toBe("raw override");
|
||||
expect(ctx.BodyForCommands).toBe("/think low check https://example.com");
|
||||
expect(ctx.BodyForCommands).not.toContain("[Link]");
|
||||
});
|
||||
|
||||
it("preserves original body for command parsing when no overrides exist", async () => {
|
||||
const mockedRunLinkUnderstanding = vi.mocked(runLinkUnderstanding);
|
||||
mockedRunLinkUnderstanding.mockResolvedValue({
|
||||
urls: ["https://example.com"],
|
||||
outputs: [{ url: "https://example.com", text: "Summary", source: "link-cli" }],
|
||||
decisions: [],
|
||||
});
|
||||
|
||||
const { applyLinkUnderstanding } = await import("./apply.js");
|
||||
const ctx: MsgContext = {
|
||||
Body: "check https://example.com",
|
||||
};
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: { links: { models: [{ command: "link-cli" }] } },
|
||||
};
|
||||
|
||||
await applyLinkUnderstanding({ ctx, cfg });
|
||||
|
||||
expect(ctx.Body).toContain("[Link]");
|
||||
expect(ctx.RawBody).toBe("check https://example.com");
|
||||
expect(ctx.BodyForCommands).toBe("check https://example.com");
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
||||
import { formatLinkUnderstandingBody, formatLinkUnderstandingSections } from "./format.js";
|
||||
import { formatLinkUnderstandingBody } from "./format.js";
|
||||
import { runLinkUnderstanding } from "./runner.js";
|
||||
import type { LinkUnderstandingDecision, LinkUnderstandingOutput } from "./types.js";
|
||||
|
||||
export type ApplyLinkUnderstandingResult = {
|
||||
outputs: LinkUnderstandingOutput[];
|
||||
outputs: string[];
|
||||
urls: string[];
|
||||
decisions: LinkUnderstandingDecision[];
|
||||
};
|
||||
|
||||
export async function applyLinkUnderstanding(params: {
|
||||
@@ -20,29 +18,16 @@ export async function applyLinkUnderstanding(params: {
|
||||
ctx: params.ctx,
|
||||
});
|
||||
|
||||
if (result.decisions.length > 0) {
|
||||
params.ctx.LinkUnderstandingDecisions = [
|
||||
...(params.ctx.LinkUnderstandingDecisions ?? []),
|
||||
...result.decisions,
|
||||
];
|
||||
}
|
||||
|
||||
if (result.outputs.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const originalBody = params.ctx.Body;
|
||||
const sections = formatLinkUnderstandingSections(result.outputs);
|
||||
if (sections.length > 0) {
|
||||
params.ctx.LinkUnderstanding = [...(params.ctx.LinkUnderstanding ?? []), ...sections];
|
||||
}
|
||||
params.ctx.LinkUnderstanding = [...(params.ctx.LinkUnderstanding ?? []), ...result.outputs];
|
||||
params.ctx.Body = formatLinkUnderstandingBody({
|
||||
body: params.ctx.Body,
|
||||
outputs: result.outputs,
|
||||
});
|
||||
if (!params.ctx.CommandBody && !params.ctx.RawBody && typeof originalBody === "string") {
|
||||
params.ctx.RawBody = originalBody;
|
||||
}
|
||||
|
||||
finalizeInboundContext(params.ctx, {
|
||||
forceBodyForAgent: true,
|
||||
forceBodyForCommands: true,
|
||||
|
||||
@@ -20,10 +20,8 @@ describe("extractLinksFromMessage", () => {
|
||||
expect(links).toEqual(["https://bare.example"]);
|
||||
});
|
||||
|
||||
it("blocks loopback and private hosts", () => {
|
||||
const links = extractLinksFromMessage(
|
||||
"http://localhost/test http://127.0.0.1/test http://[::1]/test http://10.0.0.1/test https://ok.test",
|
||||
);
|
||||
it("blocks 127.0.0.1", () => {
|
||||
const links = extractLinksFromMessage("http://127.0.0.1/test https://ok.test");
|
||||
expect(links).toEqual(["https://ok.test"]);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isBlockedHostname, isPrivateIpAddress } from "../infra/net/ssrf.js";
|
||||
import { DEFAULT_MAX_LINKS } from "./defaults.js";
|
||||
|
||||
// Remove markdown link syntax so only bare URLs are considered.
|
||||
@@ -20,10 +19,7 @@ function isAllowedUrl(raw: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
||||
const hostname = parsed.hostname;
|
||||
if (!hostname) return false;
|
||||
if (isBlockedHostname(hostname)) return false;
|
||||
if (isPrivateIpAddress(hostname)) return false;
|
||||
if (parsed.hostname === "127.0.0.1") return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { formatLinkUnderstandingBody } from "./format.js";
|
||||
|
||||
describe("formatLinkUnderstandingBody", () => {
|
||||
it("appends a structured link summary with source", () => {
|
||||
const body = formatLinkUnderstandingBody({
|
||||
body: "Check this",
|
||||
outputs: [{ url: "https://example.com", text: "Summary here", source: "link-cli" }],
|
||||
});
|
||||
expect(body).toBe(
|
||||
"Check this\n\n[Link]\nURL: https://example.com\nSource: link-cli\nSummary:\nSummary here",
|
||||
);
|
||||
});
|
||||
|
||||
it("numbers multiple links and omits empty body", () => {
|
||||
const body = formatLinkUnderstandingBody({
|
||||
outputs: [
|
||||
{ url: "https://a.example", text: "First", source: "cli-a" },
|
||||
{ url: "https://b.example", text: "Second", source: "cli-b" },
|
||||
],
|
||||
});
|
||||
expect(body).toBe(
|
||||
"[Link 1/2]\nURL: https://a.example\nSource: cli-a\nSummary:\nFirst\n\n[Link 2/2]\nURL: https://b.example\nSource: cli-b\nSummary:\nSecond",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,31 +1,10 @@
|
||||
import type { LinkUnderstandingOutput } from "./types.js";
|
||||
|
||||
function formatSection(output: LinkUnderstandingOutput, index: number, total: number): string {
|
||||
const label = total > 1 ? `Link ${index + 1}/${total}` : "Link";
|
||||
const source = output.source?.trim() || "unknown";
|
||||
const lines = [`[${label}]`, `URL: ${output.url}`, `Source: ${source}`];
|
||||
lines.push(`Summary:\n${output.text.trim()}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function formatLinkUnderstandingSections(outputs: LinkUnderstandingOutput[]): string[] {
|
||||
const trimmed = outputs
|
||||
.map((output) => ({ ...output, text: output.text.trim(), url: output.url.trim() }))
|
||||
.filter((output) => output.text && output.url);
|
||||
if (trimmed.length === 0) return [];
|
||||
return trimmed.map((output, index) => formatSection(output, index, trimmed.length));
|
||||
}
|
||||
|
||||
export function formatLinkUnderstandingBody(params: {
|
||||
body?: string;
|
||||
outputs: LinkUnderstandingOutput[];
|
||||
}): string {
|
||||
const sections = formatLinkUnderstandingSections(params.outputs);
|
||||
if (sections.length === 0) {
|
||||
export function formatLinkUnderstandingBody(params: { body?: string; outputs: string[] }): string {
|
||||
const outputs = params.outputs.map((output) => output.trim()).filter(Boolean);
|
||||
if (outputs.length === 0) {
|
||||
return params.body ?? "";
|
||||
}
|
||||
|
||||
const base = (params.body ?? "").trim();
|
||||
if (!base) return sections.join("\n\n");
|
||||
return `${base}\n\n${sections.join("\n\n")}`;
|
||||
if (!base) return outputs.join("\n");
|
||||
return `${base}\n\n${outputs.join("\n")}`;
|
||||
}
|
||||
@@ -2,10 +2,3 @@ export { applyLinkUnderstanding } from "./apply.js";
|
||||
export { extractLinksFromMessage } from "./detect.js";
|
||||
export { formatLinkUnderstandingBody } from "./format.js";
|
||||
export { runLinkUnderstanding } from "./runner.js";
|
||||
export type {
|
||||
LinkUnderstandingDecision,
|
||||
LinkUnderstandingDecisionOutcome,
|
||||
LinkUnderstandingModelDecision,
|
||||
LinkUnderstandingOutput,
|
||||
LinkUnderstandingUrlDecision,
|
||||
} from "./types.js";
|
||||
@@ -1,84 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { assertPublicHostname } from "../infra/net/ssrf.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runExec: vi.fn(),
|
||||
}));
|
||||
vi.mock("../infra/net/ssrf.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../infra/net/ssrf.js")>("../infra/net/ssrf.js");
|
||||
return {
|
||||
...actual,
|
||||
assertPublicHostname: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
async function loadRunner() {
|
||||
return await import("./runner.js");
|
||||
}
|
||||
|
||||
describe("runLinkUnderstanding", () => {
|
||||
const mockedRunExec = vi.mocked(runExec);
|
||||
const mockedAssertPublicHostname = vi.mocked(assertPublicHostname);
|
||||
|
||||
beforeEach(() => {
|
||||
mockedRunExec.mockReset();
|
||||
mockedAssertPublicHostname.mockReset();
|
||||
mockedAssertPublicHostname.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("falls back to the next model when a CLI entry fails", async () => {
|
||||
const { runLinkUnderstanding } = await loadRunner();
|
||||
mockedRunExec.mockImplementation(async (command) => {
|
||||
if (command === "fail") throw new Error("boom");
|
||||
return { stdout: "summary", stderr: "" };
|
||||
});
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: {
|
||||
links: {
|
||||
enabled: true,
|
||||
models: [{ command: "fail" }, { command: "ok" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const ctx: MsgContext = {
|
||||
Body: "see https://example.com",
|
||||
SessionKey: "session-1",
|
||||
Surface: "discord",
|
||||
ChatType: "direct",
|
||||
};
|
||||
|
||||
const result = await runLinkUnderstanding({ cfg, ctx });
|
||||
expect(result.urls).toEqual(["https://example.com"]);
|
||||
expect(result.outputs).toEqual([{ url: "https://example.com", text: "summary", source: "ok" }]);
|
||||
});
|
||||
|
||||
it("skips links that fail the public hostname check", async () => {
|
||||
const { runLinkUnderstanding } = await loadRunner();
|
||||
mockedAssertPublicHostname.mockRejectedValueOnce(new Error("Blocked: private/internal IP"));
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: {
|
||||
links: {
|
||||
enabled: true,
|
||||
models: [{ command: "ok" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const ctx: MsgContext = {
|
||||
Body: "see https://intranet.example.com",
|
||||
SessionKey: "session-1",
|
||||
Surface: "discord",
|
||||
ChatType: "direct",
|
||||
};
|
||||
|
||||
const result = await runLinkUnderstanding({ cfg, ctx });
|
||||
expect(result.outputs).toEqual([]);
|
||||
expect(result.decisions[0]?.urls[0]?.attempts[0]?.reason).toContain("Blocked");
|
||||
});
|
||||
});
|
||||
@@ -10,20 +10,12 @@ import {
|
||||
normalizeMediaUnderstandingChatType,
|
||||
resolveMediaUnderstandingScope,
|
||||
} from "../media-understanding/scope.js";
|
||||
import { assertPublicHostname } from "../infra/net/ssrf.js";
|
||||
import { DEFAULT_LINK_TIMEOUT_SECONDS } from "./defaults.js";
|
||||
import { extractLinksFromMessage } from "./detect.js";
|
||||
import type {
|
||||
LinkUnderstandingDecision,
|
||||
LinkUnderstandingModelDecision,
|
||||
LinkUnderstandingOutput,
|
||||
LinkUnderstandingUrlDecision,
|
||||
} from "./types.js";
|
||||
|
||||
export type LinkUnderstandingResult = {
|
||||
urls: string[];
|
||||
outputs: LinkUnderstandingOutput[];
|
||||
decisions: LinkUnderstandingDecision[];
|
||||
outputs: string[];
|
||||
};
|
||||
|
||||
function resolveScopeDecision(params: {
|
||||
@@ -77,42 +69,13 @@ async function runCliEntry(params: {
|
||||
return trimmed || null;
|
||||
}
|
||||
|
||||
function buildModelDecision(params: {
|
||||
entry: LinkModelConfig;
|
||||
outcome: LinkUnderstandingModelDecision["outcome"];
|
||||
reason?: string;
|
||||
}): LinkUnderstandingModelDecision {
|
||||
const command = params.entry.command?.trim();
|
||||
return {
|
||||
type: "cli",
|
||||
command: command || undefined,
|
||||
outcome: params.outcome,
|
||||
reason: params.reason,
|
||||
};
|
||||
}
|
||||
|
||||
async function assertUrlIsPublic(
|
||||
url: string,
|
||||
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
await assertPublicHostname(parsed.hostname);
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, reason: String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
async function runLinkEntries(params: {
|
||||
entries: LinkModelConfig[];
|
||||
ctx: MsgContext;
|
||||
url: string;
|
||||
config?: LinkToolsConfig;
|
||||
}): Promise<{
|
||||
output: LinkUnderstandingOutput | null;
|
||||
attempts: LinkUnderstandingModelDecision[];
|
||||
}> {
|
||||
const attempts: LinkUnderstandingModelDecision[] = [];
|
||||
}): Promise<string | null> {
|
||||
let lastError: unknown;
|
||||
for (const entry of params.entries) {
|
||||
try {
|
||||
const output = await runCliEntry({
|
||||
@@ -121,33 +84,18 @@ async function runLinkEntries(params: {
|
||||
url: params.url,
|
||||
config: params.config,
|
||||
});
|
||||
if (output) {
|
||||
const decision = buildModelDecision({ entry, outcome: "success" });
|
||||
attempts.push(decision);
|
||||
return {
|
||||
output: {
|
||||
url: params.url,
|
||||
text: output,
|
||||
source: entry.command?.trim() || undefined,
|
||||
},
|
||||
attempts,
|
||||
};
|
||||
}
|
||||
attempts.push(buildModelDecision({ entry, outcome: "skipped", reason: "empty output" }));
|
||||
if (output) return output;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
if (shouldLogVerbose()) {
|
||||
logVerbose(`Link understanding failed for ${params.url}: ${String(err)}`);
|
||||
}
|
||||
attempts.push(
|
||||
buildModelDecision({
|
||||
entry,
|
||||
outcome: "failed",
|
||||
reason: String(err),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return { output: null, attempts };
|
||||
if (lastError && shouldLogVerbose()) {
|
||||
logVerbose(`Link understanding exhausted for ${params.url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function runLinkUnderstanding(params: {
|
||||
@@ -156,66 +104,33 @@ export async function runLinkUnderstanding(params: {
|
||||
message?: string;
|
||||
}): Promise<LinkUnderstandingResult> {
|
||||
const config = params.cfg.tools?.links;
|
||||
if (!config || config.enabled === false) {
|
||||
return { urls: [], outputs: [], decisions: [{ outcome: "disabled", urls: [] }] };
|
||||
}
|
||||
if (!config || config.enabled === false) return { urls: [], outputs: [] };
|
||||
|
||||
const scopeDecision = resolveScopeDecision({ config, ctx: params.ctx });
|
||||
if (scopeDecision === "deny") {
|
||||
if (shouldLogVerbose()) {
|
||||
logVerbose("Link understanding disabled by scope policy.");
|
||||
}
|
||||
return { urls: [], outputs: [], decisions: [{ outcome: "scope-deny", urls: [] }] };
|
||||
return { urls: [], outputs: [] };
|
||||
}
|
||||
|
||||
const message = params.message ?? params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body;
|
||||
const links = extractLinksFromMessage(message ?? "", { maxLinks: config?.maxLinks });
|
||||
if (links.length === 0) {
|
||||
return { urls: [], outputs: [], decisions: [{ outcome: "no-links", urls: [] }] };
|
||||
}
|
||||
if (links.length === 0) return { urls: [], outputs: [] };
|
||||
|
||||
const entries = config?.models ?? [];
|
||||
if (entries.length === 0) {
|
||||
const urlDecisions: LinkUnderstandingUrlDecision[] = links.map((url) => ({
|
||||
url,
|
||||
attempts: [],
|
||||
}));
|
||||
return {
|
||||
urls: links,
|
||||
outputs: [],
|
||||
decisions: [{ outcome: "skipped", urls: urlDecisions }],
|
||||
};
|
||||
}
|
||||
if (entries.length === 0) return { urls: links, outputs: [] };
|
||||
|
||||
const outputs: LinkUnderstandingOutput[] = [];
|
||||
const urlDecisions: LinkUnderstandingUrlDecision[] = [];
|
||||
const outputs: string[] = [];
|
||||
for (const url of links) {
|
||||
const ssrfCheck = await assertUrlIsPublic(url);
|
||||
if (!ssrfCheck.ok) {
|
||||
urlDecisions.push({
|
||||
url,
|
||||
attempts: [
|
||||
{
|
||||
type: "cli",
|
||||
command: "ssrf",
|
||||
outcome: "skipped",
|
||||
reason: ssrfCheck.reason,
|
||||
},
|
||||
],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const { output, attempts } = await runLinkEntries({
|
||||
const output = await runLinkEntries({
|
||||
entries,
|
||||
ctx: params.ctx,
|
||||
url,
|
||||
config,
|
||||
});
|
||||
const chosen = attempts.find((attempt) => attempt.outcome === "success");
|
||||
urlDecisions.push({ url, attempts, chosen });
|
||||
if (output) outputs.push(output);
|
||||
}
|
||||
|
||||
const outcome = outputs.length > 0 ? "success" : "skipped";
|
||||
return { urls: links, outputs, decisions: [{ outcome, urls: urlDecisions }] };
|
||||
return { urls: links, outputs };
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
export type LinkUnderstandingOutput = {
|
||||
url: string;
|
||||
text: string;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type LinkUnderstandingModelDecision = {
|
||||
type: "cli";
|
||||
command?: string;
|
||||
outcome: "success" | "skipped" | "failed";
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type LinkUnderstandingUrlDecision = {
|
||||
url: string;
|
||||
attempts: LinkUnderstandingModelDecision[];
|
||||
chosen?: LinkUnderstandingModelDecision;
|
||||
};
|
||||
|
||||
export type LinkUnderstandingDecisionOutcome =
|
||||
| "success"
|
||||
| "skipped"
|
||||
| "disabled"
|
||||
| "scope-deny"
|
||||
| "no-links";
|
||||
|
||||
export type LinkUnderstandingDecision = {
|
||||
outcome: LinkUnderstandingDecisionOutcome;
|
||||
urls: LinkUnderstandingUrlDecision[];
|
||||
};
|
||||
@@ -112,6 +112,7 @@ export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
export { resolveAckReaction } from "../agents/identity.js";
|
||||
export type { ReplyPayload } from "../auto-reply/types.js";
|
||||
export type { ChunkMode } from "../auto-reply/chunk.js";
|
||||
export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js";
|
||||
export {
|
||||
buildPendingHistoryContextFromMap,
|
||||
@@ -154,6 +155,7 @@ export {
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
resolveGoogleChatGroupToolPolicy,
|
||||
resolveIMessageGroupToolPolicy,
|
||||
resolveSlackGroupToolPolicy,
|
||||
resolveTelegramGroupToolPolicy,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import { chunkMarkdownText, chunkText, resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||
import {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
resolveTextChunkLimit,
|
||||
} from "../../auto-reply/chunk.js";
|
||||
import {
|
||||
hasControlCommand,
|
||||
isControlCommandMessage,
|
||||
@@ -160,8 +167,11 @@ export function createPluginRuntime(): PluginRuntime {
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
resolveTextChunkLimit,
|
||||
hasControlCommand,
|
||||
resolveMarkdownTableMode,
|
||||
|
||||
@@ -35,8 +35,11 @@ type ResolveInboundDebounceMs =
|
||||
type ResolveCommandAuthorizedFromAuthorizers =
|
||||
typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers;
|
||||
type ResolveTextChunkLimit = typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit;
|
||||
type ResolveChunkMode = typeof import("../../auto-reply/chunk.js").resolveChunkMode;
|
||||
type ChunkMarkdownText = typeof import("../../auto-reply/chunk.js").chunkMarkdownText;
|
||||
type ChunkText = typeof import("../../auto-reply/chunk.js").chunkText;
|
||||
type ChunkTextWithMode = typeof import("../../auto-reply/chunk.js").chunkTextWithMode;
|
||||
type ChunkByNewline = typeof import("../../auto-reply/chunk.js").chunkByNewline;
|
||||
type ResolveMarkdownTableMode =
|
||||
typeof import("../../config/markdown-tables.js").resolveMarkdownTableMode;
|
||||
type ConvertMarkdownTables = typeof import("../../markdown/tables.js").convertMarkdownTables;
|
||||
@@ -173,8 +176,11 @@ export type PluginRuntime = {
|
||||
};
|
||||
channel: {
|
||||
text: {
|
||||
chunkByNewline: ChunkByNewline;
|
||||
chunkMarkdownText: ChunkMarkdownText;
|
||||
chunkText: ChunkText;
|
||||
chunkTextWithMode: ChunkTextWithMode;
|
||||
resolveChunkMode: ResolveChunkMode;
|
||||
resolveTextChunkLimit: ResolveTextChunkLimit;
|
||||
hasControlCommand: HasControlCommand;
|
||||
resolveMarkdownTableMode: ResolveMarkdownTableMode;
|
||||
|
||||
+11
-10
@@ -90,6 +90,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
let activeChatRunId: string | null = null;
|
||||
let historyLoaded = false;
|
||||
let isConnected = false;
|
||||
let wasDisconnected = false;
|
||||
let toolsExpanded = false;
|
||||
let showThinking = false;
|
||||
|
||||
@@ -584,20 +585,18 @@ export async function runTui(opts: TuiOptions) {
|
||||
|
||||
client.onConnected = () => {
|
||||
isConnected = true;
|
||||
const reconnected = wasDisconnected;
|
||||
wasDisconnected = false;
|
||||
setConnectionStatus("connected");
|
||||
void (async () => {
|
||||
await refreshAgents();
|
||||
updateHeader();
|
||||
if (!historyLoaded) {
|
||||
await loadHistory();
|
||||
setConnectionStatus("gateway connected", 4000);
|
||||
tui.requestRender();
|
||||
if (!autoMessageSent && autoMessage) {
|
||||
autoMessageSent = true;
|
||||
await sendMessage(autoMessage);
|
||||
}
|
||||
} else {
|
||||
setConnectionStatus("gateway reconnected", 4000);
|
||||
await loadHistory();
|
||||
setConnectionStatus(reconnected ? "gateway reconnected" : "gateway connected", 4000);
|
||||
tui.requestRender();
|
||||
if (!autoMessageSent && autoMessage) {
|
||||
autoMessageSent = true;
|
||||
await sendMessage(autoMessage);
|
||||
}
|
||||
updateFooter();
|
||||
tui.requestRender();
|
||||
@@ -606,6 +605,8 @@ export async function runTui(opts: TuiOptions) {
|
||||
|
||||
client.onDisconnected = (reason) => {
|
||||
isConnected = false;
|
||||
wasDisconnected = true;
|
||||
historyLoaded = false;
|
||||
const reasonLabel = reason?.trim() ? reason.trim() : "closed";
|
||||
setConnectionStatus(`gateway disconnected: ${reasonLabel}`, 5000);
|
||||
setActivityStatus("idle");
|
||||
|
||||
Reference in new issue
Block a user