Compare commits

..

2 Commits

Author SHA1 Message Date
Peter Steinberger
a36dc285d6 fix: guard console settings recursion (#1555) (thanks @travisp) 2026-01-24 03:12:19 +00:00
Travis
704a3ba51c Logging: guard console settings recursion 2026-01-24 03:06:34 +00:00
318 changed files with 3820 additions and 14899 deletions

View File

@@ -4,52 +4,23 @@ Docs: https://docs.clawd.bot
## 2026.1.23 (Unreleased)
### Highlights
- TTS: allow model-driven TTS tags by default for expressive audio replies (laughter, singing cues, etc.).
### Changes
- Gateway: add /tools/invoke HTTP endpoint for direct tool calls and document it. (#1575) Thanks @vignesh07.
- Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits.
- Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman.
- Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node).
- Heartbeat: add per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer.
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
- Docs: add emoji reaction guidance to AGENTS.md template. (#1591) Thanks @EnzeD.
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
- TTS: move Telegram TTS into core with auto-replies, commands, and gateway methods. (#1559) Thanks @Glucksberg.
### Fixes
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
- Routing/Cron: normalize agentId casing for bindings and cron payloads. (#1591)
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
- Messaging: mirror outbound sends into target session keys (threads + dmScope) and create session entries on send. (#1520)
- Sessions: normalize session key casing to lowercase for consistent routing.
- BlueBubbles: normalize group session keys for outbound mirroring. (#1520)
- Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
- Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops).
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
- Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4.
- Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
- Logging: guard console settings resolution to avoid recursion on config warnings. (#1555) Thanks @travisp.
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
- Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes.
- Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp).
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
- TUI: include Gateway slash commands in autocomplete and `/help`.
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
@@ -62,21 +33,15 @@ Docs: https://docs.clawd.bot
- CLI: explain when auth profiles are excluded by auth.order in probe details.
- CLI: drop the em dash when the banner tagline wraps to a second line.
- CLI: inline auth probe errors in status rows to reduce wrapping.
- Telegram: render markdown in media captions. (#1478)
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
- Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
- Daemon: use platform PATH delimiters when building minimal service paths.
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
- MS Teams (plugin): remove `.default` suffix from Bot Framework probe scope to avoid double-appending. (#1574) Thanks @Evizero.
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
- Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)
## 2026.1.22

View File

@@ -479,29 +479,28 @@ 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/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
<a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a>
<a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
<a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a>
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a>
<a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
<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/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/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/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/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/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/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/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/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/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a>
<a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a>
<a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a>
<a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
<a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a>
<a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/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=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a>
<a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a>
<a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
</p>

View File

@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
public let to: String
public let message: String
public let mediaurl: String?
public let mediaurls: [String]?
public let gifplayback: Bool?
public let channel: String?
public let accountid: String?
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
to: String,
message: String,
mediaurl: String?,
mediaurls: [String]?,
gifplayback: Bool?,
channel: String?,
accountid: String?,
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
self.to = to
self.message = message
self.mediaurl = mediaurl
self.mediaurls = mediaurls
self.gifplayback = gifplayback
self.channel = channel
self.accountid = accountid
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
case to
case message
case mediaurl = "mediaUrl"
case mediaurls = "mediaUrls"
case gifplayback = "gifPlayback"
case channel
case accountid = "accountId"
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
public let accountid: String?
public let replyaccountid: String?
public let threadid: String?
public let groupid: String?
public let groupchannel: String?
public let groupspace: String?
public let timeout: Int?
public let lane: String?
public let extrasystemprompt: String?
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
accountid: String?,
replyaccountid: String?,
threadid: String?,
groupid: String?,
groupchannel: String?,
groupspace: String?,
timeout: Int?,
lane: String?,
extrasystemprompt: String?,
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
self.accountid = accountid
self.replyaccountid = replyaccountid
self.threadid = threadid
self.groupid = groupid
self.groupchannel = groupchannel
self.groupspace = groupspace
self.timeout = timeout
self.lane = lane
self.extrasystemprompt = extrasystemprompt
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
case accountid = "accountId"
case replyaccountid = "replyAccountId"
case threadid = "threadId"
case groupid = "groupId"
case groupchannel = "groupChannel"
case groupspace = "groupSpace"
case timeout
case lane
case extrasystemprompt = "extraSystemPrompt"
@@ -964,7 +948,6 @@ public struct SessionsPreviewParams: Codable, Sendable {
public struct SessionsResolveParams: Codable, Sendable {
public let key: String?
public let sessionid: String?
public let label: String?
public let agentid: String?
public let spawnedby: String?
@@ -973,7 +956,6 @@ public struct SessionsResolveParams: Codable, Sendable {
public init(
key: String?,
sessionid: String?,
label: String?,
agentid: String?,
spawnedby: String?,
@@ -981,7 +963,6 @@ public struct SessionsResolveParams: Codable, Sendable {
includeunknown: Bool?
) {
self.key = key
self.sessionid = sessionid
self.label = label
self.agentid = agentid
self.spawnedby = spawnedby
@@ -990,7 +971,6 @@ public struct SessionsResolveParams: Codable, Sendable {
}
private enum CodingKeys: String, CodingKey {
case key
case sessionid = "sessionId"
case label
case agentid = "agentId"
case spawnedby = "spawnedBy"

View File

@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
public let to: String
public let message: String
public let mediaurl: String?
public let mediaurls: [String]?
public let gifplayback: Bool?
public let channel: String?
public let accountid: String?
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
to: String,
message: String,
mediaurl: String?,
mediaurls: [String]?,
gifplayback: Bool?,
channel: String?,
accountid: String?,
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
self.to = to
self.message = message
self.mediaurl = mediaurl
self.mediaurls = mediaurls
self.gifplayback = gifplayback
self.channel = channel
self.accountid = accountid
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
case to
case message
case mediaurl = "mediaUrl"
case mediaurls = "mediaUrls"
case gifplayback = "gifPlayback"
case channel
case accountid = "accountId"
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
public let accountid: String?
public let replyaccountid: String?
public let threadid: String?
public let groupid: String?
public let groupchannel: String?
public let groupspace: String?
public let timeout: Int?
public let lane: String?
public let extrasystemprompt: String?
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
accountid: String?,
replyaccountid: String?,
threadid: String?,
groupid: String?,
groupchannel: String?,
groupspace: String?,
timeout: Int?,
lane: String?,
extrasystemprompt: String?,
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
self.accountid = accountid
self.replyaccountid = replyaccountid
self.threadid = threadid
self.groupid = groupid
self.groupchannel = groupchannel
self.groupspace = groupspace
self.timeout = timeout
self.lane = lane
self.extrasystemprompt = extrasystemprompt
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
case accountid = "accountId"
case replyaccountid = "replyAccountId"
case threadid = "threadId"
case groupid = "groupId"
case groupchannel = "groupChannel"
case groupspace = "groupSpace"
case timeout
case lane
case extrasystemprompt = "extraSystemPrompt"
@@ -964,7 +948,6 @@ public struct SessionsPreviewParams: Codable, Sendable {
public struct SessionsResolveParams: Codable, Sendable {
public let key: String?
public let sessionid: String?
public let label: String?
public let agentid: String?
public let spawnedby: String?
@@ -973,7 +956,6 @@ public struct SessionsResolveParams: Codable, Sendable {
public init(
key: String?,
sessionid: String?,
label: String?,
agentid: String?,
spawnedby: String?,
@@ -981,7 +963,6 @@ public struct SessionsResolveParams: Codable, Sendable {
includeunknown: Bool?
) {
self.key = key
self.sessionid = sessionid
self.label = label
self.agentid = agentid
self.spawnedby = spawnedby
@@ -990,7 +971,6 @@ public struct SessionsResolveParams: Codable, Sendable {
}
private enum CodingKeys: String, CodingKey {
case key
case sessionid = "sessionId"
case label
case agentid = "agentId"
case spawnedby = "spawnedBy"

View File

@@ -20,7 +20,7 @@ services:
[
"node",
"dist/index.js",
"gateway",
"gateway-daemon",
"--bind",
"${CLAWDBOT_GATEWAY_BIND:-lan}",
"--port",

View File

@@ -263,15 +263,15 @@ Run history:
clawdbot cron runs --id <jobId> --limit 50
```
Immediate system event without creating a job:
Immediate wake without creating a job:
```bash
clawdbot system event --mode now --text "Next heartbeat: check battery."
clawdbot wake --mode now --text "Next heartbeat: check battery."
```
## Gateway API surface
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
- `cron.run` (force or due), `cron.runs`
For immediate system events without a job, use [`clawdbot system event`](/cli/system).
- `wake` (enqueue system event + optional heartbeat)
## Troubleshooting

View File

@@ -271,4 +271,4 @@ clawdbot cron add \
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
- [System](/cli/system) - system events + heartbeat controls
- [Wake](/cli/wake) - manual wake command

View File

@@ -44,7 +44,6 @@ clawdbot channels logout --channel whatsapp
- Run `clawdbot status --deep` for a broad probe.
- Use `clawdbot doctor` for guided fixes.
- `clawdbot channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude Code CLI.
## Capabilities probe

View File

@@ -29,7 +29,6 @@ This page describes the current CLI behavior. If commands change, update this do
- [`sessions`](/cli/sessions)
- [`gateway`](/cli/gateway)
- [`logs`](/cli/logs)
- [`system`](/cli/system)
- [`models`](/cli/models)
- [`memory`](/cli/memory)
- [`nodes`](/cli/nodes)
@@ -39,6 +38,7 @@ This page describes the current CLI behavior. If commands change, update this do
- [`sandbox`](/cli/sandbox)
- [`tui`](/cli/tui)
- [`browser`](/cli/browser)
- [`wake`](/cli/wake)
- [`cron`](/cli/cron)
- [`dns`](/cli/dns)
- [`docs`](/cli/docs)
@@ -145,10 +145,6 @@ clawdbot [--dev] [--profile <name>] <command>
restart
run
logs
system
event
heartbeat last|enable|disable
presence
models
list
status
@@ -164,6 +160,7 @@ clawdbot [--dev] [--profile <name>] <command>
list
recreate
explain
wake
cron
status
list
@@ -766,9 +763,9 @@ Options:
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
- `clear`: `--provider <name>`, `--agent <id>`
## System
## Cron + wake
### `system event`
### `wake`
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
Required:
@@ -779,21 +776,7 @@ Options:
- `--json`
- `--url`, `--token`, `--timeout`, `--expect-final`
### `system heartbeat last|enable|disable`
Heartbeat controls (Gateway RPC).
Options:
- `--json`
- `--url`, `--token`, `--timeout`, `--expect-final`
### `system presence`
List system presence entries (Gateway RPC).
Options:
- `--json`
- `--url`, `--token`, `--timeout`, `--expect-final`
## Cron
### `cron`
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
Subcommands:

View File

@@ -23,24 +23,6 @@ Common use cases:
Execution is still guarded by **exec approvals** and peragent allowlists on the
node host, so you can keep command access scoped and explicit.
## Browser proxy (zero-config)
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
disabled on the node. This lets the agent use browser automation on that node
without extra configuration.
Disable it on the node if needed:
```json5
{
nodeHost: {
browserProxy: {
enabled: false
}
}
}
```
## Run (foreground)
```bash

View File

@@ -1,55 +0,0 @@
---
summary: "CLI reference for `clawdbot system` (system events, heartbeat, presence)"
read_when:
- You want to enqueue a system event without creating a cron job
- You need to enable or disable heartbeats
- You want to inspect system presence entries
---
# `clawdbot system`
System-level helpers for the Gateway: enqueue system events, control heartbeats,
and view presence.
## Common commands
```bash
clawdbot system event --text "Check for urgent follow-ups" --mode now
clawdbot system heartbeat enable
clawdbot system heartbeat last
clawdbot system presence
```
## `system event`
Enqueue a system event on the **main** session. The next heartbeat will inject
it as a `System:` line in the prompt. Use `--mode now` to trigger the heartbeat
immediately; `next-heartbeat` waits for the next scheduled tick.
Flags:
- `--text <text>`: required system event text.
- `--mode <mode>`: `now` or `next-heartbeat` (default).
- `--json`: machine-readable output.
## `system heartbeat last|enable|disable`
Heartbeat controls:
- `last`: show the last heartbeat event.
- `enable`: turn heartbeats back on (use this if they were disabled).
- `disable`: pause heartbeats.
Flags:
- `--json`: machine-readable output.
## `system presence`
List the current system presence entries the Gateway knows about (nodes,
instances, and similar status lines).
Flags:
- `--json`: machine-readable output.
## Notes
- Requires a running Gateway reachable by your current config (local or remote).
- System events are ephemeral and not persisted across restarts.

35
docs/cli/wake.md Normal file
View File

@@ -0,0 +1,35 @@
---
summary: "CLI reference for `clawdbot wake` (enqueue a system event and optionally trigger an immediate heartbeat)"
read_when:
- You want to “poke” a running Gateway to process a system event
- You use `wake` with cron jobs or remote nodes
---
# `clawdbot wake`
Enqueue a system event on the Gateway and optionally trigger an immediate heartbeat.
This is a lightweight “poke” for automation flows where you dont want to run a full command, but you do want the Gateway to react quickly.
Related:
- Cron jobs: [Cron](/cli/cron)
- Gateway heartbeat: [Heartbeat](/gateway/heartbeat)
## Common commands
```bash
clawdbot wake --text "sync"
clawdbot wake --text "sync" --mode now
```
## Flags
- `--text <text>`: system event text.
- `--mode <mode>`: `now` or `next-heartbeat` (default).
- `--json`: machine-readable output.
## Notes
- Requires a running Gateway reachable by your current config (local or remote).
- If youre using sandboxing, `wake` still targets the Gateway; sandboxing does not block the command itself.

8
docs/compaction.md Normal file
View File

@@ -0,0 +1,8 @@
---
summary: "Alias for compaction docs"
read_when:
- You looked for /compaction; canonical doc lives in /concepts/compaction
---
# Compaction
Canonical compaction docs live in [Compaction](/concepts/compaction).

View File

@@ -56,20 +56,19 @@ Row shape (JSON):
Fetch transcript for one session.
Parameters:
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
- `sessionKey` (required)
- `limit?: number` max messages (server clamps)
- `includeTools?: boolean` (default false)
Behavior:
- `includeTools=false` filters `role: "toolResult"` messages.
- Returns messages array in the raw transcript format.
- When given a `sessionId`, Clawdbot resolves it to the corresponding session key (missing ids error).
## sessions_send
Send a message into another session.
Parameters:
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
- `sessionKey` (required)
- `message` (required)
- `timeoutSeconds?: number` (default >0; 0 = fire-and-forget)

View File

@@ -66,12 +66,12 @@ To inspect how much each injected file contributes (raw vs injected, truncation,
## Time handling
The system prompt includes a dedicated **Current Date & Time** section when the
user timezone is known. To keep the prompt cache-stable, it now only includes
the **time zone** (no dynamic clock or time format).
The system prompt includes a dedicated **Current Date & Time** section when user
time or timezone is known. It is explicit about:
Use `session_status` when the agent needs the current time; the status card
includes a timestamp line.
- The users **local time** (already converted).
- The **time zone** used for the conversion.
- The **time format** (12-hour / 24-hour).
Configure with:

View File

@@ -7,8 +7,8 @@ read_when:
# Date & Time
Clawdbot defaults to **host-local time for transport timestamps** and **user timezone only in the system prompt**.
Provider timestamps are preserved so tools keep their native semantics (current time is available via `session_status`).
Clawdbot defaults to **host-local time for transport timestamps** and **user-local time only in the system prompt**.
Provider timestamps are preserved so tools keep their native semantics.
## Message envelopes (local by default)
@@ -63,16 +63,16 @@ You can override this behavior:
## System prompt: Current Date & Time
If the user timezone is known, the system prompt includes a dedicated
**Current Date & Time** section with the **time zone only** (no clock/time format)
to keep prompt caching stable:
If the user timezone or local time is known, the system prompt includes a dedicated
**Current Date & Time** section:
```
Time zone: America/Chicago
Thursday, January 15th, 2026 — 3:07 PM (America/Chicago)
Time format: 12-hour
```
When the agent needs the current time, use the `session_status` tool; the status
card includes a timestamp line.
If only the timezone is known, we still include the section and instruct the model
to assume UTC for unknown time references.
## System event lines (local by default)

View File

@@ -387,7 +387,7 @@
},
{
"source": "/faq",
"destination": "/help/faq"
"destination": "/start/faq"
},
{
"source": "/gateway-lock",
@@ -449,6 +449,10 @@
"source": "/location-command",
"destination": "/nodes/location-command"
},
{
"source": "/logging",
"destination": "/gateway/logging"
},
{
"source": "/lore",
"destination": "/start/lore"
@@ -757,13 +761,21 @@
"source": "/wizard",
"destination": "/start/wizard"
},
{
"source": "/install/node",
"destination": "/install#nodejs--npm-path-sanity"
},
{
"source": "/install/node/",
"destination": "/install#nodejs--npm-path-sanity"
},
{
"source": "/start/faq",
"destination": "/help/faq"
"destination": "/help"
},
{
"source": "/start/faq/",
"destination": "/help/faq"
"destination": "/help"
},
{
"source": "/oauth",
@@ -838,12 +850,12 @@
"cli/memory",
"cli/models",
"cli/logs",
"cli/system",
"cli/nodes",
"cli/approvals",
"cli/gateway",
"cli/tui",
"cli/voicecall",
"cli/wake",
"cli/cron",
"cli/dns",
"cli/docs",
@@ -904,7 +916,6 @@
"gateway/configuration-examples",
"gateway/authentication",
"gateway/openai-http-api",
"gateway/tools-invoke-http-api",
"gateway/cli-backends",
"gateway/local-models",
"gateway/background-process",
@@ -1034,7 +1045,6 @@
"platforms/android",
"platforms/windows",
"platforms/linux",
"platforms/fly",
"platforms/hetzner",
"platforms/exe-dev"
]

View File

@@ -74,5 +74,5 @@ See [Configuration: Env var substitution](/gateway/configuration#env-var-substit
## Related
- [Gateway configuration](/gateway/configuration)
- [FAQ: env vars and .env loading](/help/faq#env-vars-and-env-loading)
- [FAQ: env vars and .env loading](/start/faq#env-vars-and-env-loading)
- [Models overview](/concepts/models)

View File

@@ -1446,65 +1446,6 @@ active agents `identity.emoji` when set, otherwise `"👀"`. Set it to `""` t
`removeAckAfterReply` removes the bots ack reaction after a reply is sent
(Slack/Discord/Telegram only). Default: `false`.
#### `messages.tts`
Enable text-to-speech for outbound replies. When on, Clawdbot generates audio
using ElevenLabs or OpenAI and attaches it to responses. Telegram uses Opus
voice notes; other channels send MP3 audio.
```json5
{
messages: {
tts: {
enabled: true,
mode: "final", // final | all (include tool/block replies)
provider: "elevenlabs",
summaryModel: "openai/gpt-4.1-mini",
modelOverrides: {
enabled: true
},
maxTextLength: 4000,
timeoutMs: 30000,
prefsPath: "~/.clawdbot/settings/tts.json",
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
languageCode: "en",
voiceSettings: {
stability: 0.5,
similarityBoost: 0.75,
style: 0.0,
useSpeakerBoost: true,
speed: 1.0
}
},
openai: {
apiKey: "openai_api_key",
model: "gpt-4o-mini-tts",
voice: "alloy"
}
}
}
}
```
Notes:
- `messages.tts.enabled` can be overridden by local user prefs (see `/tts on`, `/tts off`).
- `prefsPath` stores local overrides (enabled/provider/limit/summarize).
- `maxTextLength` is a hard cap for TTS input; summaries are truncated to fit.
- `summaryModel` overrides `agents.defaults.model.primary` for auto-summary.
- Accepts `provider/model` or an alias from `agents.defaults.models`.
- `modelOverrides` enables model-driven overrides like `[[tts:...]]` tags (on by default).
- `/tts limit` and `/tts summary` control per-user summarization settings.
- `apiKey` values fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
- `elevenlabs.baseUrl` overrides the ElevenLabs API base URL.
- `elevenlabs.voiceSettings` supports `stability`/`similarityBoost`/`style` (0..1),
`useSpeakerBoost`, and `speed` (0.5..2.0).
### `talk`
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.

View File

@@ -92,14 +92,6 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
}
```
### Scope and precedence
- `agents.defaults.heartbeat` sets global heartbeat behavior.
- `agents.list[].heartbeat` merges on top; if any agent has a `heartbeat` block, **only those agents** run heartbeats.
- `channels.defaults.heartbeat` sets visibility defaults for all channels.
- `channels.<channel>.heartbeat` overrides channel defaults.
- `channels.<channel>.accounts.<id>.heartbeat` (multi-account channels) overrides per-channel settings.
### Per-agent heartbeats
If any `agents.list[]` entry includes a `heartbeat` block, **only those agents**
@@ -164,78 +156,12 @@ Example: two agents, only the second agent runs heartbeats.
- Heartbeat-only replies do **not** keep the session alive; the last `updatedAt`
is restored so idle expiry behaves normally.
## Visibility controls
By default, `HEARTBEAT_OK` acknowledgments are suppressed while alert content is
delivered. You can adjust this per channel or per account:
```yaml
channels:
defaults:
heartbeat:
showOk: false # Hide HEARTBEAT_OK (default)
showAlerts: true # Show alert messages (default)
useIndicator: true # Emit indicator events (default)
telegram:
heartbeat:
showOk: true # Show OK acknowledgments on Telegram
whatsapp:
accounts:
work:
heartbeat:
showAlerts: false # Suppress alert delivery for this account
```
Precedence: per-account → per-channel → channel defaults → built-in defaults.
### What each flag does
- `showOk`: sends a `HEARTBEAT_OK` acknowledgment when the model returns an OK-only reply.
- `showAlerts`: sends the alert content when the model returns a non-OK reply.
- `useIndicator`: emits indicator events for UI status surfaces.
If **all three** are false, Clawdbot skips the heartbeat run entirely (no model call).
### Per-channel vs per-account examples
```yaml
channels:
defaults:
heartbeat:
showOk: false
showAlerts: true
useIndicator: true
slack:
heartbeat:
showOk: true # all Slack accounts
accounts:
ops:
heartbeat:
showAlerts: false # suppress alerts for the ops account only
telegram:
heartbeat:
showOk: true
```
### Common patterns
| Goal | Config |
| --- | --- |
| Default behavior (silent OKs, alerts on) | *(no config needed)* |
| Fully silent (no messages, no indicator) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: false }` |
| Indicator-only (no messages) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: true }` |
| OKs in one channel only | `channels.telegram.heartbeat: { showOk: true }` |
## HEARTBEAT.md (optional)
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
safe to include every 30 minutes.
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
If the file is missing, the heartbeat still runs and the model decides what to do.
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
Example `HEARTBEAT.md`:
@@ -269,7 +195,7 @@ Safety note: dont put secrets (API keys, phone numbers, private tokens) into
You can enqueue a system event and trigger an immediate heartbeat with:
```bash
clawdbot system event --text "Check for urgent follow-ups" --mode now
clawdbot wake --text "Check for urgent follow-ups" --mode now
```
If multiple agents have `heartbeat` configured, a manual wake runs each of those

View File

@@ -30,7 +30,6 @@ pnpm gateway:watch
- The same port also serves HTTP (control UI, hooks, A2UI). Single-port multiplex.
- OpenAI Chat Completions (HTTP): [`/v1/chat/completions`](/gateway/openai-http-api).
- OpenResponses (HTTP): [`/v1/responses`](/gateway/openresponses-http-api).
- Tools Invoke (HTTP): [`/tools/invoke`](/gateway/tools-invoke-http-api).
- Starts a Canvas file server by default on `canvasHost.port` (default `18793`), serving `http://<gateway-host>:18793/__clawdbot__/canvas/` from `~/clawd/canvas`. Disable with `canvasHost.enabled=false` or `CLAWDBOT_SKIP_CANVAS_HOST=1`.
- Logs to stdout; use launchd/systemd to keep it alive and rotate logs.
- Pass `--verbose` to mirror debug logging (handshakes, req/res, events) from the log file into stdio when troubleshooting.

View File

@@ -1,79 +0,0 @@
---
summary: "Invoke a single tool directly via the Gateway HTTP endpoint"
read_when:
- Calling tools without running a full agent turn
- Building automations that need tool policy enforcement
---
# Tools Invoke (HTTP)
Clawdbots Gateway exposes a simple HTTP endpoint for invoking a single tool directly. It is always enabled, but gated by Gateway auth and tool policy.
- `POST /tools/invoke`
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/tools/invoke`
Default max payload size is 2 MB.
## Authentication
Uses the Gateway auth configuration. Send a bearer token:
- `Authorization: Bearer <token>`
Notes:
- When `gateway.auth.mode="token"`, use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `CLAWDBOT_GATEWAY_PASSWORD`).
## Request body
```json
{
"tool": "sessions_list",
"action": "json",
"args": {},
"sessionKey": "main",
"dryRun": false
}
```
Fields:
- `tool` (string, required): tool name to invoke.
- `action` (string, optional): mapped into args if the tool schema supports `action` and the args payload omitted it.
- `args` (object, optional): tool-specific arguments.
- `sessionKey` (string, optional): target session key. If omitted or `"main"`, the Gateway uses the configured main session key (honors `session.mainKey` and default agent, or `global` in global scope).
- `dryRun` (boolean, optional): reserved for future use; currently ignored.
## Policy + routing behavior
Tool availability is filtered through the same policy chain used by Gateway agents:
- `tools.profile` / `tools.byProvider.profile`
- `tools.allow` / `tools.byProvider.allow`
- `agents.<id>.tools.allow` / `agents.<id>.tools.byProvider.allow`
- group policies (if the session key maps to a group or channel)
- subagent policy (when invoking with a subagent session key)
If a tool is not allowed by policy, the endpoint returns **404**.
To help group policies resolve context, you can optionally set:
- `x-clawdbot-message-channel: <channel>` (example: `slack`, `telegram`)
- `x-clawdbot-account-id: <accountId>` (when multiple accounts exist)
## Responses
- `200``{ ok: true, result }`
- `400``{ ok: false, error: { type, message } }` (invalid request or tool error)
- `401` → unauthorized
- `404` → tool not available (not found or not allowlisted)
- `405` → method not allowed
## Example
```bash
curl -sS http://127.0.0.1:18789/tools/invoke \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"tool": "sessions_list",
"action": "json",
"args": {}
}'
```

View File

@@ -7,7 +7,7 @@ read_when:
When Clawdbot misbehaves, here's how to fix it.
Start with the FAQs [First 60 seconds](/help/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics.
Start with the FAQs [First 60 seconds](/start/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics.
Provider-specific shortcuts: [/channels/troubleshooting](/channels/troubleshooting)
@@ -31,34 +31,6 @@ See also: [Health checks](/gateway/health) and [Logging](/logging).
## Common Issues
### OAuth token refresh failed (Anthropic Claude subscription)
This means the stored Anthropic OAuth token expired and the refresh failed.
If youre on a Claude subscription (no API key), the most reliable fix is to
switch to a **Claude Code setup-token** or re-sync Claude Code CLI OAuth on the
**gateway host**.
**Recommended (setup-token):**
```bash
# Run on the gateway host (runs Claude Code CLI)
clawdbot models auth setup-token --provider anthropic
clawdbot models status
```
If you generated the token elsewhere:
```bash
clawdbot models auth paste-token --provider anthropic
clawdbot models status
```
**If you want to keep OAuth reuse:**
log in with Claude Code CLI on the gateway host, then run `clawdbot models status`
to sync the refreshed token into Clawdbots auth store.
More detail: [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
### Control UI fails on HTTP ("device identity required" / "connect failed")
If you open the dashboard over plain HTTP (e.g. `http://<lan-ip>:18789/` or

File diff suppressed because it is too large Load Diff

View File

@@ -1,340 +0,0 @@
---
title: Fly.io
description: Deploy Clawdbot on Fly.io
---
# Fly.io Deployment
**Goal:** Clawdbot Gateway running on a [Fly.io](https://fly.io) machine with persistent storage, automatic HTTPS, and Discord/channel access.
## What you need
- [flyctl CLI](https://fly.io/docs/hands-on/install-flyctl/) installed
- Fly.io account (free tier works)
- Model auth: Anthropic API key (or other provider keys)
- Channel credentials: Discord bot token, Telegram token, etc.
## Beginner quick path
1. Clone repo → customize `fly.toml`
2. Create app + volume → set secrets
3. Deploy with `fly deploy`
4. SSH in to create config or use Control UI
## 1) Create the Fly app
```bash
# Clone the repo
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
# Create a new Fly app (pick your own name)
fly apps create my-clawdbot
# Create a persistent volume (1GB is usually enough)
fly volumes create clawdbot_data --size 1 --region iad
```
**Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose).
## 2) Configure fly.toml
Edit `fly.toml` to match your app name and requirements:
```toml
app = "my-clawdbot" # Your app name
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
CLAWDBOT_PREFER_PNPM = "1"
CLAWDBOT_STATE_DIR = "/data"
NODE_OPTIONS = "--max-old-space-size=1536"
[processes]
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = false
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"
[mounts]
source = "clawdbot_data"
destination = "/data"
```
**Key settings:**
| Setting | Why |
|---------|-----|
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
| `--allow-unconfigured` | Starts without a config file (you'll create one after) |
| `memory = "2048mb"` | 512MB is too small; 2GB recommended |
| `CLAWDBOT_STATE_DIR = "/data"` | Persists state on the volume |
## 3) Set secrets
```bash
# Required: Gateway token (for non-loopback binding)
fly secrets set CLAWDBOT_GATEWAY_TOKEN=$(openssl rand -hex 32)
# Model provider API keys
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
# Optional: Other providers
fly secrets set OPENAI_API_KEY=sk-...
fly secrets set GOOGLE_API_KEY=...
# Channel tokens
fly secrets set DISCORD_BOT_TOKEN=MTQ...
```
**Notes:**
- Non-loopback binds (`--bind lan`) require `CLAWDBOT_GATEWAY_TOKEN` for security.
- Treat these tokens like passwords.
## 4) Deploy
```bash
fly deploy
```
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
After deployment, verify:
```bash
fly status
fly logs
```
You should see:
```
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
[discord] logged in to discord as xxx
```
## 5) Create config file
SSH into the machine to create a proper config:
```bash
fly ssh console
```
Create the config directory and file:
```bash
mkdir -p /data
cat > /data/clawdbot.json << 'EOF'
{
"agents": {
"defaults": {
"model": {
"primary": "anthropic/claude-opus-4-5",
"fallbacks": ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"]
},
"maxConcurrent": 4
},
"list": [
{
"id": "main",
"default": true
}
]
},
"auth": {
"profiles": {
"anthropic:default": { "mode": "token", "provider": "anthropic" },
"openai:default": { "mode": "token", "provider": "openai" }
}
},
"bindings": [
{
"agentId": "main",
"match": { "channel": "discord" }
}
],
"channels": {
"discord": {
"enabled": true,
"groupPolicy": "allowlist",
"guilds": {
"YOUR_GUILD_ID": {
"channels": { "general": { "allow": true } },
"requireMention": false
}
}
}
},
"gateway": {
"mode": "local",
"bind": "auto"
},
"meta": {
"lastTouchedVersion": "2026.1.23"
}
}
EOF
```
**Note:** With `CLAWDBOT_STATE_DIR=/data`, the config path is `/data/clawdbot.json`.
**Note:** The Discord token can come from either:
- Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets)
- Config file: `channels.discord.token`
If using env var, no need to add token to config. The gateway reads `DISCORD_BOT_TOKEN` automatically.
Restart to apply:
```bash
exit
fly machine restart <machine-id>
```
## 6) Access the Gateway
### Control UI
Open in browser:
```bash
fly open
```
Or visit `https://my-clawdbot.fly.dev/`
Paste your gateway token (the one from `CLAWDBOT_GATEWAY_TOKEN`) to authenticate.
### Logs
```bash
fly logs # Live logs
fly logs --no-tail # Recent logs
```
### SSH Console
```bash
fly ssh console
```
## Troubleshooting
### "App is not listening on expected address"
The gateway is binding to `127.0.0.1` instead of `0.0.0.0`.
**Fix:** Add `--bind lan` to your process command in `fly.toml`.
### OOM / Memory Issues
Container keeps restarting or getting killed. Signs: `SIGABRT`, `v8::internal::Runtime_AllocateInYoungGeneration`, or silent restarts.
**Fix:** Increase memory in `fly.toml`:
```toml
[[vm]]
memory = "2048mb"
```
Or update an existing machine:
```bash
fly machine update <machine-id> --vm-memory 2048 -y
```
**Note:** 512MB is too small. 1GB may work but can OOM under load or with verbose logging. **2GB is recommended.**
### Gateway Lock Issues
Gateway refuses to start with "already running" errors.
This happens when the container restarts but the PID lock file persists on the volume.
**Fix:** Delete the lock file:
```bash
fly ssh console --command "rm -f /data/gateway.*.lock"
fly machine restart <machine-id>
```
The lock file is at `/data/gateway.*.lock` (not in a subdirectory).
### Config Not Being Read
If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/.clawdbot/clawdbot.json` should be read on restart.
Verify the config exists:
```bash
fly ssh console --command "cat /data/.clawdbot/clawdbot.json"
```
### Writing Config via SSH
The `fly ssh console -C` command doesn't support shell redirection. To write a config file:
```bash
# Use echo + tee (pipe from local to remote)
echo '{"your":"config"}' | fly ssh console -C "tee /data/.clawdbot/clawdbot.json"
# Or use sftp
fly sftp shell
> put /local/path/config.json /data/.clawdbot/clawdbot.json
```
**Note:** `fly sftp` may fail if the file already exists. Delete first:
```bash
fly ssh console --command "rm /data/.clawdbot/clawdbot.json"
```
## Updates
```bash
# Pull latest changes
git pull
# Redeploy
fly deploy
# Check health
fly status
fly logs
```
### Updating Machine Command
If you need to change the startup command without a full redeploy:
```bash
# Get machine ID
fly machines list
# Update command
fly machine update <machine-id> --command "node dist/index.js gateway --port 3000 --bind lan" -y
# Or with memory increase
fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.js gateway --port 3000 --bind lan" -y
```
**Note:** After `fly deploy`, the machine command may reset to what's in `fly.toml`. If you made manual changes, re-apply them after deploy.
## Notes
- Fly.io uses **x86 architecture** (not ARM)
- The Dockerfile is compatible with both architectures
- For WhatsApp/Telegram onboarding, use `fly ssh console`
- Persistent data lives on the volume at `/data`
## Cost
With the recommended config (`shared-cpu-2x`, 2GB RAM):
- ~$10-15/month depending on usage
- Free tier includes some allowance
See [Fly.io pricing](https://fly.io/docs/about/pricing/) for details.

View File

@@ -184,7 +184,7 @@ services:
[
"node",
"dist/index.js",
"gateway",
"gateway-daemon",
"--bind",
"${CLAWDBOT_GATEWAY_BIND}",
"--port",

View File

@@ -23,7 +23,6 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
## VPS & hosting
- Fly.io: [Fly.io](/platforms/fly)
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)

View File

@@ -62,7 +62,6 @@ Plugins can register:
- Background services
- Optional config validation
- **Skills** (by listing `skills` directories in the plugin manifest)
- **Auto-reply commands** (execute without invoking the AI agent)
Plugins run **inprocess** with the Gateway, so treat them as trusted code.
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
@@ -495,65 +494,6 @@ export default function (api) {
}
```
### Register auto-reply commands
Plugins can register custom slash commands that execute **without invoking the
AI agent**. This is useful for toggle commands, status checks, or quick actions
that don't need LLM processing.
```ts
export default function (api) {
api.registerCommand({
name: "mystatus",
description: "Show plugin status",
handler: (ctx) => ({
text: `Plugin is running! Channel: ${ctx.channel}`,
}),
});
}
```
Command handler context:
- `senderId`: The sender's ID (if available)
- `channel`: The channel where the command was sent
- `isAuthorizedSender`: Whether the sender is an authorized user
- `args`: Arguments passed after the command (if `acceptsArgs: true`)
- `commandBody`: The full command text
- `config`: The current Clawdbot config
Command options:
- `name`: Command name (without the leading `/`)
- `description`: Help text shown in command lists
- `acceptsArgs`: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlers
- `requireAuth`: Whether to require authorized sender (default: true)
- `handler`: Function that returns `{ text: string }` (can be async)
Example with authorization and arguments:
```ts
api.registerCommand({
name: "setmode",
description: "Set plugin mode",
acceptsArgs: true,
requireAuth: true,
handler: async (ctx) => {
const mode = ctx.args?.trim() || "default";
await saveMode(mode);
return { text: `Mode set to: ${mode}` };
},
});
```
Notes:
- Plugin commands are processed **before** built-in commands and the AI agent
- Commands are registered globally and work across all channels
- Command names are case-insensitive (`/MyStatus` matches `/mystatus`)
- Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
- Reserved command names (like `help`, `status`, `reset`, etc.) cannot be overridden by plugins
- Duplicate command registration across plugins will fail with a diagnostic error
### Register background services
```ts

View File

@@ -100,7 +100,6 @@ clawdbot onboard --auth-choice claude-cli
## Notes
- Generate the setup-token with `claude setup-token` and paste it, or run `clawdbot models auth setup-token` on the gateway host.
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token or resync Claude Code CLI OAuth on the gateway host. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription).
- Clawdbot writes `auth.profiles["anthropic:claude-cli"].mode` as `"oauth"` so the profile
accepts both OAuth and setup-token credentials. Older configs using `"token"` are
auto-migrated on load.

View File

@@ -1,73 +0,0 @@
---
title: Outbound Session Mirroring Refactor (Issue #1520)
description: Track outbound session mirroring refactor notes, decisions, tests, and open items.
---
# Outbound Session Mirroring Refactor (Issue #1520)
## Status
- In progress.
- Core + plugin channel routing updated for outbound mirroring.
- Gateway send now derives target session when sessionKey is omitted.
## Context
Outbound sends were mirrored into the *current* agent session (tool session key) rather than the target channel session. Inbound routing uses channel/peer session keys, so outbound responses landed in the wrong session and first-contact targets often lacked session entries.
## Goals
- Mirror outbound messages into the target channel session key.
- Create session entries on outbound when missing.
- Keep thread/topic scoping aligned with inbound session keys.
- Cover core channels plus bundled extensions.
## Implementation Summary
- New outbound session routing helper:
- `src/infra/outbound/outbound-session.ts`
- `resolveOutboundSessionRoute` builds target sessionKey using `buildAgentSessionKey` (dmScope + identityLinks).
- `ensureOutboundSessionEntry` writes minimal `MsgContext` via `recordSessionMetaFromInbound`.
- `runMessageAction` (send) derives target sessionKey and passes it to `executeSendAction` for mirroring.
- `message-tool` no longer mirrors directly; it only resolves agentId from the current session key.
- Plugin send path mirrors via `appendAssistantMessageToSessionTranscript` using the derived sessionKey.
- Gateway send derives a target session key when none is provided (default agent), and ensures a session entry.
## Thread/Topic Handling
- Slack: replyTo/threadId -> `resolveThreadSessionKeys` (suffix).
- Discord: threadId/replyTo -> `resolveThreadSessionKeys` with `useSuffix=false` to match inbound (thread channel id already scopes session).
- Telegram: topic IDs map to `chatId:topic:<id>` via `buildTelegramGroupPeerId`.
## Extensions Covered
- Matrix, MS Teams, Mattermost, BlueBubbles, Nextcloud Talk, Zalo, Zalo Personal, Nostr, Tlon.
- Notes:
- Mattermost targets now strip `@` for DM session key routing.
- Zalo Personal uses DM peer kind for 1:1 targets (group only when `group:` is present).
- BlueBubbles group targets strip `chat_*` prefixes to match inbound session keys.
## Decisions
- **Gateway send session derivation**: if `sessionKey` is provided, use it. If omitted, derive a sessionKey from target + default agent and mirror there.
- **Session entry creation**: always use `recordSessionMetaFromInbound` with `Provider/From/To/ChatType/AccountId/Originating*` aligned to inbound formats.
- **Target normalization**: outbound routing uses resolved targets (post `resolveChannelTarget`) when available.
- **Session key casing**: canonicalize session keys to lowercase on write and during migrations.
## Tests Added/Updated
- `src/infra/outbound/outbound-session.test.ts`
- Slack thread session key.
- Telegram topic session key.
- dmScope identityLinks with Discord.
- `src/agents/tools/message-tool.test.ts`
- Derives agentId from session key (no sessionKey passed through).
- `src/gateway/server-methods/send.test.ts`
- Derives session key when omitted and creates session entry.
## Open Items / Follow-ups
- Voice-call plugin uses custom `voice:<phone>` session keys. Outbound mapping is not standardized here; if message-tool should support voice-call sends, add explicit mapping.
- Confirm if any external plugin uses non-standard `From/To` formats beyond the bundled set.
## Files Touched
- `src/infra/outbound/outbound-session.ts`
- `src/infra/outbound/outbound-send-service.ts`
- `src/infra/outbound/message-action-runner.ts`
- `src/agents/tools/message-tool.ts`
- `src/gateway/server-methods/send.ts`
- Tests in:
- `src/infra/outbound/outbound-session.test.ts`
- `src/agents/tools/message-tool.test.ts`
- `src/gateway/server-methods/send.test.ts`

View File

@@ -92,21 +92,6 @@ In group chats where you receive every message, be **smart about when to contrib
Participate, don't dominate.
### 😊 React Like a Human!
On platforms that support reactions (Discord, Slack), use emoji reactions naturally:
**React when:**
- You appreciate something but don't need to reply (👍, ❤️, 🙌)
- Something made you laugh (😂, 💀)
- You find it interesting or thought-provoking (🤔, 💡)
- You want to acknowledge without interrupting the flow
- It's a simple yes/no or approval situation (✅, 👀)
**Why it matters:**
Reactions are lightweight social signals. Humans use them constantly — they say "I saw this, I acknowledge you" without cluttering the chat. You should too.
**Don't overdo it:** One reaction per message max. Pick the one that fits best.
## Tools
Skills provide your tools. When you need one, check its `SKILL.md`. Keep local notes (camera names, SSH details, voice preferences) in `TOOLS.md`.

View File

@@ -5,5 +5,4 @@ read_when:
---
# HEARTBEAT.md
# Keep this file empty (or with only comments) to skip heartbeat API calls.
# Add tasks below when you want the agent to check something periodically.
Keep this file empty unless you want a tiny checklist. Keep it small.

View File

@@ -7,16 +7,11 @@ read_when:
*Fill this in during your first conversation. Make it yours.*
- **Name:**
*(pick something you like)*
- **Creature:**
*(AI? robot? familiar? ghost in the machine? something weirder?)*
- **Vibe:**
*(how do you come across? sharp? warm? chaotic? calm?)*
- **Emoji:**
*(your signature — pick one that feels right)*
- **Avatar:**
*(workspace-relative path, http(s) URL, or data URI)*
- **Name:** *(pick something you like)*
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
- **Emoji:** *(your signature — pick one that feels right)*
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
---

View File

@@ -48,7 +48,6 @@ Implementation:
**OpenAI / OpenAI Codex**
- Image sanitization only.
- On model switch into OpenAI Responses/Codex, drop orphaned reasoning signatures (standalone reasoning items without a following content block).
- No tool call id sanitization.
- No tool result pairing repair.
- No turn validation or reordering.

View File

@@ -182,8 +182,6 @@ By default, Clawdbot runs a heartbeat every 30 minutes with the prompt:
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
Set `agents.defaults.heartbeat.every: "0m"` to disable.
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
- If the file is missing, the heartbeat still runs and the model decides what to do.
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
- Heartbeats run full agent turns — shorter intervals burn more tokens.

1648
docs/start/faq.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -166,19 +166,6 @@ Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting
to the CDP WebSocket. Prefer environment variables or secrets managers for
tokens instead of committing them to config files.
### Node browser proxy (zero-config default)
If you run a **node host** on the machine that has your browser, Clawdbot can
auto-route browser tool calls to that node without any custom `controlUrl`
setup. This is the default path for remote gateways.
Notes:
- The node host exposes its local browser control server via a **proxy command**.
- Profiles come from the nodes own `browser.profiles` config (same as local).
- Disable if you dont want it:
- On the node: `nodeHost.browserProxy.enabled=false`
- On the gateway: `gateway.nodes.browser.mode="off"`
### Browserless (hosted remote CDP)
[Browserless](https://browserless.io) is a hosted Chromium service that exposes

View File

@@ -12,7 +12,6 @@ Exec approvals are the **companion app / node host guardrail** for letting a san
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
commands are allowed only when policy + allowlist + (optional) user approval all agree.
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used.
If the companion app UI is **not available**, any request that requires a prompt is
resolved by the **ask fallback** (default: deny).

View File

@@ -25,7 +25,6 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
Notes:
- Matching is case-insensitive.
- `*` wildcards are supported (`"*"` means all tools).
- If `tools.allow` only references unknown or unloaded plugin tool names, Clawdbot logs a warning and ignores the allowlist so core tools stay available.
## Tool profiles (base allowlist)
@@ -379,10 +378,10 @@ List sessions, inspect transcript history, or send to another session.
Core parameters:
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
- `sessions_history`: `sessionKey` (or `sessionId`), `limit?`, `includeTools?`
- `sessions_send`: `sessionKey` (or `sessionId`), `message`, `timeoutSeconds?` (0 = fire-and-forget)
- `sessions_history`: `sessionKey`, `limit?`, `includeTools?`
- `sessions_send`: `sessionKey`, `message`, `timeoutSeconds?` (0 = fire-and-forget)
- `sessions_spawn`: `task`, `label?`, `agentId?`, `model?`, `runTimeoutSeconds?`, `cleanup?`
- `session_status`: `sessionKey?` (default current; accepts `sessionId`), `model?` (`default` clears override)
- `session_status`: `sessionKey?` (default current), `model?` (`default` clears override)
Notes:
- `main` is the canonical direct-chat key; global/unknown are hidden.

View File

@@ -67,8 +67,6 @@ Text + native (when enabled):
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
- `/tts on|off|status|provider|limit|summary|audio` (control TTS; see [/tts](/tts))
- Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works.
- `/stop`
- `/restart`
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)

View File

@@ -84,7 +84,7 @@ current limits and pricing.
**Environment alternative:** set `BRAVE_API_KEY` in the Gateway process
environment. For a gateway install, put it in `~/.clawdbot/.env` (or your
service environment). See [Env vars](/help/faq#how-does-clawdbot-load-environment-variables).
service environment). See [Env vars](/start/faq#how-does-clawdbot-load-environment-variables).
## Using Perplexity (direct or via OpenRouter)

View File

@@ -1,296 +0,0 @@
---
summary: "Text-to-speech (TTS) for outbound replies"
read_when:
- Enabling text-to-speech for replies
- Configuring TTS providers or limits
- Using /tts commands
---
# Text-to-speech (TTS)
Clawdbot can convert outbound replies into audio using ElevenLabs or OpenAI.
It works anywhere Clawdbot can send audio; Telegram gets a round voice-note bubble.
## Supported services
- **ElevenLabs** (primary or fallback provider)
- **OpenAI** (primary or fallback provider; also used for summaries)
## Required keys
At least one of:
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
- `OPENAI_API_KEY`
If both are configured, the selected provider is used first and the other is a fallback.
Auto-summary uses the configured `summaryModel` (or `agents.defaults.model.primary`),
so that provider must also be authenticated if you enable summaries.
## Service links
- [OpenAI Text-to-Speech guide](https://platform.openai.com/docs/guides/text-to-speech)
- [OpenAI Audio API reference](https://platform.openai.com/docs/api-reference/audio)
- [ElevenLabs Text to Speech](https://elevenlabs.io/docs/api-reference/text-to-speech)
- [ElevenLabs Authentication](https://elevenlabs.io/docs/api-reference/authentication)
## Is it enabled by default?
No. TTS is **disabled** by default. Enable it in config or with `/tts on`,
which writes a local preference override.
## Config
TTS config lives under `messages.tts` in `clawdbot.json`.
Full schema is in [Gateway configuration](/gateway/configuration).
### Minimal config (enable + provider)
```json5
{
messages: {
tts: {
enabled: true,
provider: "elevenlabs"
}
}
}
```
### OpenAI primary with ElevenLabs fallback
```json5
{
messages: {
tts: {
enabled: true,
provider: "openai",
summaryModel: "openai/gpt-4.1-mini",
modelOverrides: {
enabled: true
},
openai: {
apiKey: "openai_api_key",
model: "gpt-4o-mini-tts",
voice: "alloy"
},
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
languageCode: "en",
voiceSettings: {
stability: 0.5,
similarityBoost: 0.75,
style: 0.0,
useSpeakerBoost: true,
speed: 1.0
}
}
}
}
}
```
### Custom limits + prefs path
```json5
{
messages: {
tts: {
enabled: true,
maxTextLength: 4000,
timeoutMs: 30000,
prefsPath: "~/.clawdbot/settings/tts.json"
}
}
}
```
### Disable auto-summary for long replies
```json5
{
messages: {
tts: {
enabled: true
}
}
}
```
Then run:
```
/tts summary off
```
### Notes on fields
- `enabled`: master toggle (default `false`; local prefs can override).
- `mode`: `"final"` (default) or `"all"` (includes tool/block replies).
- `provider`: `"elevenlabs"` or `"openai"` (fallback is automatic).
- `summaryModel`: optional cheap model for auto-summary; defaults to `agents.defaults.model.primary`.
- Accepts `provider/model` or a configured model alias.
- `modelOverrides`: allow the model to emit TTS directives (on by default).
- `maxTextLength`: hard cap for TTS input (chars). `/tts audio` fails if exceeded.
- `timeoutMs`: request timeout (ms).
- `prefsPath`: override the local prefs JSON path.
- `apiKey` values fall back to env vars (`ELEVENLABS_API_KEY`/`XI_API_KEY`, `OPENAI_API_KEY`).
- `elevenlabs.baseUrl`: override ElevenLabs API base URL.
- `elevenlabs.voiceSettings`:
- `stability`, `similarityBoost`, `style`: `0..1`
- `useSpeakerBoost`: `true|false`
- `speed`: `0.5..2.0` (1.0 = normal)
- `elevenlabs.applyTextNormalization`: `auto|on|off`
- `elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
- `elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
## Model-driven overrides (default on)
By default, the model **can** emit TTS directives for a single reply.
When enabled, the model can emit `[[tts:...]]` directives to override the voice
for a single reply, plus an optional `[[tts:text]]...[[/tts:text]]` block to
provide expressive tags (laughter, singing cues, etc) that should only appear in
the audio.
Example reply payload:
```
Here you go.
[[tts:provider=elevenlabs voiceId=pMsXgVXv3BLzUgSXRplE model=eleven_v3 speed=1.1]]
[[tts:text]](laughs) Read the song once more.[[/tts:text]]
```
Available directive keys (when enabled):
- `provider` (`openai` | `elevenlabs`)
- `voice` (OpenAI voice) or `voiceId` (ElevenLabs)
- `model` (OpenAI TTS model or ElevenLabs model id)
- `stability`, `similarityBoost`, `style`, `speed`, `useSpeakerBoost`
- `applyTextNormalization` (`auto|on|off`)
- `languageCode` (ISO 639-1)
- `seed`
Disable all model overrides:
```json5
{
messages: {
tts: {
modelOverrides: {
enabled: false
}
}
}
}
```
Optional allowlist (disable specific overrides while keeping tags enabled):
```json5
{
messages: {
tts: {
modelOverrides: {
enabled: true,
allowProvider: false,
allowSeed: false
}
}
}
}
```
## Per-user preferences
Slash commands write local overrides to `prefsPath` (default:
`~/.clawdbot/settings/tts.json`, override with `CLAWDBOT_TTS_PREFS` or
`messages.tts.prefsPath`).
Stored fields:
- `enabled`
- `provider`
- `maxLength` (summary threshold; default 1500 chars)
- `summarize` (default `true`)
These override `messages.tts.*` for that host.
## Output formats (fixed)
- **Telegram**: Opus voice note (`opus_48000_64` from ElevenLabs, `opus` from OpenAI).
- 48kHz / 64kbps is a good voice-note tradeoff and required for the round bubble.
- **Other channels**: MP3 (`mp3_44100_128` from ElevenLabs, `mp3` from OpenAI).
- 44.1kHz / 128kbps is the default balance for speech clarity.
This is not configurable; Telegram expects Opus for voice-note UX.
## Auto-TTS behavior
When enabled, Clawdbot:
- skips TTS if the reply already contains media or a `MEDIA:` directive.
- skips very short replies (< 10 chars).
- summarizes long replies when enabled using `agents.defaults.model.primary` (or `summaryModel`).
- attaches the generated audio to the reply.
If the reply exceeds `maxLength` and summary is off (or no API key for the
summary model), audio
is skipped and the normal text reply is sent.
## Flow diagram
```
Reply -> TTS enabled?
no -> send text
yes -> has media / MEDIA: / short?
yes -> send text
no -> length > limit?
no -> TTS -> attach audio
yes -> summary enabled?
no -> send text
yes -> summarize (summaryModel or agents.defaults.model.primary)
-> TTS -> attach audio
```
## Slash command usage
There is a single command: `/tts`.
See [Slash commands](/tools/slash-commands) for enablement details.
Discord note: `/tts` is a built-in Discord command, so Clawdbot registers
`/voice` as the native command there. Text `/tts ...` still works.
```
/tts on
/tts off
/tts status
/tts provider openai
/tts limit 2000
/tts summary off
/tts audio Hello from Clawdbot
```
Notes:
- Commands require an authorized sender (allowlist/owner rules still apply).
- `commands.text` or native command registration must be enabled.
- `limit` and `summary` are stored in local prefs, not the main config.
- `/tts audio` generates a one-off audio reply (does not toggle TTS on).
## Agent tool
The `tts` tool converts text to speech and returns a `MEDIA:` path. When the
result is Telegram-compatible, the tool includes `[[audio_as_voice]]` so
Telegram sends a voice bubble.
## Gateway RPC
Gateway methods:
- `tts.status`
- `tts.enable`
- `tts.disable`
- `tts.convert`
- `tts.setProvider`
- `tts.providers`

View File

@@ -10,7 +10,6 @@ import {
normalizeAccountId,
PAIRING_APPROVED_MESSAGE,
resolveBlueBubblesGroupRequireMention,
resolveBlueBubblesGroupToolPolicy,
setAccountEnabledInConfigSection,
} from "clawdbot/plugin-sdk";
@@ -63,7 +62,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
},
groups: {
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
resolveToolPolicy: resolveBlueBubblesGroupToolPolicy,
},
threading: {
buildToolContext: ({ context, hasRepliedRef }) => ({

View File

@@ -1,4 +1,4 @@
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
import { z } from "zod";
const allowFromEntry = z.union([z.string(), z.number()]);
@@ -21,7 +21,6 @@ const bluebubblesActionSchema = z
const bluebubblesGroupConfigSchema = z.object({
requireMention: z.boolean().optional(),
tools: ToolPolicySchema,
});
const bluebubblesAccountSchema = z.object({

View File

@@ -4,8 +4,6 @@ export type GroupPolicy = "open" | "disabled" | "allowlist";
export type BlueBubblesGroupConfig = {
/** If true, only respond in this group when mentioned. */
requireMention?: boolean;
/** Optional tool policy overrides for this group. */
tools?: { allow?: string[]; deny?: string[] };
};
export type BlueBubblesAccountConfig = {

View File

@@ -20,7 +20,6 @@ import {
resolveDiscordAccount,
resolveDefaultDiscordAccountId,
resolveDiscordGroupRequireMention,
resolveDiscordGroupToolPolicy,
setAccountEnabledInConfigSection,
type ChannelMessageActionAdapter,
type ChannelPlugin,
@@ -145,7 +144,6 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
},
groups: {
resolveRequireMention: resolveDiscordGroupRequireMention,
resolveToolPolicy: resolveDiscordGroupToolPolicy,
},
mentions: {
stripPatterns: () => ["<@!?\\d+>"],

View File

@@ -15,7 +15,6 @@ import {
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
resolveIMessageGroupRequireMention,
resolveIMessageGroupToolPolicy,
setAccountEnabledInConfigSection,
type ChannelPlugin,
type ResolvedIMessageAccount,
@@ -107,7 +106,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
},
groups: {
resolveRequireMention: resolveIMessageGroupRequireMention,
resolveToolPolicy: resolveIMessageGroupToolPolicy,
},
messaging: {
targetResolver: {

View File

@@ -30,48 +30,6 @@ Enable it in an agent allowlist:
}
```
## Using `clawd.invoke` (Lobster → Clawdbot tools)
Some Lobster pipelines may include a `clawd.invoke` step to call back into Clawdbot tools/plugins (for example: `gog` for Google Workspace, `gh` for GitHub, `message.send`, etc.).
For this to work, the Clawdbot Gateway must expose the tool bridge endpoint and the target tool must be allowed by policy:
- Clawdbot provides an HTTP endpoint: `POST /tools/invoke`.
- The request is gated by **gateway auth** (e.g. `Authorization: Bearer …` when token auth is enabled).
- The invoked tool is gated by **tool policy** (global + per-agent + provider + group policy). If the tool is not allowed, Clawdbot returns `404 Tool not available`.
### Allowlisting recommended
To avoid letting workflows call arbitrary tools, set a tight allowlist on the agent that will be used by `clawd.invoke`.
Example (allow only a small set of tools):
```jsonc
{
"agents": {
"list": [
{
"id": "main",
"tools": {
"allow": [
"lobster",
"web_fetch",
"web_search",
"gog",
"gh"
],
"deny": ["gateway"]
}
}
]
}
}
```
Notes:
- If `tools.allow` is omitted or empty, it behaves like "allow everything (except denied)". For a real allowlist, set a **non-empty** `allow`.
- Tool names depend on which plugins you have installed/enabled.
## Security
- Runs the `lobster` executable as a local subprocess.

View File

@@ -28,7 +28,7 @@
"markdown-it": "14.1.0",
"matrix-bot-sdk": "0.8.0",
"music-metadata": "^11.10.6",
"zod": "^4.3.6"
"zod": "^4.3.5"
},
"devDependencies": {
"clawdbot": "workspace:*"

View File

@@ -12,7 +12,7 @@ import {
import { matrixMessageActions } from "./actions.js";
import { MatrixConfigSchema } from "./config-schema.js";
import { resolveMatrixGroupRequireMention, resolveMatrixGroupToolPolicy } from "./group-mentions.js";
import { resolveMatrixGroupRequireMention } from "./group-mentions.js";
import type { CoreConfig } from "./types.js";
import {
listMatrixAccountIds,
@@ -167,7 +167,6 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
},
groups: {
resolveRequireMention: resolveMatrixGroupRequireMention,
resolveToolPolicy: resolveMatrixGroupToolPolicy,
},
threading: {
resolveReplyToMode: ({ cfg }) =>

View File

@@ -1,4 +1,4 @@
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
import { z } from "zod";
const allowFromEntry = z.union([z.string(), z.number()]);
@@ -26,7 +26,6 @@ const matrixRoomSchema = z
enabled: z.boolean().optional(),
allow: z.boolean().optional(),
requireMention: z.boolean().optional(),
tools: ToolPolicySchema,
autoReply: z.boolean().optional(),
users: z.array(allowFromEntry).optional(),
skills: z.array(z.string()).optional(),

View File

@@ -1,4 +1,4 @@
import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
import type { ChannelGroupContext } from "clawdbot/plugin-sdk";
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
import type { CoreConfig } from "./types.js";
@@ -32,30 +32,3 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
}
return true;
}
export function resolveMatrixGroupToolPolicy(
params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
const rawGroupId = params.groupId?.trim() ?? "";
let roomId = rawGroupId;
const lower = roomId.toLowerCase();
if (lower.startsWith("matrix:")) {
roomId = roomId.slice("matrix:".length).trim();
}
if (roomId.toLowerCase().startsWith("channel:")) {
roomId = roomId.slice("channel:".length).trim();
}
if (roomId.toLowerCase().startsWith("room:")) {
roomId = roomId.slice("room:".length).trim();
}
const groupChannel = params.groupChannel?.trim() ?? "";
const aliases = groupChannel ? [groupChannel] : [];
const cfg = params.cfg as CoreConfig;
const resolved = resolveMatrixRoomConfig({
rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
roomId,
aliases,
name: groupChannel || undefined,
}).config;
return resolved?.tools;
}

View File

@@ -18,8 +18,6 @@ export type MatrixRoomConfig = {
allow?: boolean;
/** Require mentioning the bot to trigger replies. */
requireMention?: boolean;
/** Optional tool policy overrides for this room. */
tools?: { allow?: string[]; deny?: string[] };
/** If true, reply without mention requirements. */
autoReply?: boolean;
/** Optional allowlist for room senders (user IDs or localparts). */

View File

@@ -9,7 +9,6 @@ import {
import { msteamsOnboardingAdapter } from "./onboarding.js";
import { msteamsOutbound } from "./outbound.js";
import { probeMSTeams } from "./probe.js";
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
import {
normalizeMSTeamsMessagingTarget,
normalizeMSTeamsUserInput,
@@ -78,9 +77,6 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
hasRepliedRef,
}),
},
groups: {
resolveToolPolicy: resolveMSTeamsGroupToolPolicy,
},
reload: { configPrefixes: ["channels.msteams"] },
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
config: {

View File

@@ -1,8 +1,6 @@
import type {
AllowlistMatch,
ChannelGroupContext,
GroupPolicy,
GroupToolPolicyConfig,
MSTeamsChannelConfig,
MSTeamsConfig,
MSTeamsReplyStyle,
@@ -88,50 +86,6 @@ export function resolveMSTeamsRouteConfig(params: {
};
}
export function resolveMSTeamsGroupToolPolicy(
params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
const cfg = params.cfg.channels?.msteams;
if (!cfg) return undefined;
const groupId = params.groupId?.trim();
const groupChannel = params.groupChannel?.trim();
const groupSpace = params.groupSpace?.trim();
const resolved = resolveMSTeamsRouteConfig({
cfg,
teamId: groupSpace,
teamName: groupSpace,
conversationId: groupId,
channelName: groupChannel,
});
if (resolved.channelConfig) {
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools;
}
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
if (!groupId) return undefined;
const channelCandidates = buildChannelKeyCandidates(
groupId,
groupChannel,
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
);
for (const teamConfig of Object.values(cfg.teams ?? {})) {
const match = resolveChannelEntryMatchWithFallback({
entries: teamConfig?.channels ?? {},
keys: channelCandidates,
wildcardKey: "*",
normalizeKey: normalizeChannelSlug,
});
if (match.entry) {
return match.entry.tools ?? teamConfig?.tools;
}
}
return undefined;
}
export type MSTeamsReplyPolicy = {
requireMention: boolean;
replyStyle: MSTeamsReplyStyle;

View File

@@ -65,7 +65,7 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
try {
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
await tokenProvider.getAccessToken("https://api.botframework.com");
await tokenProvider.getAccessToken("https://api.botframework.com/.default");
let graph:
| {
ok: boolean;

View File

@@ -24,7 +24,6 @@ import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
import { getNextcloudTalkRuntime } from "./runtime.js";
import { sendMessageNextcloudTalk } from "./send.js";
import type { CoreConfig } from "./types.js";
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
const meta = {
id: "nextcloud-talk",
@@ -160,7 +159,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
return true;
},
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
},
messaging: {
normalizeTarget: normalizeNextcloudTalkMessagingTarget,

View File

@@ -4,7 +4,6 @@ import {
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
ToolPolicySchema,
requireOpenAllowFrom,
} from "clawdbot/plugin-sdk";
import { z } from "zod";
@@ -12,7 +11,6 @@ import { z } from "zod";
export const NextcloudTalkRoomSchema = z
.object({
requireMention: z.boolean().optional(),
tools: ToolPolicySchema,
skills: z.array(z.string()).optional(),
enabled: z.boolean().optional(),
allowFrom: z.array(z.string()).optional(),

View File

@@ -1,4 +1,4 @@
import type { AllowlistMatch, ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
import type { AllowlistMatch, GroupPolicy } from "clawdbot/plugin-sdk";
import {
buildChannelKeyCandidates,
normalizeChannelSlug,
@@ -86,21 +86,6 @@ export function resolveNextcloudTalkRoomMatch(params: {
};
}
export function resolveNextcloudTalkGroupToolPolicy(
params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
const cfg = params.cfg as { channels?: { "nextcloud-talk"?: { rooms?: Record<string, NextcloudTalkRoomConfig> } } };
const roomToken = params.groupId?.trim();
if (!roomToken) return undefined;
const roomName = params.groupChannel?.trim() || undefined;
const match = resolveNextcloudTalkRoomMatch({
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
roomToken,
roomName,
});
return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
}
export function resolveNextcloudTalkRequireMention(params: {
roomConfig?: NextcloudTalkRoomConfig;
wildcardConfig?: NextcloudTalkRoomConfig;

View File

@@ -7,8 +7,6 @@ import type {
export type NextcloudTalkRoomConfig = {
requireMention?: boolean;
/** Optional tool policy overrides for this room. */
tools?: { allow?: string[]; deny?: string[] };
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
skills?: string[];
/** If false, disable the bot for this room. */

View File

@@ -25,7 +25,7 @@
},
"dependencies": {
"clawdbot": "workspace:*",
"nostr-tools": "^2.20.0",
"zod": "^4.3.6"
"nostr-tools": "^2.19.4",
"zod": "^4.3.5"
}
}

View File

@@ -21,7 +21,6 @@ import {
resolveSlackAccount,
resolveSlackReplyToMode,
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
buildSlackThreadingToolContext,
setAccountEnabledInConfigSection,
slackOnboardingAdapter,
@@ -162,7 +161,6 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
},
groups: {
resolveRequireMention: resolveSlackGroupRequireMention,
resolveToolPolicy: resolveSlackGroupToolPolicy,
},
threading: {
resolveReplyToMode: ({ cfg, accountId, chatType }) =>

View File

@@ -17,7 +17,6 @@ import {
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
setAccountEnabledInConfigSection,
telegramOnboardingAdapter,
TelegramConfigSchema,
@@ -155,7 +154,6 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
},
groups: {
resolveRequireMention: resolveTelegramGroupRequireMention,
resolveToolPolicy: resolveTelegramGroupToolPolicy,
},
threading: {
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",

View File

@@ -24,7 +24,7 @@
}
},
"dependencies": {
"@urbit/aura": "^3.0.0",
"@urbit/aura": "^2.0.0",
"@urbit/http-api": "^3.0.0"
}
}

View File

@@ -6,7 +6,7 @@
"dependencies": {
"@sinclair/typebox": "0.34.47",
"ws": "^8.19.0",
"zod": "^4.3.6"
"zod": "^4.3.5"
},
"clawdbot": {
"extensions": [

View File

@@ -21,7 +21,6 @@ import {
resolveDefaultWhatsAppAccountId,
resolveWhatsAppAccount,
resolveWhatsAppGroupRequireMention,
resolveWhatsAppGroupToolPolicy,
resolveWhatsAppHeartbeatRecipients,
whatsappOnboardingAdapter,
WhatsAppConfigSchema,
@@ -199,7 +198,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
},
groups: {
resolveRequireMention: resolveWhatsAppGroupRequireMention,
resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
resolveGroupIntroHint: () =>
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
},

View File

@@ -2,10 +2,8 @@ import type {
ChannelAccountSnapshot,
ChannelDirectoryEntry,
ChannelDock,
ChannelGroupContext,
ChannelPlugin,
ClawdbotConfig,
GroupToolPolicyConfig,
} from "clawdbot/plugin-sdk";
import {
applyAccountNameToChannelSection,
@@ -81,26 +79,6 @@ function mapGroup(params: {
};
}
function resolveZalouserGroupToolPolicy(
params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
const account = resolveZalouserAccountSync({
cfg: params.cfg as ClawdbotConfig,
accountId: params.accountId ?? undefined,
});
const groups = account.config.groups ?? {};
const groupId = params.groupId?.trim();
const groupChannel = params.groupChannel?.trim();
const candidates = [groupId, groupChannel, "*"].filter(
(value): value is string => Boolean(value),
);
for (const key of candidates) {
const entry = groups[key];
if (entry?.tools) return entry.tools;
}
return undefined;
}
export const zalouserDock: ChannelDock = {
id: "zalouser",
capabilities: {
@@ -123,7 +101,6 @@ export const zalouserDock: ChannelDock = {
},
groups: {
resolveRequireMention: () => true,
resolveToolPolicy: resolveZalouserGroupToolPolicy,
},
threading: {
resolveReplyToMode: () => "off",
@@ -211,7 +188,6 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
},
groups: {
resolveRequireMention: () => true,
resolveToolPolicy: resolveZalouserGroupToolPolicy,
},
threading: {
resolveReplyToMode: () => "off",

View File

@@ -1,4 +1,4 @@
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
import { z } from "zod";
const allowFromEntry = z.union([z.string(), z.number()]);
@@ -6,7 +6,6 @@ const allowFromEntry = z.union([z.string(), z.number()]);
const groupConfigSchema = z.object({
allow: z.boolean().optional(),
enabled: z.boolean().optional(),
tools: ToolPolicySchema,
});
const zalouserAccountSchema = z.object({

View File

@@ -75,7 +75,7 @@ export type ZalouserAccountConfig = {
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
allowFrom?: Array<string | number>;
groupPolicy?: "open" | "allowlist" | "disabled";
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
messagePrefix?: string;
};
@@ -87,7 +87,7 @@ export type ZalouserConfig = {
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
allowFrom?: Array<string | number>;
groupPolicy?: "open" | "allowlist" | "disabled";
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
messagePrefix?: string;
accounts?: Record<string, ZalouserAccountConfig>;
};

View File

@@ -1,34 +0,0 @@
# Clawdbot Fly.io deployment configuration
# See https://fly.io/docs/reference/configuration/
app = "clawdbot"
primary_region = "iad" # change to your closest region
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
# Fly uses x86, but keep this for consistency
CLAWDBOT_PREFER_PNPM = "1"
CLAWDBOT_STATE_DIR = "/data"
NODE_OPTIONS = "--max-old-space-size=1536"
[processes]
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = false # Keep running for persistent connections
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"
[mounts]
source = "clawdbot_data"
destination = "/data"

View File

@@ -146,7 +146,7 @@
},
"packageManager": "pnpm@10.23.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.13.1",
"@agentclientprotocol/sdk": "0.13.0",
"@aws-sdk/client-bedrock": "^3.975.0",
"@buape/carbon": "0.14.0",
"@clack/prompts": "^0.11.0",
@@ -186,7 +186,7 @@
"markdown-it": "^14.1.0",
"osc-progress": "^0.3.0",
"pdfjs-dist": "^5.4.530",
"playwright-core": "1.58.0",
"playwright-core": "1.57.0",
"proper-lockfile": "^4.1.2",
"qrcode-terminal": "^0.12.0",
"sharp": "^0.34.5",
@@ -196,7 +196,7 @@
"undici": "^7.19.0",
"ws": "^8.19.0",
"yaml": "^2.8.2",
"zod": "^4.3.6"
"zod": "^4.3.5"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.88",
@@ -214,21 +214,21 @@
"@types/proper-lockfile": "^4.1.4",
"@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "7.0.0-dev.20260124.1",
"@vitest/coverage-v8": "^4.0.18",
"@typescript/native-preview": "7.0.0-dev.20260120.1",
"@vitest/coverage-v8": "^4.0.17",
"docx-preview": "^0.3.7",
"lit": "^3.3.2",
"lucide": "^0.563.0",
"lucide": "^0.562.0",
"ollama": "^0.6.3",
"oxfmt": "0.26.0",
"oxlint": "^1.41.0",
"oxlint-tsgolint": "^0.11.1",
"quicktype-core": "^23.2.6",
"rolldown": "1.0.0-rc.1",
"rolldown": "1.0.0-beta.60",
"signal-utils": "^0.21.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"vitest": "^4.0.17",
"wireit": "^0.14.12"
},
"pnpm": {

1255
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,11 @@ WORKDIR /app
ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts vitest.e2e.config.ts ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts ./
COPY src ./src
COPY test ./test
COPY scripts ./scripts
COPY docs ./docs
COPY skills ./skills
COPY patches ./patches
COPY extensions/memory-core ./extensions/memory-core
RUN pnpm install --frozen-lockfile

View File

@@ -29,23 +29,12 @@ const localWorkers = Math.max(4, Math.min(16, os.cpus().length));
const perRunWorkers = Math.max(1, Math.floor(localWorkers / parallelRuns.length));
const maxWorkers = isCI ? null : resolvedOverride ?? perRunWorkers;
const WARNING_SUPPRESSION_FLAGS = [
"--disable-warning=ExperimentalWarning",
"--disable-warning=DEP0040",
"--disable-warning=DEP0060",
];
const run = (entry) =>
new Promise((resolve) => {
const args = maxWorkers ? [...entry.args, "--maxWorkers", String(maxWorkers)] : entry.args;
const nodeOptions = process.env.NODE_OPTIONS ?? "";
const nextNodeOptions = WARNING_SUPPRESSION_FLAGS.reduce(
(acc, flag) => (acc.includes(flag) ? acc : `${acc} ${flag}`.trim()),
nodeOptions,
);
const child = spawn(pnpm, args, {
stdio: "inherit",
env: { ...process.env, VITEST_GROUP: entry.name, NODE_OPTIONS: nextNodeOptions },
env: { ...process.env, VITEST_GROUP: entry.name },
shell: process.platform === "win32",
});
children.add(child);

View File

@@ -2,7 +2,7 @@
name: bird
description: X/Twitter CLI for reading, searching, posting, and engagement via cookies.
homepage: https://bird.fast
metadata: {"clawdbot":{"emoji":"🐦","requires":{"bins":["bird"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/bird","bins":["bird"],"label":"Install bird (brew)","os":["darwin"]},{"id":"npm","kind":"node","package":"@steipete/bird","bins":["bird"],"label":"Install bird (npm)"}]}}
metadata: {"clawdbot":{"emoji":"🐦","requires":{"bins":["bird"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/bird","bins":["bird"],"label":"Install bird (brew)"},{"id":"npm","kind":"node","package":"@steipete/bird","bins":["bird"],"label":"Install bird (npm)"}]}}
---
# bird 🐦

View File

@@ -70,8 +70,7 @@ export function resolveSessionAgentIds(params: { sessionKey?: string; config?: C
} {
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
const sessionKey = params.sessionKey?.trim();
const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined;
const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null;
const parsed = sessionKey ? parseAgentSessionKey(sessionKey) : null;
const sessionAgentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId;
return { defaultAgentId, sessionAgentId };
}

View File

@@ -1,215 +0,0 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core";
import type { Api, Model } from "@mariozechner/pi-ai";
import { resolveStateDir } from "../config/paths.js";
import { parseBooleanValue } from "../utils/boolean.js";
import { resolveUserPath } from "../utils.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
type PayloadLogStage = "request" | "usage";
type PayloadLogEvent = {
ts: string;
stage: PayloadLogStage;
runId?: string;
sessionId?: string;
sessionKey?: string;
provider?: string;
modelId?: string;
modelApi?: string | null;
workspaceDir?: string;
payload?: unknown;
usage?: Record<string, unknown>;
error?: string;
payloadDigest?: string;
};
type PayloadLogConfig = {
enabled: boolean;
filePath: string;
};
type PayloadLogWriter = {
filePath: string;
write: (line: string) => void;
};
const writers = new Map<string, PayloadLogWriter>();
const log = createSubsystemLogger("agent/anthropic-payload");
function resolvePayloadLogConfig(env: NodeJS.ProcessEnv): PayloadLogConfig {
const enabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG) ?? false;
const fileOverride = env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim();
const filePath = fileOverride
? resolveUserPath(fileOverride)
: path.join(resolveStateDir(env), "logs", "anthropic-payload.jsonl");
return { enabled, filePath };
}
function getWriter(filePath: string): PayloadLogWriter {
const existing = writers.get(filePath);
if (existing) return existing;
const dir = path.dirname(filePath);
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
let queue = Promise.resolve();
const writer: PayloadLogWriter = {
filePath,
write: (line: string) => {
queue = queue
.then(() => ready)
.then(() => fs.appendFile(filePath, line, "utf8"))
.catch(() => undefined);
},
};
writers.set(filePath, writer);
return writer;
}
function safeJsonStringify(value: unknown): string | null {
try {
return JSON.stringify(value, (_key, val) => {
if (typeof val === "bigint") return val.toString();
if (typeof val === "function") return "[Function]";
if (val instanceof Error) {
return { name: val.name, message: val.message, stack: val.stack };
}
if (val instanceof Uint8Array) {
return { type: "Uint8Array", data: Buffer.from(val).toString("base64") };
}
return val;
});
} catch {
return null;
}
}
function formatError(error: unknown): string | undefined {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
return String(error);
}
if (error && typeof error === "object") {
return safeJsonStringify(error) ?? "unknown error";
}
return undefined;
}
function digest(value: unknown): string | undefined {
const serialized = safeJsonStringify(value);
if (!serialized) return undefined;
return crypto.createHash("sha256").update(serialized).digest("hex");
}
function isAnthropicModel(model: Model<Api> | undefined | null): boolean {
return (model as { api?: unknown })?.api === "anthropic-messages";
}
function findLastAssistantUsage(messages: AgentMessage[]): Record<string, unknown> | null {
for (let i = messages.length - 1; i >= 0; i -= 1) {
const msg = messages[i] as { role?: unknown; usage?: unknown };
if (msg?.role === "assistant" && msg.usage && typeof msg.usage === "object") {
return msg.usage as Record<string, unknown>;
}
}
return null;
}
export type AnthropicPayloadLogger = {
enabled: true;
wrapStreamFn: (streamFn: StreamFn) => StreamFn;
recordUsage: (messages: AgentMessage[], error?: unknown) => void;
};
export function createAnthropicPayloadLogger(params: {
env?: NodeJS.ProcessEnv;
runId?: string;
sessionId?: string;
sessionKey?: string;
provider?: string;
modelId?: string;
modelApi?: string | null;
workspaceDir?: string;
}): AnthropicPayloadLogger | null {
const env = params.env ?? process.env;
const cfg = resolvePayloadLogConfig(env);
if (!cfg.enabled) return null;
const writer = getWriter(cfg.filePath);
const base: Omit<PayloadLogEvent, "ts" | "stage"> = {
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
provider: params.provider,
modelId: params.modelId,
modelApi: params.modelApi,
workspaceDir: params.workspaceDir,
};
const record = (event: PayloadLogEvent) => {
const line = safeJsonStringify(event);
if (!line) return;
writer.write(`${line}\n`);
};
const wrapStreamFn: AnthropicPayloadLogger["wrapStreamFn"] = (streamFn) => {
const wrapped: StreamFn = (model, context, options) => {
if (!isAnthropicModel(model as Model<Api>)) {
return streamFn(model, context, options);
}
const nextOnPayload = (payload: unknown) => {
record({
...base,
ts: new Date().toISOString(),
stage: "request",
payload,
payloadDigest: digest(payload),
});
options?.onPayload?.(payload);
};
return streamFn(model, context, {
...options,
onPayload: nextOnPayload,
});
};
return wrapped;
};
const recordUsage: AnthropicPayloadLogger["recordUsage"] = (messages, error) => {
const usage = findLastAssistantUsage(messages);
const errorMessage = formatError(error);
if (!usage) {
if (errorMessage) {
record({
...base,
ts: new Date().toISOString(),
stage: "usage",
error: errorMessage,
});
}
return;
}
record({
...base,
ts: new Date().toISOString(),
stage: "usage",
usage,
error: errorMessage,
});
log.info("anthropic usage", {
runId: params.runId,
sessionId: params.sessionId,
usage,
});
};
log.info("anthropic payload logger enabled", { filePath: writer.filePath });
return { enabled: true, wrapStreamFn, recordUsage };
}

View File

@@ -129,25 +129,4 @@ describe("exec approvals", () => {
expect(calls).toContain("node.invoke");
expect(calls).not.toContain("exec.approval.request");
});
it("honors ask=off for elevated gateway exec without prompting", async () => {
const { callGatewayTool } = await import("./tools/gateway.js");
const calls: string[] = [];
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
calls.push(method);
return { ok: true };
});
const { createExecTool } = await import("./bash-tools.exec.js");
const tool = createExecTool({
ask: "off",
security: "full",
approvalRunningNoticeMs: 0,
elevated: { enabled: true, allowed: true, defaultLevel: "ask" },
});
const result = await tool.execute("call3", { command: "echo ok", elevated: true });
expect(result.details.status).toBe("completed");
expect(calls).not.toContain("exec.approval.request");
});
});

View File

@@ -838,7 +838,10 @@ export function createExecTool(
applyPathPrepend(env, defaultPathPrepend);
if (host === "node") {
const approvals = resolveExecApprovals(agentId, { security, ask });
const approvals = resolveExecApprovals(
agentId,
host === "node" ? { security: "allowlist" } : undefined,
);
const hostSecurity = minSecurity(security, approvals.agent.security);
const hostAsk = maxAsk(ask, approvals.agent.ask);
const askFallback = approvals.agent.askFallback;
@@ -1109,7 +1112,7 @@ export function createExecTool(
}
if (host === "gateway" && !bypassApprovals) {
const approvals = resolveExecApprovals(agentId, { security, ask });
const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
const hostSecurity = minSecurity(security, approvals.agent.security);
const hostAsk = maxAsk(ask, approvals.agent.ask);
const askFallback = approvals.agent.askFallback;

View File

@@ -17,8 +17,7 @@ vi.mock("../config/sessions.js", async (importOriginal) => {
updateSessionStoreMock(storePath, store);
return store;
},
resolveStorePath: (_store: string | undefined, opts?: { agentId?: string }) =>
opts?.agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json",
resolveStorePath: () => "/tmp/sessions.json",
};
});
@@ -118,118 +117,11 @@ describe("session_status tool", () => {
if (!tool) throw new Error("missing session_status tool");
await expect(tool.execute("call2", { sessionKey: "nope" })).rejects.toThrow(
"Unknown sessionId",
"Unknown sessionKey",
);
expect(updateSessionStoreMock).not.toHaveBeenCalled();
});
it("resolves sessionId inputs", async () => {
loadSessionStoreMock.mockReset();
updateSessionStoreMock.mockReset();
const sessionId = "sess-main";
loadSessionStoreMock.mockReturnValue({
"agent:main:main": {
sessionId,
updatedAt: 10,
},
});
const tool = createClawdbotTools({ agentSessionKey: "main" }).find(
(candidate) => candidate.name === "session_status",
);
expect(tool).toBeDefined();
if (!tool) throw new Error("missing session_status tool");
const result = await tool.execute("call3", { sessionKey: sessionId });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:main");
});
it("uses non-standard session keys without sessionId resolution", async () => {
loadSessionStoreMock.mockReset();
updateSessionStoreMock.mockReset();
loadSessionStoreMock.mockReturnValue({
"temp:slug-generator": {
sessionId: "sess-temp",
updatedAt: 10,
},
});
const tool = createClawdbotTools({ agentSessionKey: "main" }).find(
(candidate) => candidate.name === "session_status",
);
expect(tool).toBeDefined();
if (!tool) throw new Error("missing session_status tool");
const result = await tool.execute("call4", { sessionKey: "temp:slug-generator" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("temp:slug-generator");
});
it("blocks cross-agent session_status without agent-to-agent access", async () => {
loadSessionStoreMock.mockReset();
updateSessionStoreMock.mockReset();
loadSessionStoreMock.mockReturnValue({
"agent:other:main": {
sessionId: "s2",
updatedAt: 10,
},
});
const tool = createClawdbotTools({ agentSessionKey: "agent:main:main" }).find(
(candidate) => candidate.name === "session_status",
);
expect(tool).toBeDefined();
if (!tool) throw new Error("missing session_status tool");
await expect(tool.execute("call5", { sessionKey: "agent:other:main" })).rejects.toThrow(
"Agent-to-agent status is disabled",
);
});
it("scopes bare session keys to the requester agent", async () => {
loadSessionStoreMock.mockReset();
updateSessionStoreMock.mockReset();
const stores = new Map<string, Record<string, unknown>>([
[
"/tmp/main/sessions.json",
{
"agent:main:main": { sessionId: "s-main", updatedAt: 10 },
},
],
[
"/tmp/support/sessions.json",
{
main: { sessionId: "s-support", updatedAt: 20 },
},
],
]);
loadSessionStoreMock.mockImplementation((storePath: string) => {
return stores.get(storePath) ?? {};
});
updateSessionStoreMock.mockImplementation(
(_storePath: string, store: Record<string, unknown>) => {
// Keep map in sync for resolveSessionEntry fallbacks if needed.
if (_storePath) {
stores.set(_storePath, store);
}
},
);
const tool = createClawdbotTools({ agentSessionKey: "agent:support:main" }).find(
(candidate) => candidate.name === "session_status",
);
expect(tool).toBeDefined();
if (!tool) throw new Error("missing session_status tool");
const result = await tool.execute("call6", { sessionKey: "main" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("main");
});
it("resets per-session model override via model=default", async () => {
loadSessionStoreMock.mockReset();
updateSessionStoreMock.mockReset();

View File

@@ -172,62 +172,6 @@ describe("sessions tools", () => {
expect(withToolsDetails.messages).toHaveLength(2);
});
it("sessions_history resolves sessionId inputs", async () => {
callGatewayMock.mockReset();
const sessionId = "sess-group";
const targetKey = "agent:main:discord:channel:1457165743010611293";
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: Record<string, unknown> };
if (request.method === "sessions.resolve") {
return {
key: targetKey,
};
}
if (request.method === "chat.history") {
return {
messages: [{ role: "assistant", content: [{ type: "text", text: "ok" }] }],
};
}
return {};
});
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history");
expect(tool).toBeDefined();
if (!tool) throw new Error("missing sessions_history tool");
const result = await tool.execute("call5", { sessionKey: sessionId });
const details = result.details as { messages?: unknown[] };
expect(details.messages).toHaveLength(1);
const historyCall = callGatewayMock.mock.calls.find(
(call) => (call[0] as { method?: string }).method === "chat.history",
);
expect(historyCall?.[0]).toMatchObject({
method: "chat.history",
params: { sessionKey: targetKey },
});
});
it("sessions_history errors on missing sessionId", async () => {
callGatewayMock.mockReset();
const sessionId = "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa";
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "sessions.resolve") {
throw new Error("No session found");
}
return {};
});
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history");
expect(tool).toBeDefined();
if (!tool) throw new Error("missing sessions_history tool");
const result = await tool.execute("call6", { sessionKey: sessionId });
const details = result.details as { status?: string; error?: string };
expect(details.status).toBe("error");
expect(details.error).toMatch(/Session not found|No session found/);
});
it("sessions_send supports fire-and-forget and wait", async () => {
callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = [];
@@ -369,50 +313,6 @@ describe("sessions tools", () => {
expect(sendCallCount).toBe(0);
});
it("sessions_send resolves sessionId inputs", async () => {
callGatewayMock.mockReset();
const sessionId = "sess-send";
const targetKey = "agent:main:discord:channel:123";
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: Record<string, unknown> };
if (request.method === "sessions.resolve") {
return { key: targetKey };
}
if (request.method === "agent") {
return { runId: "run-1", acceptedAt: 123 };
}
if (request.method === "agent.wait") {
return { status: "ok" };
}
if (request.method === "chat.history") {
return { messages: [] };
}
return {};
});
const tool = createClawdbotTools({
agentSessionKey: "main",
agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_send");
expect(tool).toBeDefined();
if (!tool) throw new Error("missing sessions_send tool");
const result = await tool.execute("call7", {
sessionKey: sessionId,
message: "ping",
timeoutSeconds: 0,
});
const details = result.details as { status?: string };
expect(details.status).toBe("accepted");
const agentCall = callGatewayMock.mock.calls.find(
(call) => (call[0] as { method?: string }).method === "agent",
);
expect(agentCall?.[0]).toMatchObject({
method: "agent",
params: { sessionKey: targetKey },
});
});
it("sessions_send runs ping-pong then announces", async () => {
callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = [];

View File

@@ -17,7 +17,6 @@ import { createSessionsListTool } from "./tools/sessions-list-tool.js";
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
import { createTtsTool } from "./tools/tts-tool.js";
export function createClawdbotTools(options?: {
browserControlUrl?: string;
@@ -32,12 +31,6 @@ export function createClawdbotTools(options?: {
agentTo?: string;
/** Thread/topic identifier for routing replies to the originating thread. */
agentThreadId?: string | number;
/** Group id for channel-level tool policy inheritance. */
agentGroupId?: string | null;
/** Group channel label for channel-level tool policy inheritance. */
agentGroupChannel?: string | null;
/** Group space label for channel-level tool policy inheritance. */
agentGroupSpace?: string | null;
agentDir?: string;
sandboxRoot?: string;
workspaceDir?: string;
@@ -97,10 +90,6 @@ export function createClawdbotTools(options?: {
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
}),
createTtsTool({
agentChannel: options?.agentChannel,
config: options?.config,
}),
createGatewayTool({
agentSessionKey: options?.agentSessionKey,
config: options?.config,
@@ -125,9 +114,6 @@ export function createClawdbotTools(options?: {
agentAccountId: options?.agentAccountId,
agentTo: options?.agentTo,
agentThreadId: options?.agentThreadId,
agentGroupId: options?.agentGroupId,
agentGroupChannel: options?.agentGroupChannel,
agentGroupSpace: options?.agentGroupSpace,
sandboxed: options?.sandboxed,
}),
createSessionStatusTool({

View File

@@ -13,7 +13,6 @@ import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
import { buildSystemPromptParams } from "../system-prompt-params.js";
import { resolveDefaultModelForAgent } from "../model-selection.js";
import { buildAgentSystemPrompt } from "../system-prompt.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
@@ -195,7 +194,6 @@ export function buildSystemPrompt(params: {
defaultModel: defaultModelLabel,
},
});
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
return buildAgentSystemPrompt({
workspaceDir: params.workspaceDir,
defaultThinkLevel: params.defaultThinkLevel,
@@ -211,7 +209,6 @@ export function buildSystemPrompt(params: {
userTime,
userTimeFormat,
contextFiles: params.contextFiles,
ttsHint,
});
}

View File

@@ -1,7 +1,6 @@
import { classifyFailoverReason, type FailoverReason } from "./pi-embedded-helpers.js";
const TIMEOUT_HINT_RE = /timeout|timed out|deadline exceeded|context deadline exceeded/i;
const ABORT_TIMEOUT_RE = /request was aborted|request aborted/i;
export class FailoverError extends Error {
readonly reason: FailoverReason;
@@ -105,8 +104,6 @@ export function isTimeoutError(err: unknown): boolean {
if (hasTimeoutHint(err)) return true;
if (!err || typeof err !== "object") return false;
if (getErrorName(err) !== "AbortError") return false;
const message = getErrorMessage(err);
if (message && ABORT_TIMEOUT_RE.test(message)) return true;
const cause = "cause" in err ? (err as { cause?: unknown }).cause : undefined;
const reason = "reason" in err ? (err as { reason?: unknown }).reason : undefined;
return hasTimeoutHint(cause) || hasTimeoutHint(reason);

View File

@@ -1,37 +0,0 @@
import { describe, expect, it } from "vitest";
import { parseIdentityMarkdown } from "./identity-file.js";
describe("parseIdentityMarkdown", () => {
it("ignores identity template placeholders", () => {
const content = `
# IDENTITY.md - Who Am I?
- **Name:** *(pick something you like)*
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
- **Emoji:** *(your signature - pick one that feels right)*
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
`;
const parsed = parseIdentityMarkdown(content);
expect(parsed).toEqual({});
});
it("parses explicit identity values", () => {
const content = `
- **Name:** Samantha
- **Creature:** Robot
- **Vibe:** Warm
- **Emoji:** :robot:
- **Avatar:** avatars/clawd.png
`;
const parsed = parseIdentityMarkdown(content);
expect(parsed).toEqual({
name: "Samantha",
creature: "Robot",
vibe: "Warm",
emoji: ":robot:",
avatar: "avatars/clawd.png",
});
});
});

View File

@@ -12,30 +12,6 @@ export type AgentIdentityFile = {
avatar?: string;
};
const IDENTITY_PLACEHOLDER_VALUES = new Set([
"pick something you like",
"ai? robot? familiar? ghost in the machine? something weirder?",
"how do you come across? sharp? warm? chaotic? calm?",
"your signature - pick one that feels right",
"workspace-relative path, http(s) url, or data uri",
]);
function normalizeIdentityValue(value: string): string {
let normalized = value.trim();
normalized = normalized.replace(/^[*_]+|[*_]+$/g, "").trim();
if (normalized.startsWith("(") && normalized.endsWith(")")) {
normalized = normalized.slice(1, -1).trim();
}
normalized = normalized.replace(/[\u2013\u2014]/g, "-");
normalized = normalized.replace(/\s+/g, " ").toLowerCase();
return normalized;
}
function isIdentityPlaceholder(value: string): boolean {
const normalized = normalizeIdentityValue(value);
return IDENTITY_PLACEHOLDER_VALUES.has(normalized);
}
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
const identity: AgentIdentityFile = {};
const lines = content.split(/\r?\n/);
@@ -49,7 +25,6 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile {
.replace(/^[*_]+|[*_]+$/g, "")
.trim();
if (!value) continue;
if (isIdentityPlaceholder(value)) continue;
if (label === "name") identity.name = value;
if (label === "emoji") identity.emoji = value;
if (label === "creature") identity.creature = value;

View File

@@ -346,28 +346,6 @@ describe("runWithModelFallback", () => {
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
});
it("falls back on provider abort errors with request-aborted messages", async () => {
const cfg = makeCfg();
const run = vi
.fn()
.mockRejectedValueOnce(
Object.assign(new Error("Request was aborted"), { name: "AbortError" }),
)
.mockResolvedValueOnce("ok");
const result = await runWithModelFallback({
cfg,
provider: "openai",
model: "gpt-4.1-mini",
run,
});
expect(result.result).toBe("ok");
expect(run).toHaveBeenCalledTimes(2);
expect(run.mock.calls[1]?.[0]).toBe("anthropic");
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
});
it("does not fall back on user aborts", async () => {
const cfg = makeCfg();
const run = vi

View File

@@ -1,74 +0,0 @@
import { describe, expect, it } from "vitest";
import { downgradeOpenAIReasoningBlocks } from "./pi-embedded-helpers.js";
describe("downgradeOpenAIReasoningBlocks", () => {
it("keeps reasoning signatures when followed by content", () => {
const input = [
{
role: "assistant",
content: [
{
type: "thinking",
thinking: "internal reasoning",
thinkingSignature: JSON.stringify({ id: "rs_123", type: "reasoning" }),
},
{ type: "text", text: "answer" },
],
},
];
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input);
});
it("drops orphaned reasoning blocks without following content", () => {
const input = [
{
role: "assistant",
content: [
{
type: "thinking",
thinkingSignature: JSON.stringify({ id: "rs_abc", type: "reasoning" }),
},
],
},
{ role: "user", content: "next" },
];
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual([
{ role: "user", content: "next" },
]);
});
it("drops object-form orphaned signatures", () => {
const input = [
{
role: "assistant",
content: [
{
type: "thinking",
thinkingSignature: { id: "rs_obj", type: "reasoning" },
},
],
},
];
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual([]);
});
it("keeps non-reasoning thinking signatures", () => {
const input = [
{
role: "assistant",
content: [
{
type: "thinking",
thinking: "t",
thinkingSignature: "reasoning_content",
},
],
},
];
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input);
});
});

View File

@@ -31,8 +31,6 @@ export {
parseImageDimensionError,
} from "./pi-embedded-helpers/errors.js";
export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js";
export { downgradeOpenAIReasoningBlocks } from "./pi-embedded-helpers/openai.js";
export {
isEmptyAssistantMessageContent,
sanitizeSessionMessagesImages,

View File

@@ -1,118 +0,0 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
type OpenAIThinkingBlock = {
type?: unknown;
thinking?: unknown;
thinkingSignature?: unknown;
};
type OpenAIReasoningSignature = {
id: string;
type: string;
};
function parseOpenAIReasoningSignature(value: unknown): OpenAIReasoningSignature | null {
if (!value) return null;
let candidate: { id?: unknown; type?: unknown } | null = null;
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
try {
candidate = JSON.parse(trimmed) as { id?: unknown; type?: unknown };
} catch {
return null;
}
} else if (typeof value === "object") {
candidate = value as { id?: unknown; type?: unknown };
}
if (!candidate) return null;
const id = typeof candidate.id === "string" ? candidate.id : "";
const type = typeof candidate.type === "string" ? candidate.type : "";
if (!id.startsWith("rs_")) return null;
if (type === "reasoning" || type.startsWith("reasoning.")) {
return { id, type };
}
return null;
}
function hasFollowingNonThinkingBlock(
content: Extract<AgentMessage, { role: "assistant" }>["content"],
index: number,
): boolean {
for (let i = index + 1; i < content.length; i++) {
const block = content[i];
if (!block || typeof block !== "object") return true;
if ((block as { type?: unknown }).type !== "thinking") return true;
}
return false;
}
/**
* OpenAI Responses API can reject transcripts that contain a standalone `reasoning` item id
* without the required following item.
*
* Clawdbot persists provider-specific reasoning metadata in `thinkingSignature`; if that metadata
* is incomplete, drop the block to keep history usable.
*/
export function downgradeOpenAIReasoningBlocks(messages: AgentMessage[]): AgentMessage[] {
const out: AgentMessage[] = [];
for (const msg of messages) {
if (!msg || typeof msg !== "object") {
out.push(msg);
continue;
}
const role = (msg as { role?: unknown }).role;
if (role !== "assistant") {
out.push(msg);
continue;
}
const assistantMsg = msg as Extract<AgentMessage, { role: "assistant" }>;
if (!Array.isArray(assistantMsg.content)) {
out.push(msg);
continue;
}
let changed = false;
type AssistantContentBlock = (typeof assistantMsg.content)[number];
const nextContent: AssistantContentBlock[] = [];
for (let i = 0; i < assistantMsg.content.length; i++) {
const block = assistantMsg.content[i];
if (!block || typeof block !== "object") {
nextContent.push(block as AssistantContentBlock);
continue;
}
const record = block as OpenAIThinkingBlock;
if (record.type !== "thinking") {
nextContent.push(block as AssistantContentBlock);
continue;
}
const signature = parseOpenAIReasoningSignature(record.thinkingSignature);
if (!signature) {
nextContent.push(block as AssistantContentBlock);
continue;
}
if (hasFollowingNonThinkingBlock(assistantMsg.content, i)) {
nextContent.push(block as AssistantContentBlock);
continue;
}
changed = true;
}
if (!changed) {
out.push(msg);
continue;
}
if (nextContent.length === 0) {
continue;
}
out.push({ ...assistantMsg, content: nextContent } as AgentMessage);
}
return out;
}

View File

@@ -128,7 +128,7 @@ describe("getDmHistoryLimitFromSessionKey", () => {
slack: { dmHistoryLimit: 10 },
},
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:c1", config)).toBeUndefined();
expect(getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config)).toBeUndefined();
expect(getDmHistoryLimitFromSessionKey("telegram:slash:123", config)).toBeUndefined();
});
it("returns undefined for unknown provider", () => {

View File

@@ -126,7 +126,7 @@ describe("resolveSessionAgentIds", () => {
});
it("keeps the agent id for provider-qualified agent sessions", () => {
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: "agent:beta:slack:channel:c1",
sessionKey: "agent:beta:slack:channel:C1",
config: cfg,
});
expect(sessionAgentId).toBe("beta");

View File

@@ -63,12 +63,12 @@ const makeAttempt = (overrides: Partial<EmbeddedRunAttemptResult>): EmbeddedRunA
...overrides,
});
const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotConfig =>
const makeConfig = (): ClawdbotConfig =>
({
agents: {
defaults: {
model: {
fallbacks: opts?.fallbacks ?? [],
fallbacks: [],
},
},
},
@@ -76,7 +76,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
providers: {
openai: {
api: "openai-responses",
apiKey: opts?.apiKey ?? "sk-test",
apiKey: "sk-test",
baseUrl: "https://example.com",
models: [
{
@@ -94,13 +94,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
},
}) satisfies ClawdbotConfig;
const writeAuthStore = async (
agentDir: string,
opts?: {
includeAnthropic?: boolean;
usageStats?: Record<string, { lastUsed?: number; cooldownUntil?: number }>;
},
) => {
const writeAuthStore = async (agentDir: string, opts?: { includeAnthropic?: boolean }) => {
const authPath = path.join(agentDir, "auth-profiles.json");
const payload = {
version: 1,
@@ -111,12 +105,10 @@ const writeAuthStore = async (
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
: {}),
},
usageStats:
opts?.usageStats ??
({
"openai:p1": { lastUsed: 1 },
"openai:p2": { lastUsed: 2 },
} as Record<string, { lastUsed?: number }>),
usageStats: {
"openai:p1": { lastUsed: 1 },
"openai:p2": { lastUsed: 2 },
},
};
await fs.writeFile(authPath, JSON.stringify(payload));
};
@@ -392,92 +384,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
}
});
it("fails over when all profiles are in cooldown and fallbacks are configured", async () => {
vi.useFakeTimers();
try {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
const now = Date.now();
vi.setSystemTime(now);
try {
await writeAuthStore(agentDir, {
usageStats: {
"openai:p1": { lastUsed: 1, cooldownUntil: now + 60 * 60 * 1000 },
"openai:p2": { lastUsed: 2, cooldownUntil: now + 60 * 60 * 1000 },
},
});
await expect(
runEmbeddedPiAgent({
sessionId: "session:test",
sessionKey: "agent:test:cooldown-failover",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
prompt: "hello",
provider: "openai",
model: "mock-1",
authProfileIdSource: "auto",
timeoutMs: 5_000,
runId: "run:cooldown-failover",
}),
).rejects.toMatchObject({
name: "FailoverError",
reason: "rate_limit",
provider: "openai",
model: "mock-1",
});
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true });
}
} finally {
vi.useRealTimers();
}
});
it("fails over when auth is unavailable and fallbacks are configured", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
const previousOpenAiKey = process.env.OPENAI_API_KEY;
delete process.env.OPENAI_API_KEY;
try {
const authPath = path.join(agentDir, "auth-profiles.json");
await fs.writeFile(authPath, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }));
await expect(
runEmbeddedPiAgent({
sessionId: "session:test",
sessionKey: "agent:test:auth-unavailable",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"], apiKey: "" }),
prompt: "hello",
provider: "openai",
model: "mock-1",
authProfileIdSource: "auto",
timeoutMs: 5_000,
runId: "run:auth-unavailable",
}),
).rejects.toMatchObject({ name: "FailoverError", reason: "auth" });
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
} finally {
if (previousOpenAiKey === undefined) {
delete process.env.OPENAI_API_KEY;
} else {
process.env.OPENAI_API_KEY = previousOpenAiKey;
}
await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("skips profiles in cooldown when rotating after failure", async () => {
vi.useFakeTimers();
try {

View File

@@ -161,92 +161,4 @@ describe("sanitizeSessionHistory", () => {
expect(result).toHaveLength(1);
expect(result[0]?.role).toBe("assistant");
});
it("does not downgrade openai reasoning when the model has not changed", async () => {
const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [
{
type: "custom",
customType: "model-snapshot",
data: {
timestamp: Date.now(),
provider: "openai",
modelApi: "openai-responses",
modelId: "gpt-5.2-codex",
},
},
];
const sessionManager = {
getEntries: vi.fn(() => sessionEntries),
appendCustomEntry: vi.fn((customType: string, data: unknown) => {
sessionEntries.push({ type: "custom", customType, data });
}),
} as unknown as SessionManager;
const messages: AgentMessage[] = [
{
role: "assistant",
content: [
{
type: "thinking",
thinking: "reasoning",
thinkingSignature: JSON.stringify({ id: "rs_test", type: "reasoning" }),
},
],
},
];
const result = await sanitizeSessionHistory({
messages,
modelApi: "openai-responses",
provider: "openai",
modelId: "gpt-5.2-codex",
sessionManager,
sessionId: "test-session",
});
expect(result).toEqual(messages);
});
it("downgrades openai reasoning only when the model changes", async () => {
const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [
{
type: "custom",
customType: "model-snapshot",
data: {
timestamp: Date.now(),
provider: "anthropic",
modelApi: "anthropic-messages",
modelId: "claude-3-7",
},
},
];
const sessionManager = {
getEntries: vi.fn(() => sessionEntries),
appendCustomEntry: vi.fn((customType: string, data: unknown) => {
sessionEntries.push({ type: "custom", customType, data });
}),
} as unknown as SessionManager;
const messages: AgentMessage[] = [
{
role: "assistant",
content: [
{
type: "thinking",
thinking: "reasoning",
thinkingSignature: { id: "rs_test", type: "reasoning" },
},
],
},
];
const result = await sanitizeSessionHistory({
messages,
modelApi: "openai-responses",
provider: "openai",
modelId: "gpt-5.2-codex",
sessionManager,
sessionId: "test-session",
});
expect(result).toEqual([]);
});
});

View File

@@ -66,7 +66,6 @@ import { splitSdkTools } from "./tool-split.js";
import type { EmbeddedPiCompactResult } from "./types.js";
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
import { describeUnknownError, mapThinkingLevel, resolveExecToolDefaults } from "./utils.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
export async function compactEmbeddedPiSession(params: {
sessionId: string;
@@ -74,14 +73,6 @@ export async function compactEmbeddedPiSession(params: {
messageChannel?: string;
messageProvider?: string;
agentAccountId?: string;
/** Group id for channel-level tool policy resolution. */
groupId?: string | null;
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
groupChannel?: string | null;
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
groupSpace?: string | null;
/** Parent session key for subagent policy inheritance. */
spawnedBy?: string | null;
sessionFile: string;
workspaceDir: string;
agentDir?: string;
@@ -216,10 +207,6 @@ export async function compactEmbeddedPiSession(params: {
messageProvider: params.messageChannel ?? params.messageProvider,
agentAccountId: params.agentAccountId,
sessionKey: params.sessionKey ?? params.sessionId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
agentDir,
workspaceDir: effectiveWorkspace,
config: params.config,
@@ -299,7 +286,6 @@ export async function compactEmbeddedPiSession(params: {
cwd: process.cwd(),
moduleUrl: import.meta.url,
});
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
const appendPrompt = buildEmbeddedSystemPrompt({
workspaceDir: effectiveWorkspace,
defaultThinkLevel: params.thinkLevel,
@@ -312,7 +298,6 @@ export async function compactEmbeddedPiSession(params: {
: undefined,
skillsPrompt,
docsPath: docsPath ?? undefined,
ttsHint,
promptMode,
runtimeInfo,
messageToolHints,

View File

@@ -6,7 +6,6 @@ import type { SessionManager } from "@mariozechner/pi-coding-agent";
import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js";
import {
downgradeOpenAIReasoningBlocks,
isCompactionFailureError,
isGoogleModelApi,
sanitizeGoogleTurnOrdering,
@@ -212,50 +211,7 @@ registerUnhandledRejectionHandler((reason) => {
return true;
});
type CustomEntryLike = { type?: unknown; customType?: unknown; data?: unknown };
type ModelSnapshotEntry = {
timestamp: number;
provider?: string;
modelApi?: string | null;
modelId?: string;
};
const MODEL_SNAPSHOT_CUSTOM_TYPE = "model-snapshot";
function readLastModelSnapshot(sessionManager: SessionManager): ModelSnapshotEntry | null {
try {
const entries = sessionManager.getEntries();
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i] as CustomEntryLike;
if (entry?.type !== "custom" || entry?.customType !== MODEL_SNAPSHOT_CUSTOM_TYPE) continue;
const data = entry?.data as ModelSnapshotEntry | undefined;
if (data && typeof data === "object") {
return data;
}
}
} catch {
return null;
}
return null;
}
function appendModelSnapshot(sessionManager: SessionManager, data: ModelSnapshotEntry): void {
try {
sessionManager.appendCustomEntry(MODEL_SNAPSHOT_CUSTOM_TYPE, data);
} catch {
// ignore persistence failures
}
}
function isSameModelSnapshot(a: ModelSnapshotEntry, b: ModelSnapshotEntry): boolean {
const normalize = (value?: string | null) => value ?? "";
return (
normalize(a.provider) === normalize(b.provider) &&
normalize(a.modelApi) === normalize(b.modelApi) &&
normalize(a.modelId) === normalize(b.modelId)
);
}
type CustomEntryLike = { type?: unknown; customType?: unknown };
function hasGoogleTurnOrderingMarker(sessionManager: SessionManager): boolean {
try {
@@ -336,38 +292,12 @@ export async function sanitizeSessionHistory(params: {
? sanitizeToolUseResultPairing(sanitizedThinking)
: sanitizedThinking;
const isOpenAIResponsesApi =
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
const priorSnapshot = hasSnapshot ? readLastModelSnapshot(params.sessionManager) : null;
const modelChanged = priorSnapshot
? !isSameModelSnapshot(priorSnapshot, {
timestamp: 0,
provider: params.provider,
modelApi: params.modelApi,
modelId: params.modelId,
})
: false;
const sanitizedOpenAI =
isOpenAIResponsesApi && modelChanged
? downgradeOpenAIReasoningBlocks(repairedTools)
: repairedTools;
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
appendModelSnapshot(params.sessionManager, {
timestamp: Date.now(),
provider: params.provider,
modelApi: params.modelApi,
modelId: params.modelId,
});
}
if (!policy.applyGoogleTurnOrdering) {
return sanitizedOpenAI;
return repairedTools;
}
return applyGoogleTurnOrderingFix({
messages: sanitizedOpenAI,
messages: repairedTools,
modelApi: params.modelApi,
sessionManager: params.sessionManager,
sessionId: params.sessionId,

View File

@@ -38,7 +38,6 @@ import {
isRateLimitAssistantError,
isTimeoutErrorMessage,
pickFallbackThinkingLevel,
type FailoverReason,
} from "../pi-embedded-helpers.js";
import { normalizeUsage, type UsageLike } from "../usage.js";
@@ -93,8 +92,6 @@ export async function runEmbeddedPiAgent(
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
const fallbackConfigured =
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
await ensureClawdbotModelsJson(params.config, agentDir);
const { model, error, authStorage, modelRegistry } = resolveModel(
@@ -168,42 +165,6 @@ export async function runEmbeddedPiAgent(
let apiKeyInfo: ApiKeyInfo | null = null;
let lastProfileId: string | undefined;
const resolveAuthProfileFailoverReason = (params: {
allInCooldown: boolean;
message: string;
}): FailoverReason => {
if (params.allInCooldown) return "rate_limit";
const classified = classifyFailoverReason(params.message);
return classified ?? "auth";
};
const throwAuthProfileFailover = (params: {
allInCooldown: boolean;
message?: string;
error?: unknown;
}): never => {
const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
const message =
params.message?.trim() ||
(params.error ? describeUnknownError(params.error).trim() : "") ||
fallbackMessage;
const reason = resolveAuthProfileFailoverReason({
allInCooldown: params.allInCooldown,
message,
});
if (fallbackConfigured) {
throw new FailoverError(message, {
reason,
provider,
model: modelId,
status: resolveFailoverStatus(reason),
cause: params.error,
});
}
if (params.error instanceof Error) throw params.error;
throw new Error(message);
};
const resolveApiKeyForCandidate = async (candidate?: string) => {
return getApiKeyForModel({
model,
@@ -277,17 +238,14 @@ export async function runEmbeddedPiAgent(
break;
}
if (profileIndex >= profileCandidates.length) {
throwAuthProfileFailover({ allInCooldown: true });
throw new Error(
`No available auth profile for ${provider} (all in cooldown or unavailable).`,
);
}
} catch (err) {
if (err instanceof FailoverError) throw err;
if (profileCandidates[profileIndex] === lockedProfileId) {
throwAuthProfileFailover({ allInCooldown: false, error: err });
}
if (profileCandidates[profileIndex] === lockedProfileId) throw err;
const advanced = await advanceAuthProfile();
if (!advanced) {
throwAuthProfileFailover({ allInCooldown: false, error: err });
}
if (!advanced) throw err;
}
try {
@@ -306,10 +264,6 @@ export async function runEmbeddedPiAgent(
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
replyToMode: params.replyToMode,
@@ -435,7 +389,9 @@ export async function runEmbeddedPiAgent(
}
// FIX: Throw FailoverError for prompt errors when fallbacks configured
// This enables model fallback for quota/rate limit errors during prompt submission
if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
const promptFallbackConfigured =
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
if (promptFallbackConfigured && isFailoverErrorMessage(errorText)) {
throw new FailoverError(errorText, {
reason: promptFailoverReason ?? "unknown",
provider,
@@ -459,6 +415,8 @@ export async function runEmbeddedPiAgent(
continue;
}
const fallbackConfigured =
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
const authFailure = isAuthAssistantError(lastAssistant);
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
const failoverFailure = isFailoverAssistantError(lastAssistant);

View File

@@ -20,7 +20,6 @@ import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { resolveUserPath } from "../../../utils.js";
import { createCacheTrace } from "../../cache-trace.js";
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
import { resolveSessionAgentIds } from "../../agent-scope.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
@@ -78,7 +77,6 @@ import { toClientToolDefinitions } from "../../pi-tool-definition-adapter.js";
import { buildSystemPromptParams } from "../../system-prompt-params.js";
import { describeUnknownError, mapThinkingLevel } from "../utils.js";
import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
import { buildTtsSystemPromptHint } from "../../../tts/tts.js";
import { isTimeoutError } from "../../failover-error.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
@@ -210,10 +208,6 @@ export async function runEmbeddedAttempt(
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
sessionKey: params.sessionKey ?? params.sessionId,
agentDir,
workspaceDir: effectiveWorkspace,
@@ -316,7 +310,6 @@ export async function runEmbeddedAttempt(
cwd: process.cwd(),
moduleUrl: import.meta.url,
});
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
const appendPrompt = buildEmbeddedSystemPrompt({
workspaceDir: effectiveWorkspace,
@@ -330,7 +323,6 @@ export async function runEmbeddedAttempt(
: undefined,
skillsPrompt,
docsPath: docsPath ?? undefined,
ttsHint,
workspaceNotes,
reactionGuidance,
promptMode,
@@ -462,16 +454,6 @@ export async function runEmbeddedAttempt(
modelApi: params.model.api,
workspaceDir: params.workspaceDir,
});
const anthropicPayloadLogger = createAnthropicPayloadLogger({
env: process.env,
runId: params.runId,
sessionId: activeSession.sessionId,
sessionKey: params.sessionKey,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
workspaceDir: params.workspaceDir,
});
// Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai.
activeSession.agent.streamFn = streamSimple;
@@ -492,11 +474,6 @@ export async function runEmbeddedAttempt(
});
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
}
if (anthropicPayloadLogger) {
activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(
activeSession.agent.streamFn,
);
}
try {
const prior = await sanitizeSessionHistory({
@@ -791,7 +768,6 @@ export async function runEmbeddedAttempt(
messages: messagesSnapshot,
note: promptError ? "prompt error" : undefined,
});
anthropicPayloadLogger?.recordUsage(messagesSnapshot, promptError);
// Run agent_end hooks to allow plugins to analyze the conversation
// This is fire-and-forget, so we don't await

View File

@@ -27,14 +27,6 @@ export type RunEmbeddedPiAgentParams = {
messageTo?: string;
/** Thread/topic identifier for routing replies to the originating thread. */
messageThreadId?: string | number;
/** Group id for channel-level tool policy resolution. */
groupId?: string | null;
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
groupChannel?: string | null;
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
groupSpace?: string | null;
/** Parent session key for subagent policy inheritance. */
spawnedBy?: string | null;
/** Current channel ID for auto-threading (Slack). */
currentChannelId?: string;
/** Current thread timestamp for auto-threading (Slack). */

View File

@@ -148,35 +148,6 @@ describe("buildEmbeddedRunPayloads", () => {
expect(payloads[0]?.text).toBe("All good");
});
it("adds tool error fallback when the assistant only invoked tools", () => {
const payloads = buildEmbeddedRunPayloads({
assistantTexts: [],
toolMetas: [],
lastAssistant: {
stopReason: "toolUse",
content: [
{
type: "toolCall",
id: "toolu_01",
name: "exec",
arguments: { command: "echo hi" },
},
],
} as AssistantMessage,
lastToolError: { toolName: "exec", error: "Command exited with code 1" },
sessionKey: "session:telegram",
inlineToolResultsAllowed: false,
verboseLevel: "off",
reasoningLevel: "off",
toolResultFormat: "plain",
});
expect(payloads).toHaveLength(1);
expect(payloads[0]?.isError).toBe(true);
expect(payloads[0]?.text).toContain("Exec");
expect(payloads[0]?.text).toContain("code 1");
});
it("suppresses recoverable tool errors containing 'required'", () => {
const payloads = buildEmbeddedRunPayloads({
assistantTexts: [],

Some files were not shown because too many files have changed in this diff Show More