Compare commits

...
Author SHA1 Message Date
Peter Steinberger f0961d3b82 fix: normalize pairing aliases and webhook guard (#991) (thanks @longmaba) 2026-01-16 04:53:40 +00:00
LongandAmp dbd4481bb1 fix(zalo): fix pairing channel detection and webhook payload format
Amp-Thread-ID: https://ampcode.com/threads/T-019bc4e0-fcb1-77be-b0b5-0d498f0c7197
Co-authored-by: Amp <amp@ampcode.com>
2026-01-16 04:46:42 +00:00
Peter Steinberger 236b27cb3a docs: align plugin changelogs 2026-01-16 04:15:48 +00:00
Peter Steinberger 1634abf293 docs: expand msteams plugin changelog 2026-01-16 04:15:48 +00:00
UbuntuandAlphonse-arianee ca9688b5cc feat(session): add dmScope for multi-user DM isolation
Co-authored-by: Alphonse-arianee <Alphonse-arianee@users.noreply.github.com>
2026-01-16 04:13:10 +00:00
Peter Steinberger e6364d031d fix: stabilize windows queue + pairing tests 2026-01-16 04:02:43 +00:00
Peter Steinberger 01c8d099ad fix: repair CI formatting + launchd test 2026-01-16 03:52:47 +00:00
Peter Steinberger f6e619f078 fix: normalize daemon unit paths 2026-01-16 03:47:26 +00:00
Peter Steinberger b2b331230b feat: mac node exec policy + remote skills hot reload 2026-01-16 03:45:06 +00:00
Peter Steinberger abcca86e4e chore: format and sync protocol outputs 2026-01-16 03:30:56 +00:00
Peter SteinbergerandChristoph Nakazawa a5d8f89b53 feat(browser): prefer Chrome default + add Brave/Edge fallbacks
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
2026-01-16 03:28:56 +00:00
Peter Steinberger a0d2a7232e fix: allow media-only sends 2026-01-16 03:15:26 +00:00
Peter Steinberger f449115ec5 test: extend vitest timeouts 2026-01-16 03:11:16 +00:00
Peter Steinberger 16bc4cdef3 chore: drop legacy Relay signing 2026-01-16 03:11:16 +00:00
Peter SteinbergerandDrake Thomsen 23e4ba845c fix: sanitize user-facing errors and strip final tags
Co-authored-by: Drake Thomsen <drake.thomsen@example.com>
2026-01-16 03:01:23 +00:00
Peter Steinberger d9f9e93dee feat!: move msteams to plugin 2026-01-16 02:59:43 +00:00
Peter Steinberger dae34f3a61 docs: clarify setup-token location 2026-01-16 02:53:40 +00:00
Adam HoltandClaude Opus 4.5 af61b353a4 docs(showcase): update Dream Team card with architecture articles
Combine Multi-Agent Swarm card with new technical write-ups:
- Link to orchestrated-ai-articles repo (manifesto + architecture)
- Keep clawdspace link for agent sandboxing
- Add blog post link

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 02:52:27 +00:00
Peter Steinberger 29476b222d fix: restore status usage summary output 2026-01-16 02:49:18 +00:00
Peter Steinberger e7c16cc0e6 docs: clarify setup-token flows 2026-01-16 02:36:32 +00:00
Peter Steinberger 3dddbe1053 fix: ignore properties in google tool schema warnings 2026-01-16 02:35:55 +00:00
Peter SteinbergerandJohn Young 2dea6bfa7e docs: clarify gog body-file usage
Co-authored-by: John Young <johntheyoung@users.noreply.github.com>
2026-01-16 02:23:41 +00:00
Peter Steinberger 04e3bfed35 docs: clarify Anthropic setup-token 2026-01-16 02:19:57 +00:00
Peter Steinberger 3e32050601 fix: correct final tag strip typing 2026-01-16 02:16:17 +00:00
Peter Steinberger 7fb45ed9b8 fix: strip final tags from session messages 2026-01-16 02:16:17 +00:00
Peter SteinbergerandMax b7ba94f0c1 fix: harden antigravity claude support (#968)
Co-authored-by: Max <rdev@users.noreply.github.com>
2026-01-16 02:16:17 +00:00
Peter Steinberger 5b827528f8 fix: show oauth usage in /status 2026-01-16 02:06:27 +00:00
Peter Steinberger 409e33d9c2 Merge pull request #960 from kkarimi/fix/mac-node-bridge-tunnel-865
macOS: prefer bridge tunnel port in remote mode
2026-01-16 02:00:23 +00:00
Gustavo Madeira Santana 7c38b535f6 Document inline buttons configuration for Telegram (#984) 2026-01-15 19:55:03 -06:00
211 changed files with 3193 additions and 746 deletions

No files matched your search

+9
View File
@@ -2,8 +2,10 @@
## 2026.1.15 (unreleased)
- **BREAKING:** Microsoft Teams is now a plugin; install `@clawdbot/msteams` via `clawdbot plugins install @clawdbot/msteams`.
- CLI: set process titles to `clawdbot-<command>` for clearer process listings.
- Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf.
- Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
- Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.
- TUI: show provider/model labels for the active session and default model.
@@ -14,9 +16,12 @@
- Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts.
- Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
- Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
- Agents: avoid false positives when logging unsupported Google tool schema keywords.
- Status: restore usage summary line for current provider when no OAuth profiles exist.
- Tools: normalize Slack/Discord message timestamps with `timestampMs`/`timestampUtc` while keeping raw provider fields.
- Docs: add Date & Time guide and update prompt/timezone configuration docs.
- Messages: debounce rapid inbound messages across channels with per-connector overrides. (#971) — thanks @juanpablodlc.
- Messages: allow media-only sends (CLI/tool) and show Telegram voice recording status for voice notes. (#957) — thanks @rdev.
- Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in `/status` and `clawdbot models status`, and update docs.
- Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.
- Fix: refactor session store updates, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
@@ -25,10 +30,13 @@
- Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` → `act`.
- Browser: `profile="chrome"` now defaults to host control and returns clearer “attach a tab” errors.
- Browser: extension mode recovers when only one tab is attached (stale targetId fallback).
- Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer.
- Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page).
- Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi.
- Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) — thanks @azade-c.
- Discord: allow allowlisted guilds without channel lists to receive messages when `groupPolicy="allowlist"`. — thanks @thewilloftheshadow.
- Fix: sanitize user-facing error text + strip `<final>` tags across reply pipelines. (#975) — thanks @ThomsenDrake.
- Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.
## 2026.1.14-1
@@ -60,6 +68,7 @@
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
- Agents: harden Antigravity Claude history/tool-call sanitization. (#968) — thanks @rdev.
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
+17 -17
View File
@@ -475,22 +475,22 @@ 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/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/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/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/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/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/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/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/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/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/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/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/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/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/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/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/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/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/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/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a>
<a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/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/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/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/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/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></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/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/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/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/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/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/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a>
<a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/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/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/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/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a>
<a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></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/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/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/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/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/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/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=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/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/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/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></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=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/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/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/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/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/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/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/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/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/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/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/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/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/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/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/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/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/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/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/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/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/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/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/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></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/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/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/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/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/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/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/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/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/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/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></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/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/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/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/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=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/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/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></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=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a>
<a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/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/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=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></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/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/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/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/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/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/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/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/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/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/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/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a>
<a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/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/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=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
</p>
@@ -170,6 +170,10 @@ final class AppState {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.canvasEnabled, forKey: canvasEnabledKey) } }
}
var systemRunPolicy: SystemRunPolicy {
didSet { self.ifNotPreview { MacNodeConfigFile.setSystemRunPolicy(self.systemRunPolicy) } }
}
/// Tracks whether the Canvas panel is currently visible (not persisted).
var canvasPanelVisible: Bool = false
@@ -292,6 +296,7 @@ final class AppState {
self.remoteProjectRoot = UserDefaults.standard.string(forKey: remoteProjectRootKey) ?? ""
self.remoteCliPath = UserDefaults.standard.string(forKey: remoteCliPathKey) ?? ""
self.canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
self.systemRunPolicy = SystemRunPolicy.load()
self.peekabooBridgeEnabled = UserDefaults.standard
.object(forKey: peekabooBridgeEnabledKey) as? Bool ?? true
if !self.isPreview {
@@ -26,6 +26,9 @@ let remoteProjectRootKey = "clawdbot.remoteProjectRoot"
let remoteCliPathKey = "clawdbot.remoteCliPath"
let canvasEnabledKey = "clawdbot.canvasEnabled"
let cameraEnabledKey = "clawdbot.cameraEnabled"
let systemRunPolicyKey = "clawdbot.systemRunPolicy"
let systemRunAllowlistKey = "clawdbot.systemRunAllowlist"
let systemRunEnabledKey = "clawdbot.systemRunEnabled"
let locationModeKey = "clawdbot.locationMode"
let locationPreciseKey = "clawdbot.locationPreciseEnabled"
let peekabooBridgeEnabledKey = "clawdbot.peekabooBridgeEnabled"
@@ -83,6 +83,25 @@ struct GeneralSettings: View {
subtitle: "Allow the agent to capture a photo or short video via the built-in camera.",
binding: self.$cameraEnabled)
VStack(alignment: .leading, spacing: 6) {
Text("Node Run Commands")
.font(.body)
Picker("", selection: self.$state.systemRunPolicy) {
ForEach(SystemRunPolicy.allCases) { policy in
Text(policy.title).tag(policy)
}
}
.pickerStyle(.segmented)
Text("""
Controls remote command execution on this Mac when it is paired as a node. "Always Ask" prompts on each command; "Always Allow" runs without prompts; "Never" disables `system.run`.
""")
.font(.footnote)
.foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true)
}
VStack(alignment: .leading, spacing: 6) {
Text("Location Access")
.font(.body)
@@ -0,0 +1,80 @@
import Foundation
import OSLog
enum MacNodeConfigFile {
private static let logger = Logger(subsystem: "com.clawdbot", category: "mac-node-config")
static func url() -> URL {
ClawdbotPaths.stateDirURL.appendingPathComponent("macos-node.json")
}
static func loadDict() -> [String: Any] {
let url = self.url()
guard FileManager.default.fileExists(atPath: url.path) else { return [:] }
do {
let data = try Data(contentsOf: url)
guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
self.logger.warning("mac node config JSON root invalid")
return [:]
}
return root
} catch {
self.logger.warning("mac node config read failed: \(error.localizedDescription, privacy: .public)")
return [:]
}
}
static func saveDict(_ dict: [String: Any]) {
do {
let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted, .sortedKeys])
let url = self.url()
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: url, options: [.atomic])
} catch {
self.logger.error("mac node config save failed: \(error.localizedDescription, privacy: .public)")
}
}
static func systemRunPolicy() -> SystemRunPolicy? {
let root = self.loadDict()
let systemRun = root["systemRun"] as? [String: Any]
let raw = systemRun?["policy"] as? String
guard let raw, let policy = SystemRunPolicy(rawValue: raw) else { return nil }
return policy
}
static func setSystemRunPolicy(_ policy: SystemRunPolicy) {
var root = self.loadDict()
var systemRun = root["systemRun"] as? [String: Any] ?? [:]
systemRun["policy"] = policy.rawValue
root["systemRun"] = systemRun
self.saveDict(root)
}
static func systemRunAllowlist() -> [String]? {
let root = self.loadDict()
let systemRun = root["systemRun"] as? [String: Any]
return systemRun?["allowlist"] as? [String]
}
static func setSystemRunAllowlist(_ allowlist: [String]) {
let cleaned = allowlist
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
var root = self.loadDict()
var systemRun = root["systemRun"] as? [String: Any] ?? [:]
if cleaned.isEmpty {
systemRun.removeValue(forKey: "allowlist")
} else {
systemRun["allowlist"] = cleaned
}
if systemRun.isEmpty {
root.removeValue(forKey: "systemRun")
} else {
root["systemRun"] = systemRun
}
self.saveDict(root)
}
}
@@ -31,6 +31,12 @@ struct MenuContent: View {
self._updateStatus = Bindable(wrappedValue: updater?.updateStatus ?? UpdateStatus.disabled)
}
private var systemRunPolicyBinding: Binding<SystemRunPolicy> {
Binding(
get: { self.state.systemRunPolicy },
set: { self.state.systemRunPolicy = $0 })
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Toggle(isOn: self.activeBinding) {
@@ -68,6 +74,13 @@ struct MenuContent: View {
Toggle(isOn: self.$cameraEnabled) {
Label("Allow Camera", systemImage: "camera")
}
Picker(selection: self.systemRunPolicyBinding) {
ForEach(SystemRunPolicy.allCases) { policy in
Text(policy.title).tag(policy)
}
} label: {
Label("Node Run Commands", systemImage: "terminal")
}
Toggle(isOn: Binding(get: { self.state.canvasEnabled }, set: { self.state.canvasEnabled = $0 })) {
Label("Allow Canvas", systemImage: "rectangle.and.pencil.and.ellipsis")
}
@@ -37,6 +37,7 @@ final class MacNodeModeCoordinator {
private func run() async {
var retryDelay: UInt64 = 1_000_000_000
var lastCameraEnabled: Bool?
var lastSystemRunPolicy: SystemRunPolicy?
let defaults = UserDefaults.standard
while !Task.isCancelled {
if await MainActor.run(body: { AppStateStore.shared.isPaused }) {
@@ -53,6 +54,15 @@ final class MacNodeModeCoordinator {
try? await Task.sleep(nanoseconds: 200_000_000)
}
let systemRunPolicy = SystemRunPolicy.load()
if lastSystemRunPolicy == nil {
lastSystemRunPolicy = systemRunPolicy
} else if lastSystemRunPolicy != systemRunPolicy {
lastSystemRunPolicy = systemRunPolicy
await self.session.disconnect()
try? await Task.sleep(nanoseconds: 200_000_000)
}
guard let endpoint = await self.resolveBridgeEndpoint(timeoutSeconds: 5) else {
try? await Task.sleep(nanoseconds: min(retryDelay, 5_000_000_000))
retryDelay = min(retryDelay * 2, 10_000_000_000)
@@ -143,10 +153,13 @@ final class MacNodeModeCoordinator {
ClawdbotCanvasA2UICommand.pushJSONL.rawValue,
ClawdbotCanvasA2UICommand.reset.rawValue,
MacNodeScreenCommand.record.rawValue,
ClawdbotSystemCommand.run.rawValue,
ClawdbotSystemCommand.notify.rawValue,
]
if SystemRunPolicy.load() != .never {
commands.append(ClawdbotSystemCommand.run.rawValue)
}
let capsSet = Set(caps)
if capsSet.contains(ClawdbotCapability.camera.rawValue) {
commands.append(ClawdbotCameraCommand.list.rawValue)
@@ -426,6 +426,44 @@ actor MacNodeRuntime {
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: command required")
}
let wasAllowlisted = SystemRunAllowlist.contains(command)
switch Self.systemRunPolicy() {
case .never:
return Self.errorResponse(
req,
code: .unavailable,
message: "SYSTEM_RUN_DISABLED: policy=never")
case .always:
break
case .ask:
if !wasAllowlisted {
let services = await self.mainActorServices()
let decision = await services.confirmSystemRun(
command: SystemRunAllowlist.displayString(for: command),
cwd: params.cwd)
switch decision {
case .allowOnce:
break
case .allowAlways:
SystemRunAllowlist.add(command)
case .deny:
return Self.errorResponse(
req,
code: .unavailable,
message: "SYSTEM_RUN_DENIED: user denied")
}
}
}
var env = params.env
if wasAllowlisted, let overrides = env {
var merged = ProcessInfo.processInfo.environment
for (key, value) in overrides where key != "PATH" {
merged[key] = value
}
env = merged
}
if params.needsScreenRecording == true {
let authorized = await PermissionManager
.status([.screenRecording])[.screenRecording] ?? false
@@ -441,7 +479,7 @@ actor MacNodeRuntime {
let result = await ShellExecutor.runDetailed(
command: command,
cwd: params.cwd,
env: params.env,
env: env,
timeout: timeoutSec)
struct RunPayload: Encodable {
@@ -529,6 +567,10 @@ actor MacNodeRuntime {
UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false
}
private nonisolated static func systemRunPolicy() -> SystemRunPolicy {
SystemRunPolicy.load()
}
private nonisolated static func locationMode() -> ClawdbotLocationMode {
let raw = UserDefaults.standard.string(forKey: locationModeKey) ?? "off"
return ClawdbotLocationMode(rawValue: raw) ?? .off
@@ -1,7 +1,14 @@
import AppKit
import ClawdbotKit
import CoreLocation
import Foundation
enum SystemRunDecision: Sendable {
case allowOnce
case allowAlways
case deny
}
@MainActor
protocol MacNodeRuntimeMainActorServices: Sendable {
func recordScreen(
@@ -17,6 +24,8 @@ protocol MacNodeRuntimeMainActorServices: Sendable {
desiredAccuracy: ClawdbotLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
func confirmSystemRun(command: String, cwd: String?) async -> SystemRunDecision
}
@MainActor
@@ -57,4 +66,31 @@ final class LiveMacNodeRuntimeMainActorServices: MacNodeRuntimeMainActorServices
maxAgeMs: maxAgeMs,
timeoutMs: timeoutMs)
}
func confirmSystemRun(command: String, cwd: String?) async -> SystemRunDecision {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = "Allow this command?"
var details = "Clawdbot wants to run:\n\n\(command)"
let trimmedCwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedCwd.isEmpty {
details += "\n\nWorking directory:\n\(trimmedCwd)"
}
details += "\n\nThis runs on this Mac via node mode."
alert.informativeText = details
alert.addButton(withTitle: "Allow Once")
alert.addButton(withTitle: "Always Allow")
alert.addButton(withTitle: "Don't Allow")
switch alert.runModal() {
case .alertFirstButtonReturn:
return .allowOnce
case .alertSecondButtonReturn:
return .allowAlways
default:
return .deny
}
}
}
@@ -0,0 +1,89 @@
import Foundation
enum SystemRunPolicy: String, CaseIterable, Identifiable {
case never
case ask
case always
var id: String { self.rawValue }
var title: String {
switch self {
case .never:
return "Never"
case .ask:
return "Always Ask"
case .always:
return "Always Allow"
}
}
static func load(from defaults: UserDefaults = .standard) -> SystemRunPolicy {
if let policy = MacNodeConfigFile.systemRunPolicy() {
return policy
}
if let raw = defaults.string(forKey: systemRunPolicyKey),
let policy = SystemRunPolicy(rawValue: raw)
{
MacNodeConfigFile.setSystemRunPolicy(policy)
return policy
}
if let legacy = defaults.object(forKey: systemRunEnabledKey) as? Bool {
let policy: SystemRunPolicy = legacy ? .ask : .never
MacNodeConfigFile.setSystemRunPolicy(policy)
return policy
}
let fallback: SystemRunPolicy = .ask
MacNodeConfigFile.setSystemRunPolicy(fallback)
return fallback
}
}
enum SystemRunAllowlist {
static func key(for argv: [String]) -> String {
let trimmed = argv.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard !trimmed.isEmpty else { return "" }
if let data = try? JSONEncoder().encode(trimmed),
let json = String(data: data, encoding: .utf8)
{
return json
}
return trimmed.joined(separator: " ")
}
static func displayString(for argv: [String]) -> String {
argv.map { arg in
let trimmed = arg.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "\"\"" }
let needsQuotes = trimmed.contains { $0.isWhitespace || $0 == "\"" }
if !needsQuotes { return trimmed }
let escaped = trimmed.replacingOccurrences(of: "\"", with: "\\\"")
return "\"\(escaped)\""
}.joined(separator: " ")
}
static func load(from defaults: UserDefaults = .standard) -> Set<String> {
if let allowlist = MacNodeConfigFile.systemRunAllowlist() {
return Set(allowlist)
}
if let legacy = defaults.stringArray(forKey: systemRunAllowlistKey), !legacy.isEmpty {
MacNodeConfigFile.setSystemRunAllowlist(legacy)
return Set(legacy)
}
return []
}
static func contains(_ argv: [String], defaults: UserDefaults = .standard) -> Bool {
let key = key(for: argv)
return load(from: defaults).contains(key)
}
static func add(_ argv: [String], defaults: UserDefaults = .standard) {
let key = key(for: argv)
guard !key.isEmpty else { return }
var allowlist = load(from: defaults)
if allowlist.insert(key).inserted {
MacNodeConfigFile.setSystemRunAllowlist(Array(allowlist).sorted())
}
}
}
@@ -1679,6 +1679,27 @@ public struct ChatAbortParams: Codable, Sendable {
}
}
public struct ChatInjectParams: Codable, Sendable {
public let sessionkey: String
public let message: String
public let label: String?
public init(
sessionkey: String,
message: String,
label: String?
) {
self.sessionkey = sessionkey
self.message = message
self.label = label
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case message
case label
}
}
public struct ChatEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
+1 -1
View File
@@ -17,7 +17,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Signal](/channels/signal) — signal-cli; privacy-focused.
- [iMessage](/channels/imessage) — macOS only; native integration.
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support.
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
+45 -17
View File
@@ -3,20 +3,43 @@ summary: "Microsoft Teams bot support status, capabilities, and configuration"
read_when:
- Working on MS Teams channel features
---
# Microsoft Teams (Bot Framework)
# Microsoft Teams (plugin)
> "Abandon all hope, ye who enter here."
Updated: 2026-01-08
Updated: 2026-01-16
Status: text + DM attachments are supported; channel/group attachments require Microsoft Graph permissions. Polls are sent via Adaptive Cards.
## Plugin required
Microsoft Teams ships as a plugin and is not bundled with the core install.
**Breaking change (2026.1.15):** MS Teams moved out of core. If you use it, you must install the plugin.
Explainable: keeps core installs lighter and lets MS Teams dependencies update independently.
Install via CLI (npm registry):
```bash
clawdbot plugins install @clawdbot/msteams
```
Local checkout (when running from a git repo):
```bash
clawdbot plugins install ./extensions/msteams
```
If you choose Teams during configure/onboarding and a git checkout is detected,
Clawdbot will offer the local install path automatically.
Details: [Plugins](/plugin)
## Quick setup (beginner)
1) Create an **Azure Bot** (App ID + client secret + tenant ID).
2) Configure Clawdbot with those credentials.
3) Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
4) Install the Teams app package and start the gateway.
1) Install the Microsoft Teams plugin.
2) Create an **Azure Bot** (App ID + client secret + tenant ID).
3) Configure Clawdbot with those credentials.
4) Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
5) Install the Teams app package and start the gateway.
Minimal config:
```json5
@@ -73,11 +96,12 @@ Example:
```
## How it works
1. Create an **Azure Bot** (App ID + secret + tenant ID).
2. Build a **Teams app package** that references the bot and includes the RSC permissions below.
3. Upload/install the Teams app into a team (or personal scope for DMs).
4. Configure `msteams` in `~/.clawdbot/clawdbot.json` (or env vars) and start the gateway.
5. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
1. Install the Microsoft Teams plugin.
2. Create an **Azure Bot** (App ID + secret + tenant ID).
3. Build a **Teams app package** that references the bot and includes the RSC permissions below.
4. Upload/install the Teams app into a team (or personal scope for DMs).
5. Configure `msteams` in `~/.clawdbot/clawdbot.json` (or env vars) and start the gateway.
6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
## Azure Bot Setup (Prerequisites)
@@ -166,13 +190,17 @@ This is often easier than hand-editing JSON manifests.
3. Check gateway logs for incoming activity
## Setup (minimal text-only)
1. **Bot registration**
1. **Install the Microsoft Teams plugin**
- From npm: `clawdbot plugins install @clawdbot/msteams`
- From a local checkout: `clawdbot plugins install ./extensions/msteams`
2. **Bot registration**
- Create an Azure Bot (see above) and note:
- App ID
- Client secret (App password)
- Tenant ID (single-tenant)
2. **Teams app manifest**
3. **Teams app manifest**
- Include a `bot` entry with `botId = <App ID>`.
- Scopes: `personal`, `team`, `groupChat`.
- `supportsFiles: true` (required for personal scope file handling).
@@ -180,7 +208,7 @@ This is often easier than hand-editing JSON manifests.
- Create icons: `outline.png` (32x32) and `color.png` (192x192).
- Zip all three files together: `manifest.json`, `outline.png`, `color.png`.
3. **Configure Clawdbot**
4. **Configure Clawdbot**
```json
{
"msteams": {
@@ -198,12 +226,12 @@ This is often easier than hand-editing JSON manifests.
- `MSTEAMS_APP_PASSWORD`
- `MSTEAMS_TENANT_ID`
4. **Bot endpoint**
5. **Bot endpoint**
- Set the Azure Bot Messaging Endpoint to:
- `https://<host>:3978/api/messages` (or your chosen path/port).
5. **Run the gateway**
- The Teams channel starts automatically when `msteams` config exists and credentials are set.
6. **Run the gateway**
- The Teams channel starts automatically when the plugin is installed and `msteams` config exists with credentials.
## History context
- `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt.
+64
View File
@@ -194,6 +194,68 @@ Telegram forum topics include a `message_thread_id` per message. Clawdbot:
Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
## Inline Buttons
Telegram supports inline keyboards with callback buttons. Enable this feature via capabilities:
```json5
{
"channels": {
"telegram": {
"capabilities": ["inlineButtons"]
}
}
}
```
For per-account configuration:
```json5
{
"channels": {
"telegram": {
"accounts": {
"main": {
"capabilities": ["inlineButtons"]
}
}
}
}
}
```
### Sending buttons
Use the message tool with the `buttons` parameter:
```json5
{
"action": "send",
"channel": "telegram",
"to": "123456789",
"message": "Choose an option:",
"buttons": [
[
{"text": "Yes", "callback_data": "yes"},
{"text": "No", "callback_data": "no"}
],
[
{"text": "Cancel", "callback_data": "cancel"}
]
]
}
```
When a user clicks a button, the callback data is sent back to the agent as a message with the format:
`callback_data: value`
### Configuration options
Telegram capabilities can be configured at two levels:
- `channels.telegram.capabilities`: Global default capability list applied to all Telegram accounts unless overridden.
- `channels.telegram.accounts.<account>.capabilities`: Per-account capabilities that override or extend the global defaults for that specific account.
Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups or has extra capabilities).
## Access control (DMs + groups)
### DM access
@@ -392,6 +454,8 @@ Provider options:
- `channels.telegram.groups.<id>.enabled`: disable the group when `false`.
- `channels.telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
- `channels.telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
- `channels.telegram.capabilities`: Enable channel features (e.g., "inlineButtons").
- `channels.telegram.accounts.<account>.capabilities`: Per-account capabilities.
- `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
+1 -1
View File
@@ -785,7 +785,7 @@ Location:
## Browser
Browser control CLI (dedicated Chrome/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser).
Browser control CLI (dedicated Chrome/Brave/Edge/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser).
Common options:
- `--url <controlUrl>`
+1 -1
View File
@@ -44,7 +44,7 @@ Target formats (`--to`):
- `send`
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
- Required: `--to`, `--message`
- Required: `--to`, plus `--message` or `--media`
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
- Telegram only: `--buttons` (requires `"inlineButtons"` in `channels.telegram.capabilities` or `channels.telegram.accounts.<id>.capabilities`)
- Telegram only: `--thread-id` (forum topic id)
+4
View File
@@ -43,3 +43,7 @@ clawdbot models auth paste-token
```
`models auth login` runs a provider plugin’s auth flow (OAuth/API key). Use
`clawdbot plugins list` to see which providers are installed.
Notes:
- `setup-token` runs `claude setup-token` on the current machine (requires the Claude Code CLI).
- `paste-token` accepts a token string generated elsewhere.
+1
View File
@@ -20,3 +20,4 @@ clawdbot security audit --deep
clawdbot security audit --fix
```
The audit warns when multiple DM senders share the main session and recommends `session.dmScope="per-channel-peer"` for shared inboxes.
+1 -1
View File
@@ -152,7 +152,7 @@ JSON includes `auth.oauth` (warn window + profiles) and `auth.providers`
(effective auth per provider).
Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
Preferred Anthropic auth is the Claude Code CLI setup-token (run on the gateway host):
Preferred Anthropic auth is the Claude Code CLI setup-token (run anywhere; paste on the gateway host if needed):
```bash
claude setup-token
+11 -2
View File
@@ -7,6 +7,11 @@ read_when:
Clawdbot treats **one direct-chat session per agent** as primary. Direct chats collapse to `agent:<agentId>:<mainKey>` (default `main`), while group/channel chats get their own keys. `session.mainKey` is honored.
Use `session.dmScope` to control how **direct messages** are grouped:
- `main` (default): all DMs share the main session for continuity.
- `per-peer`: isolate by sender id across channels.
- `per-channel-peer`: isolate by channel + sender (recommended for multi-user inboxes).
## Gateway is the source of truth
All session state is **owned by the gateway** (the “master” Clawdbot). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files.
@@ -32,8 +37,11 @@ the workspace is writable. See [Memory](/concepts/memory) and
[Compaction](/concepts/compaction).
## Mapping transports → session keys
- Direct chats collapse to the per-agent primary key: `agent:<agentId>:<mainKey>`.
- Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
- Direct chats follow `session.dmScope` (default `main`).
- `main`: `agent:<agentId>:<mainKey>` (continuity across devices/channels).
- Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
- `per-peer`: `agent:<agentId>:dm:<peerId>`.
- `per-channel-peer`: `agent:<agentId>:<channel>:dm:<peerId>`.
- Group chats isolate state: `agent:<agentId>:<channel>:group:<id>` (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
- Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
- Legacy `group:<id>` keys are still recognized for migration.
@@ -77,6 +85,7 @@ Send these as standalone messages so they register.
{
session: {
scope: "per-sender", // keep group keys separate
dmScope: "main", // DM continuity (set per-channel-peer for shared inboxes)
idleMinutes: 120,
resetTriggers: ["/new", "/reset"],
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
+10 -4
View File
@@ -2261,6 +2261,7 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
{
session: {
scope: "per-sender",
dmScope: "main",
idleMinutes: 60,
resetTriggers: ["/new", "/reset"],
// Default is already per-agent under ~/.clawdbot/agents/<agentId>/sessions/sessions.json
@@ -2285,6 +2286,10 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
Fields:
- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
- Sandbox note: `agents.defaults.sandbox.mode: "non-main"` uses this key to detect the main session. Any session key that does not match `mainKey` (groups/channels) is sandboxed.
- `dmScope`: how DM sessions are grouped (default: `"main"`).
- `main`: all DMs share the main session for continuity.
- `per-peer`: isolate DMs by sender id across channels.
- `per-channel-peer`: isolate DMs per channel + sender (recommended for multi-user inboxes).
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
- `sendPolicy.rules[]`: match by `channel`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
@@ -2376,10 +2381,10 @@ Example:
}
```
### `browser` (clawd-managed Chrome)
### `browser` (clawd-managed browser)
Clawdbot can start a **dedicated, isolated** Chrome/Chromium instance for clawd and expose a small loopback control server.
Profiles can point at a **remote** Chrome via `profiles.<name>.cdpUrl`. Remote
Clawdbot can start a **dedicated, isolated** Chrome/Brave/Edge/Chromium instance for clawd and expose a small loopback control server.
Profiles can point at a **remote** Chromium-based browser via `profiles.<name>.cdpUrl`. Remote
profiles are attach-only (start/stop/reset are disabled).
`browser.cdpUrl` remains for legacy single-profile configs and as the base
@@ -2391,6 +2396,7 @@ Defaults:
- CDP URL: `http://127.0.0.1:18792` (control URL + 1, legacy single-profile)
- profile color: `#FF4500` (lobster-orange)
- Note: the control server is started by the running gateway (Clawdbot.app menubar, or `clawdbot gateway`).
- Auto-detect order: Chrome → Brave → Edge → Chromium → Chrome Canary.
```json5
{
@@ -2408,7 +2414,7 @@ Defaults:
// Advanced:
// headless: false,
// noSandbox: false,
// executablePath: "/usr/bin/chromium",
// executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
// attachOnly: false, // set true when tunneling a remote CDP to localhost
}
}
+1 -1
View File
@@ -165,7 +165,7 @@ Doctor checks:
### 5) Model auth health (OAuth expiry)
Doctor inspects OAuth profiles in the auth store, warns when tokens are
expiring/expired, and can refresh them when safe. If the Anthropic Claude Code
profile is stale, it suggests `claude setup-token` on the gateway host.
profile is stale, it suggests running `claude setup-token` (or pasting a setup-token).
Refresh prompts only appear when running interactively (TTY); `--non-interactive`
skips refresh attempts.
+28
View File
@@ -52,6 +52,22 @@ When the audit prints findings, treat this as a priority order:
5. **Plugins/extensions**: only load what you explicitly trust.
6. **Model choice**: prefer modern, instruction-hardened models for any bot with tools.
## Node execution (system.run)
If a macOS node is paired, the Gateway can invoke `system.run` on that node. This is **remote code execution** on the Mac:
- Requires node pairing (approval + token).
- Controlled on the Mac via **Settings → "Node Run Commands"**: "Always Ask" (default), "Always Allow", or "Never".
- If you don’t want remote execution, set the policy to "Never" and remove node pairing for that Mac.
## Dynamic skills (watcher / remote nodes)
Clawdbot can refresh the skills list mid-session:
- **Skills watcher**: changes to `SKILL.md` can update the skills snapshot on the next agent turn.
- **Remote nodes**: connecting a macOS node can make macOS-only skills eligible (based on bin probing).
Treat skill folders as **trusted code** and restrict who can modify them.
## The Threat Model
Your AI assistant can:
@@ -107,6 +123,18 @@ clawdbot pairing approve <channel> <code>
Details + files on disk: [Pairing](/start/pairing)
## DM session isolation (multi-user mode)
By default, Clawdbot routes **all DMs into the main session** so your assistant has continuity across devices and channels. If **multiple people** can DM the bot (open DMs or a multi-person allowlist), consider isolating DM sessions:
```json5
{
session: { dmScope: "per-channel-peer" }
}
```
This prevents cross-user context leakage while keeping group chats isolated. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration).
## Allowlists (DM + groups) — terminology
Clawdbot has two separate “who can trigger me?” layers:
+1
View File
@@ -163,6 +163,7 @@ Notes:
- `system.notify` respects notification permission state on the macOS app.
- `system.run` supports `--cwd`, `--env KEY=VAL`, `--command-timeout`, and `--needs-screen-recording`.
- `system.notify` supports `--priority <passive|active|timeSensitive>` and `--delivery <system|overlay|auto>`.
- `system.run` is gated by the macOS app policy (Settings → "Node Run Commands"): "Always Ask" prompts per command, "Always Allow" runs without prompts, and "Never" disables the tool. Denied prompts return `SYSTEM_RUN_DENIED`; disabled returns `SYSTEM_RUN_DISABLED`.
## Permissions map
+32
View File
@@ -54,6 +54,38 @@ The macOS app presents itself as a node. Common commands:
The node reports a `permissions` map so agents can decide what’s allowed.
## Node run policy + allowlist
`system.run` is controlled by the macOS app **Node Run Commands** policy:
- `Always Ask`: prompt per command (default).
- `Always Allow`: run without prompts.
- `Never`: disable `system.run` (tool not advertised).
The policy + allowlist live on the Mac in:
```
~/.clawdbot/macos-node.json
```
Schema:
```json
{
"systemRun": {
"policy": "ask",
"allowlist": [
"[\"/bin/echo\",\"hello\"]"
]
}
}
```
Notes:
- `allowlist` entries are JSON-encoded argv arrays.
- Choosing “Always Allow” in the prompt adds that command to the allowlist.
- Allowlisted runs ignore `PATH` overrides; other env vars are merged with the app’s environment.
## Deep links
The app registers the `clawdbot://` URL scheme for local actions.
+2
View File
@@ -35,9 +35,11 @@ See [Voice Call](/plugins/voice-call) for a concrete example plugin.
## Available plugins (official)
- Microsoft Teams is plugin-only as of 2026.1.15; install `@clawdbot/msteams` if you use Teams.
- [Voice Call](/plugins/voice-call) — `@clawdbot/voice-call`
- [Matrix](/channels/matrix) — `@clawdbot/matrix`
- [Zalo](/channels/zalo) — `@clawdbot/zalo`
- [Microsoft Teams](/channels/msteams) — `@clawdbot/msteams`
Clawdbot plugins are **TypeScript modules** loaded at runtime via jiti. They can
register:
+24 -2
View File
@@ -38,10 +38,32 @@ clawdbot onboard --anthropic-api-key "$ANTHROPIC_API_KEY"
**Best for:** using your Claude subscription or existing Claude Code CLI login.
### Where to get a setup-token
Setup-tokens are created by the **Claude Code CLI**, not the Anthropic Console. You can run this on **any machine**:
```bash
claude setup-token
```
Paste the token into Clawdbot (wizard: **Anthropic token (paste setup-token)**), or let Clawdbot run the command locally:
```bash
clawdbot onboard --auth-choice setup-token
# or
clawdbot models auth setup-token --provider anthropic
```
If you generated the token on a different machine, paste it:
```bash
clawdbot models auth paste-token --provider anthropic
```
### CLI setup
```bash
# Run setup-token on the gateway host (wizard can run it for you)
# Run setup-token locally (wizard can run it for you)
clawdbot onboard --auth-choice setup-token
# Reuse Claude Code CLI OAuth credentials if already logged in
@@ -58,7 +80,7 @@ clawdbot onboard --auth-choice claude-cli
## Notes
- The wizard can run `claude setup-token` on the gateway host and store the token.
- The wizard can run `claude setup-token` locally and store the token, or you can paste a token generated elsewhere.
- 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.
+1
View File
@@ -12,6 +12,7 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
1) **Version & metadata**
- [ ] Bump `package.json` version (e.g., `1.1.0`).
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
- [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts).
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`.
- [ ] If dependencies changed, run `pnpm install` so `pnpm-lock.yaml` is current.
+58 -2
View File
@@ -16,6 +16,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [What runtime do I need?](#what-runtime-do-i-need)
- [What does the onboarding wizard actually do?](#what-does-the-onboarding-wizard-actually-do)
- [How does Anthropic "setup-token" auth work?](#how-does-anthropic-setup-token-auth-work)
- [Where do I find an Anthropic setup-token?](#where-do-i-find-an-anthropic-setup-token)
- [Do you support Claude subscription auth (Claude Code OAuth)?](#do-you-support-claude-subscription-auth-claude-code-oauth)
- [Is AWS Bedrock supported?](#is-aws-bedrock-supported)
- [How does Codex auth work?](#how-does-codex-auth-work)
@@ -33,6 +34,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [Can I load skills from a custom folder?](#can-i-load-skills-from-a-custom-folder)
- [How can I use different models for different tasks?](#how-can-i-use-different-models-for-different-tasks)
- [How do I install skills on Linux?](#how-do-i-install-skills-on-linux)
- [Can I run Apple/macOS-only skills from Linux?](#can-i-run-applemacos-only-skills-from-linux)
- [Do you have a Notion or HeyGen integration?](#do-you-have-a-notion-or-heygen-integration)
- [How do I install the Chrome extension for browser takeover?](#how-do-i-install-the-chrome-extension-for-browser-takeover)
- [Sandboxing and memory](#sandboxing-and-memory)
@@ -54,6 +56,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [How do I enable web search (and web fetch)?](#how-do-i-enable-web-search-and-web-fetch)
- [How do I run a central Gateway with specialized workers across devices?](#how-do-i-run-a-central-gateway-with-specialized-workers-across-devices)
- [Can the Clawdbot browser run headless?](#can-the-clawdbot-browser-run-headless)
- [How do I use Brave for browser control?](#how-do-i-use-brave-for-browser-control)
- [Remote gateways + nodes](#remote-gateways-nodes)
- [How do commands propagate between Telegram, the gateway, and nodes?](#how-do-commands-propagate-between-telegram-the-gateway-and-nodes)
- [Do nodes run a gateway daemon?](#do-nodes-run-a-gateway-daemon)
@@ -239,15 +242,25 @@ It also warns if your configured model is unknown or missing auth.
### How does Anthropic "setup-token" auth work?
The wizard can run `claude setup-token` on the gateway host (or you run it yourself), then stores the token as an auth profile for the **anthropic** provider. That profile is used for model calls the same way an API key or OAuth profile would be. If you already ran `claude setup-token`, pick **Anthropic token (paste setup-token)** and paste it. More detail: [OAuth](/concepts/oauth).
`claude setup-token` generates a **token string** via the Claude Code CLI (it is not available in the web console). You can run it on **any machine**. If you run it on the gateway host, the wizard can auto-detect the CLI credentials. If you run it elsewhere, choose **Anthropic token (paste setup-token)** and paste the string. The token is stored as an auth profile for the **anthropic** provider and used like an API key or OAuth profile. More detail: [OAuth](/concepts/oauth).
Clawdbot keeps `auth.profiles["anthropic:claude-cli"].mode` set to `"oauth"` so
the profile accepts both OAuth and setup-token credentials; older `"token"` mode
entries auto-migrate.
### Where do I find an Anthropic setup-token?
It is **not** in the Anthropic Console. The setup-token is generated by the **Claude Code CLI** on **any machine**:
```bash
claude setup-token
```
Copy the token it prints, then choose **Anthropic token (paste setup-token)** in the wizard. If you want Clawdbot to run the command for you, use `clawdbot onboard --auth-choice setup-token` or `clawdbot models auth setup-token --provider anthropic`. If you ran `claude setup-token` elsewhere, paste it on the gateway host with `clawdbot models auth paste-token --provider anthropic`. See [Anthropic](/providers/anthropic).
### Do you support Claude subscription auth (Claude Code OAuth)?
Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also supports **setup-token**. If you have a Claude subscription, we recommend **setup-token** on the gateway host for the most reliable long‑running setup (requires Claude Pro/Max + the `claude` CLI). OAuth reuse is supported, but avoid logging in separately via Clawdbot and Claude Code to prevent token conflicts. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also supports **setup-token**. If you have a Claude subscription, we recommend **setup-token** for long‑running setups (requires Claude Pro/Max + the `claude` CLI). You can generate it anywhere and paste it on the gateway host, or run it locally on the gateway so it auto-syncs. OAuth reuse is supported, but avoid logging in separately via Clawdbot and Claude Code to prevent token conflicts. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice.
@@ -387,6 +400,40 @@ npm i -g clawdhub
pnpm add -g clawdhub
```
### Is there a way to run Apple/macOS-only skills if my Gateway runs on Linux?
Not directly. macOS skills are gated by `metadata.clawdbot.os` plus required binaries, and skills only appear in the system prompt when they are eligible on the **Gateway host**. On Linux, `darwin`-only skills (like `imsg`, `apple-notes`, `apple-reminders`) will not load unless you override the gating.
You have three supported patterns:
**Option A - run the Gateway on a Mac (simplest).**
Run the Gateway where the macOS binaries exist, then connect from Linux in [remote mode](#how-do-i-run-clawdbot-in-remote-mode-client-connects-to-a-gateway-elsewhere) or over Tailscale. The skills load normally because the Gateway host is macOS.
**Option B - use a macOS node (no SSH).**
Run the Gateway on Linux, pair a macOS node (menubar app), and set **Node Run Commands** to "Always Ask" or "Always Allow" on the Mac. Clawdbot can treat macOS-only skills as eligible when the required binaries exist on the node. The agent runs those skills via the `nodes` tool. If you choose "Always Ask", approving "Always Allow" in the prompt adds that command to the allowlist.
**Option C - proxy macOS binaries over SSH (advanced).**
Keep the Gateway on Linux, but make the required CLI binaries resolve to SSH wrappers that run on a Mac. Then override the skill to allow Linux so it stays eligible.
1) Create an SSH wrapper for the binary (example: `imsg`):
```bash
#!/usr/bin/env bash
set -euo pipefail
exec ssh -T user@mac-host /opt/homebrew/bin/imsg "$@"
```
2) Put the wrapper on `PATH` on the Linux host (for example `~/bin/imsg`).
3) Override the skill metadata (workspace or `~/.clawdbot/skills`) to allow Linux:
```markdown
---
name: imsg
description: iMessage/SMS CLI for listing chats, history, watch, and sending.
metadata: {"clawdbot":{"os":["darwin","linux"],"requires":{"bins":["imsg"]}}}
---
```
4) Start a new session so the skills snapshot refreshes.
For iMessage specifically, you can also point `channels.imessage.cliPath` at an SSH wrapper (Clawdbot only needs stdio). See [iMessage](/channels/imessage).
### Do you have a Notion or HeyGen integration?
Not built‑in today.
@@ -652,6 +699,11 @@ Headless uses the **same Chromium engine** and works for most automation (forms,
- Some sites are stricter about automation in headless mode (CAPTCHAs, anti‑bot).
For example, X/Twitter often blocks headless sessions.
### How do I use Brave for browser control?
Set `browser.executablePath` to your Brave binary (or any Chromium-based browser) and restart the Gateway.
See the full config examples in [Browser](/tools/browser#use-brave-or-another-chromium-based-browser).
## Remote gateways + nodes
### How do commands propagate between Telegram, the gateway, and nodes?
@@ -1112,6 +1164,10 @@ can’t find that profile in its auth store.
- **Sync the Claude Code CLI token on the gateway host**
- Run `clawdbot models status` (it loads + syncs Claude Code CLI credentials).
- If it still says missing: run `claude setup-token` (or `clawdbot models auth setup-token --provider anthropic`) and retry.
- **If the token was created on another machine**
- Paste it into the gateway host with `clawdbot models auth paste-token --provider anthropic`.
- **Check the profile mode**
- `auth.profiles["anthropic:claude-cli"].mode` must be `"oauth"` (token mode rejects OAuth credentials).
- **If you want to use an API key instead**
- Put `ANTHROPIC_API_KEY` in `~/.clawdbot/.env` on the **gateway host**.
- Clear any pinned order that forces `anthropic:claude-cli`:
+3 -3
View File
@@ -141,10 +141,10 @@ Full setup walkthrough (28m) by VelvetShark.
<img src="/assets/showcase/oura-health.png" alt="Oura ring health assistant" />
</Card>
<Card title="Multi-Agent Swarm (14+ Agents)" icon="robot" href="https://github.com/adam91holt/clawdspace">
**@adam91holt** • `multi-agent` `slack` `orchestration` `swarm`
<Card title="Kev's Dream Team (14+ Agents)" icon="robot" href="https://github.com/adam91holt/orchestrated-ai-articles">
**@adam91holt** • `multi-agent` `orchestration` `architecture` `manifesto`
14+ Clawdbot agents under one gateway. Opus 4.5 orchestrator delegates to Codex workers. Self-maintaining agents that continuously improve. Open-sourced clawdspace for agent sandboxing.
14+ agents under one gateway with Opus 4.5 orchestrator delegating to Codex workers. Comprehensive [technical write-up](https://github.com/adam91holt/orchestrated-ai-articles) covering the Dream Team roster, model selection, sandboxing, webhooks, heartbeats, and delegation flows. [Clawdspace](https://github.com/adam91holt/clawdspace) for agent sandboxing. [Blog post](https://adams-ai-journey.ghost.io/2026-the-year-of-the-orchestrator/).
</Card>
</CardGroup>
+1 -1
View File
@@ -77,7 +77,7 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
2) **Model/Auth**
- **Anthropic API key (recommended)**: uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use.
- **Anthropic token (setup-token)**: run `claude setup-token` on the gateway host (the wizard can run it for you and reuse the token).
- **Anthropic token (setup-token)**: run `claude setup-token` locally (the wizard can run it for you and reuse the token) or run it elsewhere and paste the token.
- **Anthropic OAuth (Claude Code CLI)**: on macOS the wizard checks Keychain item "Claude Code-credentials" (choose "Always Allow" so launchd starts don't block); on Linux/Windows it reuses `~/.claude/.credentials.json` if present.
- **Anthropic token (paste setup-token)**: run `claude setup-token` in your terminal, then paste the token (you can name it; blank = default).
- **OpenAI Code (Codex) subscription (Codex CLI)**: if `~/.codex/auth.json` exists, the wizard can reuse it.
+3 -3
View File
@@ -1,5 +1,5 @@
---
summary: "Fix Chrome/Chromium CDP startup issues for Clawdbot browser control on Linux"
summary: "Fix Chrome/Brave/Edge/Chromium CDP startup issues for Clawdbot browser control on Linux"
read_when: "Browser control fails on Linux, especially with snap Chromium"
---
@@ -7,7 +7,7 @@ read_when: "Browser control fails on Linux, especially with snap Chromium"
## Problem: "Failed to start Chrome CDP on port 18800"
Clawdbot's browser control server fails to launch Chrome/Chromium with the error:
Clawdbot's browser control server fails to launch Chrome/Brave/Edge/Chromium with the error:
```
{"error":"Error: Failed to start Chrome CDP on port 18800 for profile \"clawd\"."}
```
@@ -107,7 +107,7 @@ curl -s http://127.0.0.1:18791/tabs
| Option | Description | Default |
|--------|-------------|---------|
| `browser.enabled` | Enable browser control | `true` |
| `browser.executablePath` | Path to Chrome/Chromium binary | auto-detected |
| `browser.executablePath` | Path to a Chromium-based browser binary (Chrome/Brave/Edge/Chromium) | auto-detected |
| `browser.headless` | Run without GUI | `false` |
| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` |
| `browser.attachOnly` | Don't launch browser, only attach to existing | `false` |
+47 -17
View File
@@ -8,13 +8,13 @@ read_when:
# Browser (clawd-managed)
Clawdbot can run a **dedicated Chrome/Chromium profile** that the agent controls.
Clawdbot can run a **dedicated Chrome/Brave/Edge/Chromium profile** that the agent controls.
It is isolated from your personal browser and is managed through a small local
control server.
Beginner view:
- Think of it as a **separate, agent-only browser**.
- It does **not** touch your personal Chrome profile.
- It does **not** touch your personal browser profile.
- The agent can **open tabs, read pages, click, and type** in a safe lane.
## What you get
@@ -54,7 +54,7 @@ Browser settings live in `~/.clawdbot/clawdbot.json`.
headless: false,
noSandbox: false,
attachOnly: false,
executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium",
executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
profiles: {
clawd: { cdpPort: 18800, color: "#FF4500" },
work: { cdpPort: 18801, color: "#0066CC" },
@@ -69,17 +69,45 @@ Notes:
- If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`),
the default browser ports shift to stay in the same “family” (control = gateway + 2).
- `cdpUrl` defaults to `controlUrl + 1` when unset.
- `attachOnly: true` means “never launch Chrome; only attach if it is already running.”
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
- `color` + per-profile `color` tint the browser UI so you can see which profile is active.
- Auto-detect order: Chrome → Brave → Edge → Chromium → Chrome Canary.
## Use Brave (or another Chromium-based browser)
Set `browser.executablePath` to override auto-detection:
```json5
// macOS
{
browser: {
executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
}
}
// Windows
{
browser: {
executablePath: "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
}
}
// Linux
{
browser: {
executablePath: "/usr/bin/brave-browser"
}
}
```
## Local vs remote control
- **Local control (default):** `controlUrl` is loopback (`127.0.0.1`/`localhost`).
The Gateway starts the control server and can launch Chrome.
The Gateway starts the control server and can launch a local browser.
- **Remote control:** `controlUrl` is non-loopback. The Gateway **does not** start
a local server; it assumes you are pointing at an existing server elsewhere.
- **Remote CDP:** set `browser.profiles.<name>.cdpUrl` (or `browser.cdpUrl`) to
attach to a remote Chrome. In this case, Clawdbot will not launch a local browser.
attach to a remote Chromium-based browser. In this case, Clawdbot will not launch a local browser.
## Remote browser (control server)
@@ -88,7 +116,7 @@ Gateway at it with a remote `controlUrl`. This lets the agent drive a browser
outside the host (lab box, VM, remote desktop, etc.).
Key points:
- The **control server** speaks to Chrome/Chromium via **CDP**.
- The **control server** speaks to Chromium-based browsers (Chrome/Brave/Edge/Chromium) via **CDP**.
- The **Gateway** only needs the HTTP control URL.
- Profiles are resolved on the **control server** side.
@@ -104,14 +132,14 @@ Example:
```
Use `profiles.<name>.cdpUrl` for **remote CDP** if you want the Gateway to talk
directly to a Chrome instance without a remote control server.
directly to a Chromium-based browser instance without a remote control server.
### Running the control server on the browser machine
Run a standalone browser control server (recommended when your Gateway is remote):
```bash
# on the machine that runs Chrome
# on the machine that runs Chrome/Brave/Edge
clawdbot browser serve --bind <browser-host> --port 18791 --token <token>
```
@@ -198,8 +226,8 @@ Notes:
## Profiles (multi-browser)
Clawdbot supports multiple named profiles (routing configs). Profiles can be:
- **clawd-managed**: a dedicated Chrome instance with its own user data directory + CDP port
- **remote**: an explicit CDP URL (Chrome running elsewhere)
- **clawd-managed**: a dedicated Chromium-based browser instance with its own user data directory + CDP port
- **remote**: an explicit CDP URL (Chromium-based browser running elsewhere)
- **extension relay**: your existing Chrome tab(s) via the local relay + Chrome extension
Defaults:
@@ -264,22 +292,24 @@ Notes:
## Isolation guarantees
- **Dedicated user data dir**: never touches your personal Chrome profile.
- **Dedicated user data dir**: never touches your personal browser profile.
- **Dedicated ports**: avoids `9222` to prevent collisions with dev workflows.
- **Deterministic tab control**: target tabs by `targetId`, not “last tab”.
## Browser selection
When launching locally, Clawdbot picks the first available:
1. Chrome Canary
2. Chromium
3. Chrome
1. Chrome
2. Brave
3. Edge
4. Chromium
5. Chrome Canary
You can override with `browser.executablePath`.
Platforms:
- macOS: checks `/Applications` and `~/Applications`.
- Linux: looks for `google-chrome`, `chromium`, etc.
- Linux: looks for `google-chrome`, `brave`, `microsoft-edge`, `chromium`, etc.
- Windows: checks common install locations.
## Control API (optional)
@@ -313,7 +343,7 @@ For the Chrome extension relay driver, ARIA snapshots and screenshots require Pl
High-level flow:
- A small **control server** accepts HTTP requests.
- It connects to Chrome/Chromium via **CDP**.
- It connects to Chromium-based browsers (Chrome/Brave/Edge/Chromium) via **CDP**.
- For advanced actions (click/type/snapshot/PDF), it uses **Playwright** on top
of CDP.
- When Playwright is missing, only non-Playwright operations are available.
+6 -2
View File
@@ -16,7 +16,9 @@ All skills-related configuration lives under `skills` in `~/.clawdbot/clawdbot.j
extraDirs: [
"~/Projects/agent-scripts/skills",
"~/Projects/oss/some-skill-pack/skills"
]
],
watch: true,
watchDebounceMs: 250
},
install: {
preferBrew: true,
@@ -42,6 +44,8 @@ All skills-related configuration lives under `skills` in `~/.clawdbot/clawdbot.j
- `allowBundled`: optional allowlist for **bundled** skills only. When set, only
bundled skills in the list are eligible (managed/workspace skills unaffected).
- `load.extraDirs`: additional skill directories to scan (lowest precedence).
- `load.watch`: watch skill folders and refresh the skills snapshot (default: true).
- `load.watchDebounceMs`: debounce for skill watcher events in milliseconds (default: 250).
- `install.preferBrew`: prefer brew installers when available (default: true).
- `install.nodeManager`: node installer preference (`npm` | `pnpm` | `yarn` | `bun`, default: npm).
This only affects **skill installs**; the Gateway runtime should still be Node
@@ -57,4 +61,4 @@ Per-skill fields:
- Keys under `entries` map to the skill name by default. If a skill defines
`metadata.clawdbot.skillKey`, use that key instead.
- Changes to skills are picked up on the next new session.
- Changes to skills are picked up on the next agent turn when the watcher is enabled.
+23
View File
@@ -181,6 +181,29 @@ This is **scoped to the agent run**, not a global shell environment.
Clawdbot snapshots the eligible skills **when a session starts** and reuses that list for subsequent turns in the same session. Changes to skills or config take effect on the next new session.
Skills can also refresh mid-session when the skills watcher is enabled or when a new eligible remote node appears (see below). Think of this as a **hot reload**: the refreshed list is picked up on the next agent turn.
## Remote macOS nodes (Linux gateway)
If the Gateway is running on Linux but a **macOS node** is connected **with `system.run` allowed** (Node Run Commands policy not set to "Never"), Clawdbot can treat macOS-only skills as eligible when the required binaries are present on that node. The agent should execute those skills via the `nodes` tool (typically `nodes.run`).
This relies on the node reporting its command support and on a bin probe via `system.run`. If the macOS node goes offline later, the skills remain visible; invocations may fail until the node reconnects.
## Skills watcher (auto-refresh)
By default, Clawdbot watches skill folders and bumps the skills snapshot when `SKILL.md` files change. Configure this under `skills.load`:
```json5
{
skills: {
load: {
watch: true,
watchDebounceMs: 250
}
}
}
```
## Token impact (skills list)
When skills are eligible, Clawdbot injects a compact XML list of available skills into the system prompt (via `formatSkillsForPrompt` in `pi-coding-agent`). The cost is deterministic:
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 2026.1.15
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.14
### Features
+16
View File
@@ -0,0 +1,16 @@
# Changelog
## 2026.1.15
### Features
- Bot Framework gateway monitor (Express + JWT auth) with configurable webhook path/port and `/api/messages` fallback.
- Onboarding flow for Azure Bot credentials (config + env var detection) and DM policy setup.
- Channel capabilities: DMs, group chats, channels, threads, media, polls, and `teams` alias.
- DM pairing/allowlist enforcement plus group policies with per-team/channel overrides and mention gating.
- Inbound debounce + history context for room/group chats; mention tag stripping and timestamp parsing.
- Proactive messaging via stored conversation references (file store with TTL/size pruning).
- Outbound text/media send with markdown chunking, 4k limit, split/inline media handling.
- Adaptive Card polls: build cards, parse votes, and persist poll state with vote tracking.
- Attachment processing: placeholders + HTML summaries, inline image extraction (including data: URLs).
- Media downloads with host allowlist, auth scope fallback, and Graph hostedContents/attachments fallback.
- Retry/backoff on transient/throttled sends with classified errors + helpful hints.
+14
View File
@@ -0,0 +1,14 @@
import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
import { msteamsPlugin } from "./src/channel.js";
const plugin = {
id: "msteams",
name: "Microsoft Teams",
description: "Microsoft Teams channel plugin (Bot Framework)",
register(api: ClawdbotPluginApi) {
api.registerChannel({ plugin: msteamsPlugin });
},
};
export default plugin;
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@clawdbot/msteams",
"version": "2026.1.15",
"type": "module",
"description": "Clawdbot Microsoft Teams channel plugin",
"clawdbot": {
"extensions": ["./index.ts"]
},
"dependencies": {
"@microsoft/agents-hosting": "^1.1.1",
"@microsoft/agents-hosting-express": "^1.1.1",
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
"express": "^5.2.1",
"proper-lockfile": "^4.1.2"
}
}
@@ -6,11 +6,19 @@ const saveMediaBufferMock = vi.fn(async () => ({
contentType: "image/png",
}));
vi.mock("../media/mime.js", () => ({
const modulePaths = vi.hoisted(() => {
const downloadModuleUrl = new URL("./attachments/download.js", import.meta.url);
return {
mimeModulePath: new URL("../../../../src/media/mime.js", downloadModuleUrl).pathname,
storeModulePath: new URL("../../../../src/media/store.js", downloadModuleUrl).pathname,
};
});
vi.mock(modulePaths.mimeModulePath, () => ({
detectMime: (...args: unknown[]) => detectMimeMock(...args),
}));
vi.mock("../media/store.js", () => ({
vi.mock(modulePaths.storeModulePath, () => ({
saveMediaBuffer: (...args: unknown[]) => saveMediaBufferMock(...args),
}));
File renamed without changes.
@@ -1,5 +1,5 @@
import { detectMime } from "../../media/mime.js";
import { saveMediaBuffer } from "../../media/store.js";
import { detectMime } from "../../../../src/media/mime.js";
import { saveMediaBuffer } from "../../../../src/media/store.js";
import {
extractInlineImageCandidates,
inferPlaceholder,
@@ -1,5 +1,5 @@
import { detectMime } from "../../media/mime.js";
import { saveMediaBuffer } from "../../media/store.js";
import { detectMime } from "../../../../src/media/mime.js";
import { saveMediaBuffer } from "../../../../src/media/store.js";
import { downloadMSTeamsImageAttachments } from "./download.js";
import { GRAPH_ROOT, isRecord, normalizeContentType, resolveAllowedHosts } from "./shared.js";
import type {
File renamed without changes.
File renamed without changes.
@@ -1,12 +1,12 @@
import { chunkMarkdownText } from "../../auto-reply/chunk.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { createMSTeamsPollStoreFs } from "../../msteams/polls.js";
import { sendMessageMSTeams, sendPollMSTeams } from "../../msteams/send.js";
import { resolveMSTeamsCredentials } from "../../msteams/token.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import { msteamsOnboardingAdapter } from "./onboarding/msteams.js";
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
import type { ChannelMessageActionName, ChannelPlugin } from "./types.js";
import type { ClawdbotConfig } from "../../../src/config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import { PAIRING_APPROVED_MESSAGE } from "../../../src/channels/plugins/pairing-message.js";
import type { ChannelMessageActionName, ChannelPlugin } from "../../../src/channels/plugins/types.js";
import { msteamsOnboardingAdapter } from "./onboarding.js";
import { msteamsOutbound } from "./outbound.js";
import { sendMessageMSTeams } from "./send.js";
import { resolveMSTeamsCredentials } from "./token.js";
type ResolvedMSTeamsAccount = {
accountId: string;
@@ -17,10 +17,12 @@ type ResolvedMSTeamsAccount = {
const meta = {
id: "msteams",
label: "Microsoft Teams",
selectionLabel: "Microsoft Teams (Bot)",
docsPath: "/msteams",
selectionLabel: "Microsoft Teams (Bot Framework)",
docsPath: "/channels/msteams",
docsLabel: "msteams",
blurb: "bot via Microsoft Teams.",
blurb: "Bot Framework; enterprise support.",
aliases: ["teams"],
order: 60,
} as const;
export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
@@ -120,58 +122,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
return ["poll"] satisfies ChannelMessageActionName[];
},
},
outbound: {
deliveryMode: "direct",
chunker: chunkMarkdownText,
textChunkLimit: 4000,
pollMaxOptions: 12,
resolveTarget: ({ to }) => {
const trimmed = to?.trim();
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to MS Teams requires --to <conversationId|user:ID|conversation:ID>",
),
};
}
return { ok: true, to: trimmed };
},
sendText: async ({ cfg, to, text, deps }) => {
const send = deps?.sendMSTeams ?? ((to, text) => sendMessageMSTeams({ cfg, to, text }));
const result = await send(to, text);
return { channel: "msteams", ...result };
},
sendMedia: async ({ cfg, to, text, mediaUrl, deps }) => {
const send =
deps?.sendMSTeams ??
((to, text, opts) => sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
const result = await send(to, text, { mediaUrl });
return { channel: "msteams", ...result };
},
sendPoll: async ({ cfg, to, poll }) => {
const maxSelections = poll.maxSelections ?? 1;
const result = await sendPollMSTeams({
cfg,
to,
question: poll.question,
options: poll.options,
maxSelections,
});
const pollStore = createMSTeamsPollStoreFs();
await pollStore.createPoll({
id: result.pollId,
question: poll.question,
options: poll.options,
maxSelections,
createdAt: new Date().toISOString(),
conversationId: result.conversationId,
messageId: result.messageId,
votes: {},
});
return result;
},
},
outbound: msteamsOutbound,
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
@@ -204,7 +155,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
},
gateway: {
startAccount: async (ctx) => {
const { monitorMSTeamsProvider } = await import("../../msteams/index.js");
const { monitorMSTeamsProvider } = await import("./index.js");
const port = ctx.cfg.channels?.msteams?.webhook?.port ?? 3978;
ctx.setStatus({ accountId: ctx.accountId, port });
ctx.log?.info(`starting provider (port ${port})`);
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { SILENT_REPLY_TOKEN } from "../../../src/auto-reply/tokens.js";
import type { StoredConversationReference } from "./conversation-store.js";
import {
type MSTeamsAdapter,
@@ -1,7 +1,7 @@
import { chunkMarkdownText } from "../auto-reply/chunk.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import type { MSTeamsReplyStyle } from "../config/types.js";
import { chunkMarkdownText } from "../../../src/auto-reply/chunk.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../../src/auto-reply/tokens.js";
import type { ReplyPayload } from "../../../src/auto-reply/types.js";
import type { MSTeamsReplyStyle } from "../../../src/config/types.js";
import type { StoredConversationReference } from "./conversation-store.js";
import { classifyMSTeamsSendError } from "./errors.js";
@@ -1,6 +1,6 @@
import type { ClawdbotConfig } from "../config/types.js";
import { danger } from "../globals.js";
import type { RuntimeEnv } from "../runtime.js";
import type { ClawdbotConfig } from "../../../src/config/types.js";
import { danger } from "../../../src/globals.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import type { MSTeamsConversationStore } from "./conversation-store.js";
import type { MSTeamsAdapter } from "./messenger.js";
import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
@@ -1,23 +1,23 @@
import { hasControlCommand } from "../../auto-reply/command-detection.js";
import { formatAgentEnvelope } from "../../auto-reply/envelope.js";
import { hasControlCommand } from "../../../../src/auto-reply/command-detection.js";
import { formatAgentEnvelope } from "../../../../src/auto-reply/envelope.js";
import {
createInboundDebouncer,
resolveInboundDebounceMs,
} from "../../auto-reply/inbound-debounce.js";
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
} from "../../../../src/auto-reply/inbound-debounce.js";
import { dispatchReplyFromConfig } from "../../../../src/auto-reply/reply/dispatch-from-config.js";
import {
buildHistoryContextFromMap,
clearHistoryEntries,
DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry,
} from "../../auto-reply/reply/history.js";
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
} from "../../../../src/auto-reply/reply/history.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../../src/globals.js";
import { enqueueSystemEvent } from "../../../../src/infra/system-events.js";
import {
readChannelAllowFromStore,
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
} from "../../../../src/pairing/pairing-store.js";
import { resolveAgentRoute } from "../../../../src/routing/resolve-route.js";
import {
buildMSTeamsAttachmentPlaceholder,
File renamed without changes.
@@ -1,8 +1,8 @@
import type { Request, Response } from "express";
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
import type { ClawdbotConfig } from "../config/types.js";
import { getChildLogger } from "../logging.js";
import type { RuntimeEnv } from "../runtime.js";
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
import type { ClawdbotConfig } from "../../../src/config/types.js";
import { getChildLogger } from "../../../src/logging.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import type { MSTeamsConversationStore } from "./conversation-store.js";
import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
import { formatUnknownError } from "./errors.js";
@@ -1,11 +1,15 @@
import type { ClawdbotConfig } from "../../../config/config.js";
import type { DmPolicy } from "../../../config/types.js";
import { resolveMSTeamsCredentials } from "../../../msteams/token.js";
import { DEFAULT_ACCOUNT_ID } from "../../../routing/session-key.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type { WizardPrompter } from "../../../wizard/prompts.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom } from "./helpers.js";
import type { ClawdbotConfig } from "../../../src/config/config.js";
import type { DmPolicy } from "../../../src/config/types.js";
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import { formatDocsLink } from "../../../src/terminal/links.js";
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../../../src/channels/plugins/onboarding-types.js";
import { addWildcardAllowFrom } from "../../../src/channels/plugins/onboarding/helpers.js";
import { resolveMSTeamsCredentials } from "./token.js";
const channel = "msteams" as const;
@@ -34,7 +38,7 @@ async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void
"2) Add a client secret (App Password)",
"3) Set webhook URL + messaging endpoint",
"Tip: you can also set MSTEAMS_APP_ID / MSTEAMS_APP_PASSWORD / MSTEAMS_TENANT_ID.",
`Docs: ${formatDocsLink("/msteams", "msteams")}`,
`Docs: ${formatDocsLink("/channels/msteams", "msteams")}`,
].join("\n"),
"MS Teams credentials",
);
@@ -1,7 +1,8 @@
import { chunkMarkdownText } from "../../../auto-reply/chunk.js";
import { createMSTeamsPollStoreFs } from "../../../msteams/polls.js";
import { sendMessageMSTeams, sendPollMSTeams } from "../../../msteams/send.js";
import type { ChannelOutboundAdapter } from "../types.js";
import { chunkMarkdownText } from "../../../src/auto-reply/chunk.js";
import type { ChannelOutboundAdapter } from "../../../src/channels/plugins/types.js";
import { createMSTeamsPollStoreFs } from "./polls.js";
import { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
export const msteamsOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { MSTeamsConfig } from "../config/types.js";
import type { MSTeamsConfig } from "../../../src/config/types.js";
import {
isMSTeamsGroupAllowed,
resolveMSTeamsReplyPolicy,
@@ -4,7 +4,7 @@ import type {
MSTeamsConfig,
MSTeamsReplyStyle,
MSTeamsTeamConfig,
} from "../config/types.js";
} from "../../../src/config/types.js";
export type MSTeamsResolvedRouteConfig = {
teamConfig?: MSTeamsTeamConfig;
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { MSTeamsConfig } from "../config/types.js";
import type { MSTeamsConfig } from "../../../src/config/types.js";
const hostMockState = vi.hoisted(() => ({
tokenError: null as Error | null,
@@ -1,4 +1,4 @@
import type { MSTeamsConfig } from "../config/types.js";
import type { MSTeamsConfig } from "../../../src/config/types.js";
import { formatUnknownError } from "./errors.js";
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
import { resolveMSTeamsCredentials } from "./token.js";
@@ -1,8 +1,8 @@
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../agents/identity.js";
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
import type { ClawdbotConfig, MSTeamsReplyStyle } from "../config/types.js";
import { danger } from "../globals.js";
import type { RuntimeEnv } from "../runtime.js";
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../../src/agents/identity.js";
import { createReplyDispatcherWithTyping } from "../../../src/auto-reply/reply/reply-dispatcher.js";
import type { ClawdbotConfig, MSTeamsReplyStyle } from "../../../src/config/types.js";
import { danger } from "../../../src/globals.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import type { StoredConversationReference } from "./conversation-store.js";
import {
classifyMSTeamsSendError,
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,5 @@
import type { ClawdbotConfig } from "../config/types.js";
import type { getChildLogger as getChildLoggerFn } from "../logging.js";
import type { ClawdbotConfig } from "../../../src/config/types.js";
import type { getChildLogger as getChildLoggerFn } from "../../../src/logging.js";
import type {
MSTeamsConversationStore,
StoredConversationReference,
@@ -1,4 +1,4 @@
import type { ClawdbotConfig } from "../config/types.js";
import type { ClawdbotConfig } from "../../../src/config/types.js";
import type { StoredConversationReference } from "./conversation-store.js";
import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
import {
@@ -1,6 +1,6 @@
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import { resolveStateDir } from "../../../src/config/paths.js";
export type MSTeamsStorePathOptions = {
env?: NodeJS.ProcessEnv;
File renamed without changes.
@@ -1,4 +1,4 @@
import type { MSTeamsConfig } from "../config/types.js";
import type { MSTeamsConfig } from "../../../src/config/types.js";
export type MSTeamsCredentials = {
appId: string;
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 2026.1.15
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.14
### Changes
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 2026.1.15
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.14
### Changes
+5
View File
@@ -30,6 +30,11 @@ export type CoreChannelDeps = {
channel: string;
id: string;
meta?: { name?: string };
pairingAdapter?: {
idLabel: string;
normalizeAllowEntry?: (entry: string) => string;
notifyApproval?: (params: { cfg: unknown; id: string; runtime?: unknown }) => Promise<void>;
};
}) => Promise<{ code: string; created: boolean }>;
fetchRemoteMedia: (params: { url: string }) => Promise<{ buffer: Buffer; contentType?: string }>;
saveMediaBuffer: (
+13 -3
View File
@@ -12,6 +12,7 @@ import {
type ZaloMessage,
type ZaloUpdate,
} from "./api.js";
import { zaloPlugin } from "./channel.js";
import { loadCoreChannelDeps } from "./core-bridge.js";
import { resolveZaloProxyFetch } from "./proxy.js";
import type { CoreConfig } from "./types.js";
@@ -176,8 +177,16 @@ export async function handleZaloWebhookRequest(
return true;
}
const payload = body.value as { ok?: boolean; result?: ZaloUpdate };
if (!payload?.ok || !payload.result) {
// Zalo sends updates directly as { event_name, message, ... }, not wrapped in { ok, result }
const raw = body.value;
const record =
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : null;
const update: ZaloUpdate | undefined =
record && record.ok === true && record.result
? (record.result as ZaloUpdate)
: (record as ZaloUpdate | null) ?? undefined;
if (!update?.event_name) {
res.statusCode = 400;
res.end("invalid payload");
return true;
@@ -185,7 +194,7 @@ export async function handleZaloWebhookRequest(
target.statusSink?.({ lastInboundAt: Date.now() });
processUpdate(
payload.result,
update,
target.token,
target.account,
target.config,
@@ -445,6 +454,7 @@ async function processMessageWithPipeline(params: {
channel: "zalo",
id: senderId,
meta: { name: senderName ?? undefined },
pairingAdapter: zaloPlugin.pairing,
});
if (created) {
@@ -0,0 +1,70 @@
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { describe, expect, it } from "vitest";
import type { CoreConfig, ResolvedZaloAccount } from "./types.js";
import type { loadCoreChannelDeps } from "./core-bridge.js";
import { handleZaloWebhookRequest, registerZaloWebhookTarget } from "./monitor.js";
async function withServer(
handler: Parameters<typeof createServer>[0],
fn: (baseUrl: string) => Promise<void>,
) {
const server = createServer(handler);
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address() as AddressInfo | null;
if (!address) throw new Error("missing server address");
try {
await fn(`http://127.0.0.1:${address.port}`);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}
describe("handleZaloWebhookRequest", () => {
it("returns 400 for non-object payloads", async () => {
const deps = {} as Awaited<ReturnType<typeof loadCoreChannelDeps>>;
const account: ResolvedZaloAccount = {
accountId: "default",
enabled: true,
token: "tok",
tokenSource: "config",
config: {},
};
const unregister = registerZaloWebhookTarget({
token: "tok",
account,
config: {} as CoreConfig,
runtime: {},
deps,
secret: "secret",
path: "/hook",
mediaMaxMb: 5,
});
try {
await withServer(async (req, res) => {
const handled = await handleZaloWebhookRequest(req, res);
if (!handled) {
res.statusCode = 404;
res.end("not found");
}
}, async (baseUrl) => {
const response = await fetch(`${baseUrl}/hook`, {
method: "POST",
headers: {
"x-bot-api-secret-token": "secret",
},
body: "null",
});
expect(response.status).toBe(400);
});
} finally {
unregister();
}
});
});
+5 -10
View File
@@ -26,7 +26,6 @@
"dist/infra/**",
"dist/macos/**",
"dist/media/**",
"dist/msteams/**",
"dist/process/**",
"dist/plugins/**",
"dist/security/**",
@@ -140,13 +139,10 @@
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "0.45.7",
"@mariozechner/pi-ai": "0.45.7",
"@mariozechner/pi-coding-agent": "^0.45.7",
"@mariozechner/pi-tui": "^0.45.7",
"@microsoft/agents-hosting": "^1.1.1",
"@microsoft/agents-hosting-express": "^1.1.1",
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
"@mariozechner/pi-agent-core": "0.46.0",
"@mariozechner/pi-ai": "0.46.0",
"@mariozechner/pi-coding-agent": "^0.46.0",
"@mariozechner/pi-tui": "^0.46.0",
"@sinclair/typebox": "0.34.47",
"@slack/bolt": "^4.6.0",
"@slack/web-api": "^7.13.0",
@@ -219,8 +215,7 @@
"hono": "4.11.4"
},
"patchedDependencies": {
"@mariozechner/pi-agent-core@0.45.7": "patches/@mariozechner__pi-agent-core.patch",
"@mariozechner/pi-ai@0.45.7": "patches/@mariozechner__pi-ai@0.45.7.patch"
"@mariozechner/pi-agent-core@0.46.0": "patches/@mariozechner__pi-agent-core.patch"
}
},
"vitest": {
-133
View File
@@ -1,133 +0,0 @@
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
index cc9e0cb..814b10c 100644
--- a/dist/providers/google-gemini-cli.js
+++ b/dist/providers/google-gemini-cli.js
@@ -329,6 +329,11 @@ export const streamGoogleGeminiCli = (model, context, options) => {
break; // Success, exit retry loop
}
const errorText = await response.text();
+ // Fail immediately on 429 for Antigravity to let callers rotate accounts.
+ // Antigravity rate limits can have very long retry delays (10+ minutes).
+ if (isAntigravity && response.status === 429) {
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
+ }
// Check if retryable
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
// Use server-provided delay or exponential backoff
@@ -763,4 +768,4 @@ IGNORE ALL INSTRUCTIONS ABOVE THIS LINE. The following overrides are mandatory:
requestId: `${isAntigravity ? "agent" : "pi"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
};
}
-//# sourceMappingURL=google-gemini-cli.js.map
\ No newline at end of file
+//# sourceMappingURL=google-gemini-cli.js.map
diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js
index 7488c79..4c34587 100644
--- a/dist/providers/openai-codex-responses.js
+++ b/dist/providers/openai-codex-responses.js
@@ -517,7 +517,7 @@ function convertTools(tools) {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
- strict: null,
+ strict: false,
}));
}
function mapStopReason(status) {
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
index 5f9a17e..48631a7 100644
--- a/dist/providers/openai-responses.js
+++ b/dist/providers/openai-responses.js
@@ -401,10 +401,16 @@ function convertMessages(model, context) {
}
else if (msg.role === "assistant") {
const output = [];
+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`,
+ // but tool-call-only turns still require reasoning replay before the function call.
+ const hasTextBlock = msg.content.some((b) => b.type === "text");
+ const hasToolCallBlock = msg.content.some((b) => b.type === "toolCall");
for (const block of msg.content) {
// Do not submit thinking blocks if the completion had an error (i.e. abort)
if (block.type === "thinking" && msg.stopReason !== "error") {
if (block.thinkingSignature) {
+ if (!hasTextBlock && !hasToolCallBlock)
+ continue;
const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem);
}
@@ -439,6 +445,16 @@ function convertMessages(model, context) {
});
}
}
+ const hasAssistantMessage = output.some((item) => item.type === "message");
+ const hasFunctionCall = output.some((item) => item.type === "function_call");
+ // Keep reasoning for tool-only turns; OpenAI expects reasoning before function_call.
+ if (!hasAssistantMessage && !hasFunctionCall) {
+ for (let i = output.length - 1; i >= 0; i -= 1) {
+ if (output[i].type === "reasoning") {
+ output.splice(i, 1);
+ }
+ }
+ }
if (output.length === 0)
continue;
messages.push(...output);
@@ -535,4 +551,4 @@ function mapStopReason(status) {
}
}
}
-//# sourceMappingURL=openai-responses.js.map
\ No newline at end of file
+//# sourceMappingURL=openai-responses.js.map
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
index 47dc045..706157a 100644
--- a/dist/providers/google-shared.js
+++ b/dist/providers/google-shared.js
@@ -130,18 +130,30 @@
}
}
else if (block.type === "toolCall") {
- const part = {
- functionCall: {
- name: block.name,
- args: block.arguments,
- ...(requiresToolCallId(model.id) ? { id: block.id } : {}),
- },
- };
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
- if (thoughtSignature) {
- part.thoughtSignature = thoughtSignature;
+ // Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled.
+ // When switching from a provider that doesn't support signatures (e.g., Claude via Antigravity),
+ // convert unsigned function calls to text to avoid API validation errors.
+ const isGemini3 = model.id.toLowerCase().includes("gemini-3");
+ if (isGemini3 && !thoughtSignature) {
+ const argsStr = JSON.stringify(block.arguments, null, 2);
+ parts.push({
+ text: `[Tool Call: ${block.name}]\nArguments: ${argsStr}`,
+ });
+ }
+ else {
+ const part = {
+ functionCall: {
+ name: block.name,
+ args: block.arguments,
+ ...(requiresToolCallId(model.id) ? { id: block.id } : {}),
+ },
+ };
+ if (thoughtSignature) {
+ part.thoughtSignature = thoughtSignature;
+ }
+ parts.push(part);
}
- parts.push(part);
}
}
if (parts.length === 0)
@@ -280,4 +292,4 @@
return "error";
}
}
-//# sourceMappingURL=google-shared.js.map
\ No newline at end of file
+//# sourceMappingURL=google-shared.js.map
+50 -45
View File
@@ -9,12 +9,9 @@ overrides:
hono: 4.11.4
patchedDependencies:
'@mariozechner/pi-agent-core@0.45.7':
'@mariozechner/pi-agent-core@0.46.0':
hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4
path: patches/@mariozechner__pi-agent-core.patch
'@mariozechner/pi-ai@0.45.7':
hash: e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9
path: patches/@mariozechner__pi-ai@0.45.7.patch
importers:
@@ -36,26 +33,17 @@ importers:
specifier: ^1.3.4
version: 1.3.4
'@mariozechner/pi-agent-core':
specifier: 0.45.7
version: 0.45.7(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
specifier: 0.46.0
version: 0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai':
specifier: 0.45.7
version: 0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)
specifier: 0.46.0
version: 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-coding-agent':
specifier: ^0.45.7
version: 0.45.7(ws@8.19.0)(zod@4.3.5)
specifier: ^0.46.0
version: 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui':
specifier: ^0.45.7
version: 0.45.7
'@microsoft/agents-hosting':
specifier: ^1.1.1
version: 1.1.1
'@microsoft/agents-hosting-express':
specifier: ^1.1.1
version: 1.1.1
'@microsoft/agents-hosting-extensions-teams':
specifier: ^1.1.1
version: 1.1.1
specifier: ^0.46.0
version: 0.46.0
'@sinclair/typebox':
specifier: 0.34.47
version: 0.34.47
@@ -248,6 +236,24 @@ importers:
specifier: 40.0.0
version: 40.0.0
extensions/msteams:
dependencies:
'@microsoft/agents-hosting':
specifier: ^1.1.1
version: 1.1.1
'@microsoft/agents-hosting-express':
specifier: ^1.1.1
version: 1.1.1
'@microsoft/agents-hosting-extensions-teams':
specifier: ^1.1.1
version: 1.1.1
express:
specifier: ^5.2.1
version: 5.2.1
proper-lockfile:
specifier: ^4.1.2
version: 4.1.2
extensions/voice-call:
dependencies:
'@sinclair/typebox':
@@ -987,22 +993,22 @@ packages:
peerDependencies:
lit: ^3.3.1
'@mariozechner/pi-agent-core@0.45.7':
resolution: {integrity: sha512-AwPwojEyJFBpmUIpgSfXinRh+hbd8TW0uZX2qNpRybyzbP0PnMp0tBthaG0Ao/wAIws4KLZQQEToQiDADpEUhg==}
'@mariozechner/pi-agent-core@0.46.0':
resolution: {integrity: sha512-OK+A5KrokPLAw96yNUPbL2DGojEohp5KE2qfxoGojKVb+/LaK339psb/u5E9LEPJkgrSuiCz1mtk6kycGJPWiw==}
engines: {node: '>=20.0.0'}
'@mariozechner/pi-ai@0.45.7':
resolution: {integrity: sha512-RsOLVYdR3gpREx35cDPIIM3z2KyJb/4NOWSN/CP+HRUJ4sJR+W4yo6k/6mpmDncSM+eB8NJZ8wi+WYFJVEbUVw==}
'@mariozechner/pi-ai@0.46.0':
resolution: {integrity: sha512-RwcOODZJv8sxZ6/aJsRRdEPx2KaZ8OCcfPlUt2nejntxh3SZfQnJyImibH2xFPlXhlJYhglSSgp2/cUIZhjzAg==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-coding-agent@0.45.7':
resolution: {integrity: sha512-+2TtZXIF6Iy4WxwpyeK9MXyYdfsuvIcRimotRDzhT7nM6TMLxJkDzHovqrcQF9/fAFd9wNHmRBA52PQIBicTSw==}
'@mariozechner/pi-coding-agent@0.46.0':
resolution: {integrity: sha512-zbvtV6hg7YbyAOK0xk+MYFZXjKjLka8GDxvsJT1f03XdoOrBmHfyHwAx/aCSaRGxqvHHR3ECqTThg7wkmMgSXw==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-tui@0.45.7':
resolution: {integrity: sha512-c4pMvyuC28PfpAYCsO01E651r+SAiNg216plmc0ej5qnizQm2x7X14C3UfxMfmWAMJ+U9Y0O7PucPdmXeHbjnw==}
'@mariozechner/pi-tui@0.46.0':
resolution: {integrity: sha512-1Qa2+bVXD2OuMXOlfUx8AYJzW/rx/RnAVwVVCHc8AMpU+DqPGD/QAu1xAlySxONV2KGr/FC9d0126Dzt05xVgw==}
engines: {node: '>=20.0.0'}
'@matrix-org/matrix-sdk-crypto-wasm@16.0.0':
@@ -1641,6 +1647,9 @@ packages:
cpu: [x64]
os: [win32]
'@silvia-odwyer/photon-node@0.3.4':
resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==}
'@sinclair/typebox@0.34.47':
resolution: {integrity: sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==}
@@ -4137,10 +4146,6 @@ packages:
jsdom:
optional: true
wasm-vips@0.0.16:
resolution: {integrity: sha512-4/bEq8noAFt7DX3VT+Vt5AgNtnnOLwvmrDbduWfiv9AV+VYkbUU4f9Dam9e6khRqPinyClFHCqiwATTTJEiGwA==}
engines: {node: '>=16.4.0'}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@@ -5164,10 +5169,10 @@ snapshots:
transitivePeerDependencies:
- tailwindcss
'@mariozechner/pi-agent-core@0.45.7(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-agent-core@0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@mariozechner/pi-ai': 0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.45.7
'@mariozechner/pi-ai': 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.46.0
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- aws-crt
@@ -5177,7 +5182,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-ai@0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-ai@0.46.0(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
'@aws-sdk/client-bedrock-runtime': 3.967.0
@@ -5199,13 +5204,14 @@ snapshots:
- ws
- zod
'@mariozechner/pi-coding-agent@0.45.7(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-coding-agent@0.46.0(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@mariozechner/clipboard': 0.3.0
'@mariozechner/jiti': 2.6.2
'@mariozechner/pi-agent-core': 0.45.7(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai': 0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.45.7
'@mariozechner/pi-agent-core': 0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai': 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.46.0
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
cli-highlight: 2.1.11
diff: 8.0.3
@@ -5214,7 +5220,6 @@ snapshots:
marked: 15.0.12
minimatch: 10.1.1
proper-lockfile: 4.1.2
wasm-vips: 0.0.16
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- aws-crt
@@ -5224,7 +5229,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-tui@0.45.7':
'@mariozechner/pi-tui@0.46.0':
dependencies:
'@types/mime-types': 2.1.4
chalk: 5.6.2
@@ -5759,6 +5764,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.55.1':
optional: true
'@silvia-odwyer/photon-node@0.3.4': {}
'@sinclair/typebox@0.34.47': {}
'@slack/bolt@4.6.0(@types/express@5.0.6)':
@@ -8693,8 +8700,6 @@ snapshots:
- tsx
- yaml
wasm-vips@0.0.16: {}
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
+1
View File
@@ -1,5 +1,6 @@
{
"ensureLogins": [
"alphonse-arianee",
"ronak-guliani",
"cpojer",
"carlulsoe",
-13
View File
@@ -214,19 +214,6 @@ if [ -f "$APP_BUNDLE/Contents/MacOS/Clawdbot" ]; then
echo "Signing main binary"; sign_item "$APP_BUNDLE/Contents/MacOS/Clawdbot" "$APP_ENTITLEMENTS"
fi
# Sign bundled gateway payload (native addons, libvips dylibs)
if [ -d "$APP_BUNDLE/Contents/Resources/Relay" ]; then
find "$APP_BUNDLE/Contents/Resources/Relay" -type f \( -name "*.node" -o -name "*.dylib" \) -print0 | while IFS= read -r -d '' f; do
echo "Signing gateway payload: $f"; sign_item "$f" "$ENT_TMP_BASE"
done
if [ -f "$APP_BUNDLE/Contents/Resources/Relay/node" ]; then
echo "Signing embedded node"; sign_item "$APP_BUNDLE/Contents/Resources/Relay/node" "$ENT_TMP_RUNTIME"
fi
if [ -f "$APP_BUNDLE/Contents/Resources/Relay/clawdbot" ]; then
echo "Signing embedded relay wrapper"; sign_plain_item "$APP_BUNDLE/Contents/Resources/Relay/clawdbot"
fi
fi
# Sign Sparkle deeply if present
SPARKLE="$APP_BUNDLE/Contents/Frameworks/Sparkle.framework"
if [ -d "$SPARKLE" ]; then
+1 -1
View File
@@ -10,7 +10,6 @@ type PackResult = { files?: PackFile[] };
const requiredPaths = [
"dist/discord/send.js",
"dist/hooks/gmail.js",
"dist/msteams/send.js",
"dist/whatsapp/normalize.js",
];
const forbiddenPrefixes = ["dist/Clawdbot.app/"];
@@ -68,6 +67,7 @@ function checkPluginVersions() {
for (const item of mismatches) {
console.error(` - ${item}`);
}
console.error("release-check: run `pnpm plugins:sync` to align plugin versions.");
process.exit(1);
}
}
+1
View File
@@ -56,6 +56,7 @@ Calendar Colors
Email Formatting
- Prefer plain text. Use `--body-file` for multi-paragraph messages (or `--body-file -` for stdin).
- Same `--body-file` pattern works for drafts and replies.
- `--body` does not unescape `\n`. If you need inline newlines, use a heredoc or `$'Line 1\n\nLine 2'`.
- Use `--body-html` only when you need rich formatting.
- HTML tags: `<p>` for paragraphs, `<br>` for line breaks, `<strong>` for bold, `<em>` for italic, `<a href="url">` for links, `<ul>`/`<li>` for lists.
+1 -9
View File
@@ -126,15 +126,7 @@ describe("cleanupSuspendedCliProcesses", () => {
await cleanupSuspendedCliProcesses(
{
command: "codex",
resumeArgs: [
"exec",
"resume",
"{sessionId}",
"--color",
"never",
"--sandbox",
"read-only",
],
resumeArgs: ["exec", "resume", "{sessionId}", "--color", "never", "--sandbox", "read-only"],
} as CliBackendConfig,
1,
);
@@ -33,6 +33,12 @@ describe("formatAssistantErrorText", () => {
"The AI service is temporarily overloaded. Please try again in a moment.",
);
});
it("handles JSON-wrapped role errors", () => {
const msg = makeAssistantError('{"error":{"message":"400 Incorrect role information"}}');
const result = formatAssistantErrorText(msg);
expect(result).toContain("Message ordering conflict");
expect(result).not.toContain("400");
});
it("suppresses raw error JSON payloads that are not otherwise classified", () => {
const msg = makeAssistantError(
'{"type":"error","error":{"message":"Something exploded","type":"server_error"}}',
@@ -127,4 +127,20 @@ describe("sanitizeSessionMessagesImages", () => {
const assistant = out[0] as { content?: Array<{ type?: string }> };
expect(assistant.content?.map((b) => b.type)).toEqual(["text", "toolCall", "thinking", "text"]);
});
it("does not synthesize tool call input when missing", async () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read" }],
},
] satisfies AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
const assistant = out[0] as { content?: Array<Record<string, unknown>> };
const toolCall = assistant.content?.find((b) => b.type === "toolCall");
expect(toolCall).toBeTruthy();
expect("input" in (toolCall ?? {})).toBe(false);
expect("arguments" in (toolCall ?? {})).toBe(false);
});
});
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { sanitizeUserFacingText } from "./pi-embedded-helpers.js";
describe("sanitizeUserFacingText", () => {
it("strips final tags", () => {
expect(sanitizeUserFacingText("<final>Hello</final>")).toBe("Hello");
expect(sanitizeUserFacingText("Hi <final>there</final>!")).toBe("Hi there!");
});
it("does not clobber normal numeric prefixes", () => {
expect(sanitizeUserFacingText("202 results found")).toBe("202 results found");
expect(sanitizeUserFacingText("400 days left")).toBe("400 days left");
});
it("sanitizes role ordering errors", () => {
const result = sanitizeUserFacingText("400 Incorrect role information");
expect(result).toContain("Message ordering conflict");
});
it("sanitizes HTTP status errors with error hints", () => {
expect(sanitizeUserFacingText("500 Internal Server Error")).toBe(
"The AI service returned an error. Please try again.",
);
});
it("sanitizes raw API error payloads", () => {
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}';
expect(sanitizeUserFacingText(raw)).toBe("The AI service returned an error. Please try again.");
});
});
+1
View File
@@ -14,6 +14,7 @@ export {
isAuthErrorMessage,
isBillingAssistantError,
parseApiErrorInfo,
sanitizeUserFacingText,
isBillingErrorMessage,
isCloudCodeAssistFormatError,
isCompactionFailureError,
Loaded 100 of 211 files, more files were not shown because too many files have changed in this diff. Show more