Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ba61028f | ||
|
|
efe5c62ae1 |
No files matched your search
@@ -4,52 +4,19 @@ Docs: https://docs.clawd.bot
|
|||||||
|
|
||||||
## 2026.1.23 (Unreleased)
|
## 2026.1.23 (Unreleased)
|
||||||
|
|
||||||
### Highlights
|
|
||||||
- TTS: allow model-driven TTS tags by default for expressive audio replies (laughter, singing cues, etc.).
|
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Gateway: add /tools/invoke HTTP endpoint for direct tool calls and document it. (#1575) Thanks @vignesh07.
|
|
||||||
- Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits.
|
|
||||||
- Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman.
|
|
||||||
- Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node).
|
|
||||||
- Heartbeat: add per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer.
|
|
||||||
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
|
|
||||||
- Docs: add emoji reaction guidance to AGENTS.md template. (#1591) Thanks @EnzeD.
|
|
||||||
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
||||||
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
||||||
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
|
|
||||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
|
||||||
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
||||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
|
|
||||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||||
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
||||||
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
|
|
||||||
- TTS: move Telegram TTS into core with auto-replies, commands, and gateway methods. (#1559) Thanks @Glucksberg.
|
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
|
||||||
- Routing/Cron: normalize agentId casing for bindings and cron payloads. (#1591)
|
|
||||||
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
|
|
||||||
- Messaging: mirror outbound sends into target session keys (threads + dmScope) and create session entries on send. (#1520)
|
|
||||||
- Sessions: normalize session key casing to lowercase for consistent routing.
|
|
||||||
- BlueBubbles: normalize group session keys for outbound mirroring. (#1520)
|
|
||||||
- Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
|
|
||||||
- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
|
|
||||||
- Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops).
|
|
||||||
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
|
|
||||||
- Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4.
|
|
||||||
- Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
|
|
||||||
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
|
||||||
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
||||||
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
||||||
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
|
|
||||||
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
||||||
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
|
||||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||||
- Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes.
|
|
||||||
- Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp).
|
|
||||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||||
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
|
||||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
||||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
||||||
@@ -58,25 +25,12 @@ Docs: https://docs.clawd.bot
|
|||||||
- CLI: render auth probe results as a table in `clawdbot models status`.
|
- CLI: render auth probe results as a table in `clawdbot models status`.
|
||||||
- CLI: suppress probe-only embedded logs unless `--verbose` is set.
|
- CLI: suppress probe-only embedded logs unless `--verbose` is set.
|
||||||
- CLI: move auth probe errors below the table to reduce wrapping.
|
- CLI: move auth probe errors below the table to reduce wrapping.
|
||||||
- CLI: prevent ANSI color bleed when table cells wrap.
|
|
||||||
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
|
||||||
- CLI: drop the em dash when the banner tagline wraps to a second line.
|
|
||||||
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
|
||||||
- Telegram: render markdown in media captions. (#1478)
|
|
||||||
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
|
|
||||||
- Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
|
|
||||||
- Daemon: use platform PATH delimiters when building minimal service paths.
|
|
||||||
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
|
||||||
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
||||||
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
||||||
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
|
|
||||||
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
||||||
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
||||||
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
||||||
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
||||||
- MS Teams (plugin): remove `.default` suffix from Bot Framework probe scope to avoid double-appending. (#1574) Thanks @Evizero.
|
|
||||||
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
|
|
||||||
- Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)
|
|
||||||
|
|
||||||
## 2026.1.22
|
## 2026.1.22
|
||||||
|
|
||||||
|
|||||||
@@ -479,29 +479,28 @@ Thanks to all clawtributors:
|
|||||||
<p align="left">
|
<p align="left">
|
||||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
||||||
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||||
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a>
|
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a>
|
||||||
<a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a>
|
<a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a>
|
||||||
<a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a>
|
<a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a>
|
||||||
<a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a>
|
<a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a>
|
||||||
<a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a>
|
<a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a>
|
||||||
<a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a>
|
<a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a>
|
||||||
<a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a>
|
<a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a>
|
||||||
<a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a>
|
<a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a>
|
||||||
<a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a>
|
<a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a>
|
||||||
<a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a>
|
<a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a>
|
||||||
<a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a>
|
<a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a>
|
||||||
<a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a>
|
<a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a>
|
||||||
<a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a>
|
<a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a>
|
||||||
<a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a>
|
<a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a>
|
||||||
<a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a>
|
<a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a>
|
||||||
<a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
|
<a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a>
|
||||||
<a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a>
|
<a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/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/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a>
|
||||||
<a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a>
|
<a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a>
|
||||||
<a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a>
|
<a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a>
|
||||||
<a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
|
<a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a>
|
||||||
<a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a>
|
<a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a>
|
||||||
<a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
|
<a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a>
|
||||||
<a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a>
|
<a href="https://github.com/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/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/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a>
|
||||||
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a>
|
<a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||||
<a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
|
||||||
</p>
|
</p>
|
||||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
public let to: String
|
public let to: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let mediaurl: String?
|
public let mediaurl: String?
|
||||||
public let mediaurls: [String]?
|
|
||||||
public let gifplayback: Bool?
|
public let gifplayback: Bool?
|
||||||
public let channel: String?
|
public let channel: String?
|
||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
to: String,
|
to: String,
|
||||||
message: String,
|
message: String,
|
||||||
mediaurl: String?,
|
mediaurl: String?,
|
||||||
mediaurls: [String]?,
|
|
||||||
gifplayback: Bool?,
|
gifplayback: Bool?,
|
||||||
channel: String?,
|
channel: String?,
|
||||||
accountid: String?,
|
accountid: String?,
|
||||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
self.to = to
|
self.to = to
|
||||||
self.message = message
|
self.message = message
|
||||||
self.mediaurl = mediaurl
|
self.mediaurl = mediaurl
|
||||||
self.mediaurls = mediaurls
|
|
||||||
self.gifplayback = gifplayback
|
self.gifplayback = gifplayback
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
case to
|
case to
|
||||||
case message
|
case message
|
||||||
case mediaurl = "mediaUrl"
|
case mediaurl = "mediaUrl"
|
||||||
case mediaurls = "mediaUrls"
|
|
||||||
case gifplayback = "gifPlayback"
|
case gifplayback = "gifPlayback"
|
||||||
case channel
|
case channel
|
||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
public let replyaccountid: String?
|
public let replyaccountid: String?
|
||||||
public let threadid: String?
|
public let threadid: String?
|
||||||
public let groupid: String?
|
|
||||||
public let groupchannel: String?
|
|
||||||
public let groupspace: String?
|
|
||||||
public let timeout: Int?
|
public let timeout: Int?
|
||||||
public let lane: String?
|
public let lane: String?
|
||||||
public let extrasystemprompt: String?
|
public let extrasystemprompt: String?
|
||||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
accountid: String?,
|
accountid: String?,
|
||||||
replyaccountid: String?,
|
replyaccountid: String?,
|
||||||
threadid: String?,
|
threadid: String?,
|
||||||
groupid: String?,
|
|
||||||
groupchannel: String?,
|
|
||||||
groupspace: String?,
|
|
||||||
timeout: Int?,
|
timeout: Int?,
|
||||||
lane: String?,
|
lane: String?,
|
||||||
extrasystemprompt: String?,
|
extrasystemprompt: String?,
|
||||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
self.replyaccountid = replyaccountid
|
self.replyaccountid = replyaccountid
|
||||||
self.threadid = threadid
|
self.threadid = threadid
|
||||||
self.groupid = groupid
|
|
||||||
self.groupchannel = groupchannel
|
|
||||||
self.groupspace = groupspace
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.lane = lane
|
self.lane = lane
|
||||||
self.extrasystemprompt = extrasystemprompt
|
self.extrasystemprompt = extrasystemprompt
|
||||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
case replyaccountid = "replyAccountId"
|
case replyaccountid = "replyAccountId"
|
||||||
case threadid = "threadId"
|
case threadid = "threadId"
|
||||||
case groupid = "groupId"
|
|
||||||
case groupchannel = "groupChannel"
|
|
||||||
case groupspace = "groupSpace"
|
|
||||||
case timeout
|
case timeout
|
||||||
case lane
|
case lane
|
||||||
case extrasystemprompt = "extraSystemPrompt"
|
case extrasystemprompt = "extraSystemPrompt"
|
||||||
@@ -964,7 +948,6 @@ public struct SessionsPreviewParams: Codable, Sendable {
|
|||||||
|
|
||||||
public struct SessionsResolveParams: Codable, Sendable {
|
public struct SessionsResolveParams: Codable, Sendable {
|
||||||
public let key: String?
|
public let key: String?
|
||||||
public let sessionid: String?
|
|
||||||
public let label: String?
|
public let label: String?
|
||||||
public let agentid: String?
|
public let agentid: String?
|
||||||
public let spawnedby: String?
|
public let spawnedby: String?
|
||||||
@@ -973,7 +956,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
key: String?,
|
key: String?,
|
||||||
sessionid: String?,
|
|
||||||
label: String?,
|
label: String?,
|
||||||
agentid: String?,
|
agentid: String?,
|
||||||
spawnedby: String?,
|
spawnedby: String?,
|
||||||
@@ -981,7 +963,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
|||||||
includeunknown: Bool?
|
includeunknown: Bool?
|
||||||
) {
|
) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.sessionid = sessionid
|
|
||||||
self.label = label
|
self.label = label
|
||||||
self.agentid = agentid
|
self.agentid = agentid
|
||||||
self.spawnedby = spawnedby
|
self.spawnedby = spawnedby
|
||||||
@@ -990,7 +971,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case key
|
case key
|
||||||
case sessionid = "sessionId"
|
|
||||||
case label
|
case label
|
||||||
case agentid = "agentId"
|
case agentid = "agentId"
|
||||||
case spawnedby = "spawnedBy"
|
case spawnedby = "spawnedBy"
|
||||||
|
|||||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
public let to: String
|
public let to: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let mediaurl: String?
|
public let mediaurl: String?
|
||||||
public let mediaurls: [String]?
|
|
||||||
public let gifplayback: Bool?
|
public let gifplayback: Bool?
|
||||||
public let channel: String?
|
public let channel: String?
|
||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
to: String,
|
to: String,
|
||||||
message: String,
|
message: String,
|
||||||
mediaurl: String?,
|
mediaurl: String?,
|
||||||
mediaurls: [String]?,
|
|
||||||
gifplayback: Bool?,
|
gifplayback: Bool?,
|
||||||
channel: String?,
|
channel: String?,
|
||||||
accountid: String?,
|
accountid: String?,
|
||||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
self.to = to
|
self.to = to
|
||||||
self.message = message
|
self.message = message
|
||||||
self.mediaurl = mediaurl
|
self.mediaurl = mediaurl
|
||||||
self.mediaurls = mediaurls
|
|
||||||
self.gifplayback = gifplayback
|
self.gifplayback = gifplayback
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
case to
|
case to
|
||||||
case message
|
case message
|
||||||
case mediaurl = "mediaUrl"
|
case mediaurl = "mediaUrl"
|
||||||
case mediaurls = "mediaUrls"
|
|
||||||
case gifplayback = "gifPlayback"
|
case gifplayback = "gifPlayback"
|
||||||
case channel
|
case channel
|
||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
public let replyaccountid: String?
|
public let replyaccountid: String?
|
||||||
public let threadid: String?
|
public let threadid: String?
|
||||||
public let groupid: String?
|
|
||||||
public let groupchannel: String?
|
|
||||||
public let groupspace: String?
|
|
||||||
public let timeout: Int?
|
public let timeout: Int?
|
||||||
public let lane: String?
|
public let lane: String?
|
||||||
public let extrasystemprompt: String?
|
public let extrasystemprompt: String?
|
||||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
accountid: String?,
|
accountid: String?,
|
||||||
replyaccountid: String?,
|
replyaccountid: String?,
|
||||||
threadid: String?,
|
threadid: String?,
|
||||||
groupid: String?,
|
|
||||||
groupchannel: String?,
|
|
||||||
groupspace: String?,
|
|
||||||
timeout: Int?,
|
timeout: Int?,
|
||||||
lane: String?,
|
lane: String?,
|
||||||
extrasystemprompt: String?,
|
extrasystemprompt: String?,
|
||||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
self.replyaccountid = replyaccountid
|
self.replyaccountid = replyaccountid
|
||||||
self.threadid = threadid
|
self.threadid = threadid
|
||||||
self.groupid = groupid
|
|
||||||
self.groupchannel = groupchannel
|
|
||||||
self.groupspace = groupspace
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.lane = lane
|
self.lane = lane
|
||||||
self.extrasystemprompt = extrasystemprompt
|
self.extrasystemprompt = extrasystemprompt
|
||||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
case replyaccountid = "replyAccountId"
|
case replyaccountid = "replyAccountId"
|
||||||
case threadid = "threadId"
|
case threadid = "threadId"
|
||||||
case groupid = "groupId"
|
|
||||||
case groupchannel = "groupChannel"
|
|
||||||
case groupspace = "groupSpace"
|
|
||||||
case timeout
|
case timeout
|
||||||
case lane
|
case lane
|
||||||
case extrasystemprompt = "extraSystemPrompt"
|
case extrasystemprompt = "extraSystemPrompt"
|
||||||
@@ -964,7 +948,6 @@ public struct SessionsPreviewParams: Codable, Sendable {
|
|||||||
|
|
||||||
public struct SessionsResolveParams: Codable, Sendable {
|
public struct SessionsResolveParams: Codable, Sendable {
|
||||||
public let key: String?
|
public let key: String?
|
||||||
public let sessionid: String?
|
|
||||||
public let label: String?
|
public let label: String?
|
||||||
public let agentid: String?
|
public let agentid: String?
|
||||||
public let spawnedby: String?
|
public let spawnedby: String?
|
||||||
@@ -973,7 +956,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
key: String?,
|
key: String?,
|
||||||
sessionid: String?,
|
|
||||||
label: String?,
|
label: String?,
|
||||||
agentid: String?,
|
agentid: String?,
|
||||||
spawnedby: String?,
|
spawnedby: String?,
|
||||||
@@ -981,7 +963,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
|||||||
includeunknown: Bool?
|
includeunknown: Bool?
|
||||||
) {
|
) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.sessionid = sessionid
|
|
||||||
self.label = label
|
self.label = label
|
||||||
self.agentid = agentid
|
self.agentid = agentid
|
||||||
self.spawnedby = spawnedby
|
self.spawnedby = spawnedby
|
||||||
@@ -990,7 +971,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case key
|
case key
|
||||||
case sessionid = "sessionId"
|
|
||||||
case label
|
case label
|
||||||
case agentid = "agentId"
|
case agentid = "agentId"
|
||||||
case spawnedby = "spawnedBy"
|
case spawnedby = "spawnedBy"
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ services:
|
|||||||
[
|
[
|
||||||
"node",
|
"node",
|
||||||
"dist/index.js",
|
"dist/index.js",
|
||||||
"gateway",
|
"gateway-daemon",
|
||||||
"--bind",
|
"--bind",
|
||||||
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
@@ -263,15 +263,15 @@ Run history:
|
|||||||
clawdbot cron runs --id <jobId> --limit 50
|
clawdbot cron runs --id <jobId> --limit 50
|
||||||
```
|
```
|
||||||
|
|
||||||
Immediate system event without creating a job:
|
Immediate wake without creating a job:
|
||||||
```bash
|
```bash
|
||||||
clawdbot system event --mode now --text "Next heartbeat: check battery."
|
clawdbot wake --mode now --text "Next heartbeat: check battery."
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gateway API surface
|
## Gateway API surface
|
||||||
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
||||||
- `cron.run` (force or due), `cron.runs`
|
- `cron.run` (force or due), `cron.runs`
|
||||||
For immediate system events without a job, use [`clawdbot system event`](/cli/system).
|
- `wake` (enqueue system event + optional heartbeat)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -271,4 +271,4 @@ clawdbot cron add \
|
|||||||
|
|
||||||
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
|
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
|
||||||
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
|
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
|
||||||
- [System](/cli/system) - system events + heartbeat controls
|
- [Wake](/cli/wake) - manual wake command
|
||||||
@@ -17,37 +17,6 @@ not an API key.
|
|||||||
- Auth: AWS credentials (env vars, shared config, or instance role)
|
- Auth: AWS credentials (env vars, shared config, or instance role)
|
||||||
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
|
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
|
||||||
|
|
||||||
## Automatic model discovery
|
|
||||||
|
|
||||||
If AWS credentials are detected, Clawdbot can automatically discover Bedrock
|
|
||||||
models that support **streaming** and **text output**. Discovery uses
|
|
||||||
`bedrock:ListFoundationModels` and is cached (default: 1 hour).
|
|
||||||
|
|
||||||
Config options live under `models.bedrockDiscovery`:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
models: {
|
|
||||||
bedrockDiscovery: {
|
|
||||||
enabled: true,
|
|
||||||
region: "us-east-1",
|
|
||||||
providerFilter: ["anthropic", "amazon"],
|
|
||||||
refreshInterval: 3600,
|
|
||||||
defaultContextWindow: 32000,
|
|
||||||
defaultMaxTokens: 4096
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- `enabled` defaults to `true` when AWS credentials are present.
|
|
||||||
- `region` defaults to `AWS_REGION` or `AWS_DEFAULT_REGION`, then `us-east-1`.
|
|
||||||
- `providerFilter` matches Bedrock provider names (for example `anthropic`).
|
|
||||||
- `refreshInterval` is seconds; set to `0` to disable caching.
|
|
||||||
- `defaultContextWindow` (default: `32000`) and `defaultMaxTokens` (default: `4096`)
|
|
||||||
are used for discovered models (override if you know your model limits).
|
|
||||||
|
|
||||||
## Setup (manual)
|
## Setup (manual)
|
||||||
|
|
||||||
1) Ensure AWS credentials are available on the **gateway host**:
|
1) Ensure AWS credentials are available on the **gateway host**:
|
||||||
@@ -98,7 +67,6 @@ export AWS_BEARER_TOKEN_BEDROCK="..."
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Bedrock requires **model access** enabled in your AWS account/region.
|
- Bedrock requires **model access** enabled in your AWS account/region.
|
||||||
- Automatic discovery needs the `bedrock:ListFoundationModels` permission.
|
|
||||||
- If you use profiles, set `AWS_PROFILE` on the gateway host.
|
- If you use profiles, set `AWS_PROFILE` on the gateway host.
|
||||||
- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`,
|
- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`,
|
||||||
then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the
|
then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ clawdbot channels logout --channel whatsapp
|
|||||||
|
|
||||||
- Run `clawdbot status --deep` for a broad probe.
|
- Run `clawdbot status --deep` for a broad probe.
|
||||||
- Use `clawdbot doctor` for guided fixes.
|
- Use `clawdbot doctor` for guided fixes.
|
||||||
- `clawdbot channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude Code CLI.
|
|
||||||
|
|
||||||
## Capabilities probe
|
## Capabilities probe
|
||||||
|
|
||||||
|
|||||||
+5
-22
@@ -29,7 +29,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`sessions`](/cli/sessions)
|
- [`sessions`](/cli/sessions)
|
||||||
- [`gateway`](/cli/gateway)
|
- [`gateway`](/cli/gateway)
|
||||||
- [`logs`](/cli/logs)
|
- [`logs`](/cli/logs)
|
||||||
- [`system`](/cli/system)
|
|
||||||
- [`models`](/cli/models)
|
- [`models`](/cli/models)
|
||||||
- [`memory`](/cli/memory)
|
- [`memory`](/cli/memory)
|
||||||
- [`nodes`](/cli/nodes)
|
- [`nodes`](/cli/nodes)
|
||||||
@@ -39,6 +38,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`sandbox`](/cli/sandbox)
|
- [`sandbox`](/cli/sandbox)
|
||||||
- [`tui`](/cli/tui)
|
- [`tui`](/cli/tui)
|
||||||
- [`browser`](/cli/browser)
|
- [`browser`](/cli/browser)
|
||||||
|
- [`wake`](/cli/wake)
|
||||||
- [`cron`](/cli/cron)
|
- [`cron`](/cli/cron)
|
||||||
- [`dns`](/cli/dns)
|
- [`dns`](/cli/dns)
|
||||||
- [`docs`](/cli/docs)
|
- [`docs`](/cli/docs)
|
||||||
@@ -145,10 +145,6 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
restart
|
restart
|
||||||
run
|
run
|
||||||
logs
|
logs
|
||||||
system
|
|
||||||
event
|
|
||||||
heartbeat last|enable|disable
|
|
||||||
presence
|
|
||||||
models
|
models
|
||||||
list
|
list
|
||||||
status
|
status
|
||||||
@@ -164,6 +160,7 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
list
|
list
|
||||||
recreate
|
recreate
|
||||||
explain
|
explain
|
||||||
|
wake
|
||||||
cron
|
cron
|
||||||
status
|
status
|
||||||
list
|
list
|
||||||
@@ -766,9 +763,9 @@ Options:
|
|||||||
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
||||||
- `clear`: `--provider <name>`, `--agent <id>`
|
- `clear`: `--provider <name>`, `--agent <id>`
|
||||||
|
|
||||||
## System
|
## Cron + wake
|
||||||
|
|
||||||
### `system event`
|
### `wake`
|
||||||
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
||||||
|
|
||||||
Required:
|
Required:
|
||||||
@@ -779,21 +776,7 @@ Options:
|
|||||||
- `--json`
|
- `--json`
|
||||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||||
|
|
||||||
### `system heartbeat last|enable|disable`
|
### `cron`
|
||||||
Heartbeat controls (Gateway RPC).
|
|
||||||
|
|
||||||
Options:
|
|
||||||
- `--json`
|
|
||||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
|
||||||
|
|
||||||
### `system presence`
|
|
||||||
List system presence entries (Gateway RPC).
|
|
||||||
|
|
||||||
Options:
|
|
||||||
- `--json`
|
|
||||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
|
||||||
|
|
||||||
## Cron
|
|
||||||
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
|
|||||||
@@ -23,24 +23,6 @@ Common use cases:
|
|||||||
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
||||||
node host, so you can keep command access scoped and explicit.
|
node host, so you can keep command access scoped and explicit.
|
||||||
|
|
||||||
## Browser proxy (zero-config)
|
|
||||||
|
|
||||||
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
|
|
||||||
disabled on the node. This lets the agent use browser automation on that node
|
|
||||||
without extra configuration.
|
|
||||||
|
|
||||||
Disable it on the node if needed:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
nodeHost: {
|
|
||||||
browserProxy: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run (foreground)
|
## Run (foreground)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "CLI reference for `clawdbot system` (system events, heartbeat, presence)"
|
|
||||||
read_when:
|
|
||||||
- You want to enqueue a system event without creating a cron job
|
|
||||||
- You need to enable or disable heartbeats
|
|
||||||
- You want to inspect system presence entries
|
|
||||||
---
|
|
||||||
|
|
||||||
# `clawdbot system`
|
|
||||||
|
|
||||||
System-level helpers for the Gateway: enqueue system events, control heartbeats,
|
|
||||||
and view presence.
|
|
||||||
|
|
||||||
## Common commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
|
||||||
clawdbot system heartbeat enable
|
|
||||||
clawdbot system heartbeat last
|
|
||||||
clawdbot system presence
|
|
||||||
```
|
|
||||||
|
|
||||||
## `system event`
|
|
||||||
|
|
||||||
Enqueue a system event on the **main** session. The next heartbeat will inject
|
|
||||||
it as a `System:` line in the prompt. Use `--mode now` to trigger the heartbeat
|
|
||||||
immediately; `next-heartbeat` waits for the next scheduled tick.
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
- `--text <text>`: required system event text.
|
|
||||||
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
|
||||||
- `--json`: machine-readable output.
|
|
||||||
|
|
||||||
## `system heartbeat last|enable|disable`
|
|
||||||
|
|
||||||
Heartbeat controls:
|
|
||||||
- `last`: show the last heartbeat event.
|
|
||||||
- `enable`: turn heartbeats back on (use this if they were disabled).
|
|
||||||
- `disable`: pause heartbeats.
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
- `--json`: machine-readable output.
|
|
||||||
|
|
||||||
## `system presence`
|
|
||||||
|
|
||||||
List the current system presence entries the Gateway knows about (nodes,
|
|
||||||
instances, and similar status lines).
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
- `--json`: machine-readable output.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Requires a running Gateway reachable by your current config (local or remote).
|
|
||||||
- System events are ephemeral and not persisted across restarts.
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
summary: "CLI reference for `clawdbot wake` (enqueue a system event and optionally trigger an immediate heartbeat)"
|
||||||
|
read_when:
|
||||||
|
- You want to “poke” a running Gateway to process a system event
|
||||||
|
- You use `wake` with cron jobs or remote nodes
|
||||||
|
---
|
||||||
|
|
||||||
|
# `clawdbot wake`
|
||||||
|
|
||||||
|
Enqueue a system event on the Gateway and optionally trigger an immediate heartbeat.
|
||||||
|
|
||||||
|
This is a lightweight “poke” for automation flows where you don’t want to run a full command, but you do want the Gateway to react quickly.
|
||||||
|
|
||||||
|
Related:
|
||||||
|
- Cron jobs: [Cron](/cli/cron)
|
||||||
|
- Gateway heartbeat: [Heartbeat](/gateway/heartbeat)
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot wake --text "sync"
|
||||||
|
clawdbot wake --text "sync" --mode now
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
- `--text <text>`: system event text.
|
||||||
|
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
||||||
|
- `--json`: machine-readable output.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Requires a running Gateway reachable by your current config (local or remote).
|
||||||
|
- If you’re using sandboxing, `wake` still targets the Gateway; sandboxing does not block the command itself.
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
summary: "Alias for compaction docs"
|
||||||
|
read_when:
|
||||||
|
- You looked for /compaction; canonical doc lives in /concepts/compaction
|
||||||
|
---
|
||||||
|
# Compaction
|
||||||
|
|
||||||
|
Canonical compaction docs live in [Compaction](/concepts/compaction).
|
||||||
@@ -56,20 +56,19 @@ Row shape (JSON):
|
|||||||
Fetch transcript for one session.
|
Fetch transcript for one session.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
|
- `sessionKey` (required)
|
||||||
- `limit?: number` max messages (server clamps)
|
- `limit?: number` max messages (server clamps)
|
||||||
- `includeTools?: boolean` (default false)
|
- `includeTools?: boolean` (default false)
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
- `includeTools=false` filters `role: "toolResult"` messages.
|
- `includeTools=false` filters `role: "toolResult"` messages.
|
||||||
- Returns messages array in the raw transcript format.
|
- Returns messages array in the raw transcript format.
|
||||||
- When given a `sessionId`, Clawdbot resolves it to the corresponding session key (missing ids error).
|
|
||||||
|
|
||||||
## sessions_send
|
## sessions_send
|
||||||
Send a message into another session.
|
Send a message into another session.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
|
- `sessionKey` (required)
|
||||||
- `message` (required)
|
- `message` (required)
|
||||||
- `timeoutSeconds?: number` (default >0; 0 = fire-and-forget)
|
- `timeoutSeconds?: number` (default >0; 0 = fire-and-forget)
|
||||||
|
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ To inspect how much each injected file contributes (raw vs injected, truncation,
|
|||||||
|
|
||||||
## Time handling
|
## Time handling
|
||||||
|
|
||||||
The system prompt includes a dedicated **Current Date & Time** section when the
|
The system prompt includes a dedicated **Current Date & Time** section when user
|
||||||
user timezone is known. To keep the prompt cache-stable, it now only includes
|
time or timezone is known. It is explicit about:
|
||||||
the **time zone** (no dynamic clock or time format).
|
|
||||||
|
|
||||||
Use `session_status` when the agent needs the current time; the status card
|
- The user’s **local time** (already converted).
|
||||||
includes a timestamp line.
|
- The **time zone** used for the conversion.
|
||||||
|
- The **time format** (12-hour / 24-hour).
|
||||||
|
|
||||||
Configure with:
|
Configure with:
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -7,8 +7,8 @@ read_when:
|
|||||||
|
|
||||||
# Date & Time
|
# Date & Time
|
||||||
|
|
||||||
Clawdbot defaults to **host-local time for transport timestamps** and **user timezone only in the system prompt**.
|
Clawdbot defaults to **host-local time for transport timestamps** and **user-local time only in the system prompt**.
|
||||||
Provider timestamps are preserved so tools keep their native semantics (current time is available via `session_status`).
|
Provider timestamps are preserved so tools keep their native semantics.
|
||||||
|
|
||||||
## Message envelopes (local by default)
|
## Message envelopes (local by default)
|
||||||
|
|
||||||
@@ -63,16 +63,16 @@ You can override this behavior:
|
|||||||
|
|
||||||
## System prompt: Current Date & Time
|
## System prompt: Current Date & Time
|
||||||
|
|
||||||
If the user timezone is known, the system prompt includes a dedicated
|
If the user timezone or local time is known, the system prompt includes a dedicated
|
||||||
**Current Date & Time** section with the **time zone only** (no clock/time format)
|
**Current Date & Time** section:
|
||||||
to keep prompt caching stable:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Time zone: America/Chicago
|
Thursday, January 15th, 2026 — 3:07 PM (America/Chicago)
|
||||||
|
Time format: 12-hour
|
||||||
```
|
```
|
||||||
|
|
||||||
When the agent needs the current time, use the `session_status` tool; the status
|
If only the timezone is known, we still include the section and instruct the model
|
||||||
card includes a timestamp line.
|
to assume UTC for unknown time references.
|
||||||
|
|
||||||
## System event lines (local by default)
|
## System event lines (local by default)
|
||||||
|
|
||||||
|
|||||||
+16
-8
@@ -387,7 +387,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/faq",
|
"source": "/faq",
|
||||||
"destination": "/help/faq"
|
"destination": "/start/faq"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/gateway-lock",
|
"source": "/gateway-lock",
|
||||||
@@ -449,6 +449,10 @@
|
|||||||
"source": "/location-command",
|
"source": "/location-command",
|
||||||
"destination": "/nodes/location-command"
|
"destination": "/nodes/location-command"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/logging",
|
||||||
|
"destination": "/gateway/logging"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/lore",
|
"source": "/lore",
|
||||||
"destination": "/start/lore"
|
"destination": "/start/lore"
|
||||||
@@ -757,13 +761,21 @@
|
|||||||
"source": "/wizard",
|
"source": "/wizard",
|
||||||
"destination": "/start/wizard"
|
"destination": "/start/wizard"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/install/node",
|
||||||
|
"destination": "/install#nodejs--npm-path-sanity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/install/node/",
|
||||||
|
"destination": "/install#nodejs--npm-path-sanity"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/start/faq",
|
"source": "/start/faq",
|
||||||
"destination": "/help/faq"
|
"destination": "/help"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/start/faq/",
|
"source": "/start/faq/",
|
||||||
"destination": "/help/faq"
|
"destination": "/help"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/oauth",
|
"source": "/oauth",
|
||||||
@@ -838,12 +850,12 @@
|
|||||||
"cli/memory",
|
"cli/memory",
|
||||||
"cli/models",
|
"cli/models",
|
||||||
"cli/logs",
|
"cli/logs",
|
||||||
"cli/system",
|
|
||||||
"cli/nodes",
|
"cli/nodes",
|
||||||
"cli/approvals",
|
"cli/approvals",
|
||||||
"cli/gateway",
|
"cli/gateway",
|
||||||
"cli/tui",
|
"cli/tui",
|
||||||
"cli/voicecall",
|
"cli/voicecall",
|
||||||
|
"cli/wake",
|
||||||
"cli/cron",
|
"cli/cron",
|
||||||
"cli/dns",
|
"cli/dns",
|
||||||
"cli/docs",
|
"cli/docs",
|
||||||
@@ -904,7 +916,6 @@
|
|||||||
"gateway/configuration-examples",
|
"gateway/configuration-examples",
|
||||||
"gateway/authentication",
|
"gateway/authentication",
|
||||||
"gateway/openai-http-api",
|
"gateway/openai-http-api",
|
||||||
"gateway/tools-invoke-http-api",
|
|
||||||
"gateway/cli-backends",
|
"gateway/cli-backends",
|
||||||
"gateway/local-models",
|
"gateway/local-models",
|
||||||
"gateway/background-process",
|
"gateway/background-process",
|
||||||
@@ -989,8 +1000,6 @@
|
|||||||
"group": "Tools & Skills",
|
"group": "Tools & Skills",
|
||||||
"pages": [
|
"pages": [
|
||||||
"tools",
|
"tools",
|
||||||
"tools/lobster",
|
|
||||||
"tools/llm-task",
|
|
||||||
"plugin",
|
"plugin",
|
||||||
"plugins/voice-call",
|
"plugins/voice-call",
|
||||||
"plugins/zalouser",
|
"plugins/zalouser",
|
||||||
@@ -1034,7 +1043,6 @@
|
|||||||
"platforms/android",
|
"platforms/android",
|
||||||
"platforms/windows",
|
"platforms/windows",
|
||||||
"platforms/linux",
|
"platforms/linux",
|
||||||
"platforms/fly",
|
|
||||||
"platforms/hetzner",
|
"platforms/hetzner",
|
||||||
"platforms/exe-dev"
|
"platforms/exe-dev"
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -74,5 +74,5 @@ See [Configuration: Env var substitution](/gateway/configuration#env-var-substit
|
|||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Gateway configuration](/gateway/configuration)
|
- [Gateway configuration](/gateway/configuration)
|
||||||
- [FAQ: env vars and .env loading](/help/faq#env-vars-and-env-loading)
|
- [FAQ: env vars and .env loading](/start/faq#env-vars-and-env-loading)
|
||||||
- [Models overview](/concepts/models)
|
- [Models overview](/concepts/models)
|
||||||
@@ -1446,65 +1446,6 @@ active agent’s `identity.emoji` when set, otherwise `"👀"`. Set it to `""` t
|
|||||||
`removeAckAfterReply` removes the bot’s ack reaction after a reply is sent
|
`removeAckAfterReply` removes the bot’s ack reaction after a reply is sent
|
||||||
(Slack/Discord/Telegram only). Default: `false`.
|
(Slack/Discord/Telegram only). Default: `false`.
|
||||||
|
|
||||||
#### `messages.tts`
|
|
||||||
|
|
||||||
Enable text-to-speech for outbound replies. When on, Clawdbot generates audio
|
|
||||||
using ElevenLabs or OpenAI and attaches it to responses. Telegram uses Opus
|
|
||||||
voice notes; other channels send MP3 audio.
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
tts: {
|
|
||||||
enabled: true,
|
|
||||||
mode: "final", // final | all (include tool/block replies)
|
|
||||||
provider: "elevenlabs",
|
|
||||||
summaryModel: "openai/gpt-4.1-mini",
|
|
||||||
modelOverrides: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
maxTextLength: 4000,
|
|
||||||
timeoutMs: 30000,
|
|
||||||
prefsPath: "~/.clawdbot/settings/tts.json",
|
|
||||||
elevenlabs: {
|
|
||||||
apiKey: "elevenlabs_api_key",
|
|
||||||
baseUrl: "https://api.elevenlabs.io",
|
|
||||||
voiceId: "voice_id",
|
|
||||||
modelId: "eleven_multilingual_v2",
|
|
||||||
seed: 42,
|
|
||||||
applyTextNormalization: "auto",
|
|
||||||
languageCode: "en",
|
|
||||||
voiceSettings: {
|
|
||||||
stability: 0.5,
|
|
||||||
similarityBoost: 0.75,
|
|
||||||
style: 0.0,
|
|
||||||
useSpeakerBoost: true,
|
|
||||||
speed: 1.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openai: {
|
|
||||||
apiKey: "openai_api_key",
|
|
||||||
model: "gpt-4o-mini-tts",
|
|
||||||
voice: "alloy"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- `messages.tts.enabled` can be overridden by local user prefs (see `/tts on`, `/tts off`).
|
|
||||||
- `prefsPath` stores local overrides (enabled/provider/limit/summarize).
|
|
||||||
- `maxTextLength` is a hard cap for TTS input; summaries are truncated to fit.
|
|
||||||
- `summaryModel` overrides `agents.defaults.model.primary` for auto-summary.
|
|
||||||
- Accepts `provider/model` or an alias from `agents.defaults.models`.
|
|
||||||
- `modelOverrides` enables model-driven overrides like `[[tts:...]]` tags (on by default).
|
|
||||||
- `/tts limit` and `/tts summary` control per-user summarization settings.
|
|
||||||
- `apiKey` values fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
|
|
||||||
- `elevenlabs.baseUrl` overrides the ElevenLabs API base URL.
|
|
||||||
- `elevenlabs.voiceSettings` supports `stability`/`similarityBoost`/`style` (0..1),
|
|
||||||
`useSpeakerBoost`, and `speed` (0.5..2.0).
|
|
||||||
|
|
||||||
### `talk`
|
### `talk`
|
||||||
|
|
||||||
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.
|
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.
|
||||||
@@ -2029,7 +1970,6 @@ Example (provider/model-specific allowlist):
|
|||||||
```
|
```
|
||||||
|
|
||||||
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||||||
Matching is case-insensitive and supports `*` wildcards (`"*"` means all tools).
|
|
||||||
This is applied even when the Docker sandbox is **off**.
|
This is applied even when the Docker sandbox is **off**.
|
||||||
|
|
||||||
Example (disable browser/canvas everywhere):
|
Example (disable browser/canvas everywhere):
|
||||||
|
|||||||
@@ -92,14 +92,6 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scope and precedence
|
|
||||||
|
|
||||||
- `agents.defaults.heartbeat` sets global heartbeat behavior.
|
|
||||||
- `agents.list[].heartbeat` merges on top; if any agent has a `heartbeat` block, **only those agents** run heartbeats.
|
|
||||||
- `channels.defaults.heartbeat` sets visibility defaults for all channels.
|
|
||||||
- `channels.<channel>.heartbeat` overrides channel defaults.
|
|
||||||
- `channels.<channel>.accounts.<id>.heartbeat` (multi-account channels) overrides per-channel settings.
|
|
||||||
|
|
||||||
### Per-agent heartbeats
|
### Per-agent heartbeats
|
||||||
|
|
||||||
If any `agents.list[]` entry includes a `heartbeat` block, **only those agents**
|
If any `agents.list[]` entry includes a `heartbeat` block, **only those agents**
|
||||||
@@ -164,78 +156,12 @@ Example: two agents, only the second agent runs heartbeats.
|
|||||||
- Heartbeat-only replies do **not** keep the session alive; the last `updatedAt`
|
- Heartbeat-only replies do **not** keep the session alive; the last `updatedAt`
|
||||||
is restored so idle expiry behaves normally.
|
is restored so idle expiry behaves normally.
|
||||||
|
|
||||||
## Visibility controls
|
|
||||||
|
|
||||||
By default, `HEARTBEAT_OK` acknowledgments are suppressed while alert content is
|
|
||||||
delivered. You can adjust this per channel or per account:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
channels:
|
|
||||||
defaults:
|
|
||||||
heartbeat:
|
|
||||||
showOk: false # Hide HEARTBEAT_OK (default)
|
|
||||||
showAlerts: true # Show alert messages (default)
|
|
||||||
useIndicator: true # Emit indicator events (default)
|
|
||||||
telegram:
|
|
||||||
heartbeat:
|
|
||||||
showOk: true # Show OK acknowledgments on Telegram
|
|
||||||
whatsapp:
|
|
||||||
accounts:
|
|
||||||
work:
|
|
||||||
heartbeat:
|
|
||||||
showAlerts: false # Suppress alert delivery for this account
|
|
||||||
```
|
|
||||||
|
|
||||||
Precedence: per-account → per-channel → channel defaults → built-in defaults.
|
|
||||||
|
|
||||||
### What each flag does
|
|
||||||
|
|
||||||
- `showOk`: sends a `HEARTBEAT_OK` acknowledgment when the model returns an OK-only reply.
|
|
||||||
- `showAlerts`: sends the alert content when the model returns a non-OK reply.
|
|
||||||
- `useIndicator`: emits indicator events for UI status surfaces.
|
|
||||||
|
|
||||||
If **all three** are false, Clawdbot skips the heartbeat run entirely (no model call).
|
|
||||||
|
|
||||||
### Per-channel vs per-account examples
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
channels:
|
|
||||||
defaults:
|
|
||||||
heartbeat:
|
|
||||||
showOk: false
|
|
||||||
showAlerts: true
|
|
||||||
useIndicator: true
|
|
||||||
slack:
|
|
||||||
heartbeat:
|
|
||||||
showOk: true # all Slack accounts
|
|
||||||
accounts:
|
|
||||||
ops:
|
|
||||||
heartbeat:
|
|
||||||
showAlerts: false # suppress alerts for the ops account only
|
|
||||||
telegram:
|
|
||||||
heartbeat:
|
|
||||||
showOk: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Common patterns
|
|
||||||
|
|
||||||
| Goal | Config |
|
|
||||||
| --- | --- |
|
|
||||||
| Default behavior (silent OKs, alerts on) | *(no config needed)* |
|
|
||||||
| Fully silent (no messages, no indicator) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: false }` |
|
|
||||||
| Indicator-only (no messages) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: true }` |
|
|
||||||
| OKs in one channel only | `channels.telegram.heartbeat: { showOk: true }` |
|
|
||||||
|
|
||||||
## HEARTBEAT.md (optional)
|
## HEARTBEAT.md (optional)
|
||||||
|
|
||||||
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
|
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
|
||||||
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
||||||
safe to include every 30 minutes.
|
safe to include every 30 minutes.
|
||||||
|
|
||||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
|
||||||
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
|
||||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
|
||||||
|
|
||||||
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
||||||
|
|
||||||
Example `HEARTBEAT.md`:
|
Example `HEARTBEAT.md`:
|
||||||
@@ -269,7 +195,7 @@ Safety note: don’t put secrets (API keys, phone numbers, private tokens) into
|
|||||||
You can enqueue a system event and trigger an immediate heartbeat with:
|
You can enqueue a system event and trigger an immediate heartbeat with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
clawdbot wake --text "Check for urgent follow-ups" --mode now
|
||||||
```
|
```
|
||||||
|
|
||||||
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ pnpm gateway:watch
|
|||||||
- The same port also serves HTTP (control UI, hooks, A2UI). Single-port multiplex.
|
- The same port also serves HTTP (control UI, hooks, A2UI). Single-port multiplex.
|
||||||
- OpenAI Chat Completions (HTTP): [`/v1/chat/completions`](/gateway/openai-http-api).
|
- OpenAI Chat Completions (HTTP): [`/v1/chat/completions`](/gateway/openai-http-api).
|
||||||
- OpenResponses (HTTP): [`/v1/responses`](/gateway/openresponses-http-api).
|
- OpenResponses (HTTP): [`/v1/responses`](/gateway/openresponses-http-api).
|
||||||
- Tools Invoke (HTTP): [`/tools/invoke`](/gateway/tools-invoke-http-api).
|
|
||||||
- Starts a Canvas file server by default on `canvasHost.port` (default `18793`), serving `http://<gateway-host>:18793/__clawdbot__/canvas/` from `~/clawd/canvas`. Disable with `canvasHost.enabled=false` or `CLAWDBOT_SKIP_CANVAS_HOST=1`.
|
- Starts a Canvas file server by default on `canvasHost.port` (default `18793`), serving `http://<gateway-host>:18793/__clawdbot__/canvas/` from `~/clawd/canvas`. Disable with `canvasHost.enabled=false` or `CLAWDBOT_SKIP_CANVAS_HOST=1`.
|
||||||
- Logs to stdout; use launchd/systemd to keep it alive and rotate logs.
|
- Logs to stdout; use launchd/systemd to keep it alive and rotate logs.
|
||||||
- Pass `--verbose` to mirror debug logging (handshakes, req/res, events) from the log file into stdio when troubleshooting.
|
- Pass `--verbose` to mirror debug logging (handshakes, req/res, events) from the log file into stdio when troubleshooting.
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "Invoke a single tool directly via the Gateway HTTP endpoint"
|
|
||||||
read_when:
|
|
||||||
- Calling tools without running a full agent turn
|
|
||||||
- Building automations that need tool policy enforcement
|
|
||||||
---
|
|
||||||
# Tools Invoke (HTTP)
|
|
||||||
|
|
||||||
Clawdbot’s Gateway exposes a simple HTTP endpoint for invoking a single tool directly. It is always enabled, but gated by Gateway auth and tool policy.
|
|
||||||
|
|
||||||
- `POST /tools/invoke`
|
|
||||||
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/tools/invoke`
|
|
||||||
|
|
||||||
Default max payload size is 2 MB.
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
Uses the Gateway auth configuration. Send a bearer token:
|
|
||||||
|
|
||||||
- `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- When `gateway.auth.mode="token"`, use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
|
||||||
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `CLAWDBOT_GATEWAY_PASSWORD`).
|
|
||||||
|
|
||||||
## Request body
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "sessions_list",
|
|
||||||
"action": "json",
|
|
||||||
"args": {},
|
|
||||||
"sessionKey": "main",
|
|
||||||
"dryRun": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Fields:
|
|
||||||
- `tool` (string, required): tool name to invoke.
|
|
||||||
- `action` (string, optional): mapped into args if the tool schema supports `action` and the args payload omitted it.
|
|
||||||
- `args` (object, optional): tool-specific arguments.
|
|
||||||
- `sessionKey` (string, optional): target session key. If omitted or `"main"`, the Gateway uses the configured main session key (honors `session.mainKey` and default agent, or `global` in global scope).
|
|
||||||
- `dryRun` (boolean, optional): reserved for future use; currently ignored.
|
|
||||||
|
|
||||||
## Policy + routing behavior
|
|
||||||
|
|
||||||
Tool availability is filtered through the same policy chain used by Gateway agents:
|
|
||||||
- `tools.profile` / `tools.byProvider.profile`
|
|
||||||
- `tools.allow` / `tools.byProvider.allow`
|
|
||||||
- `agents.<id>.tools.allow` / `agents.<id>.tools.byProvider.allow`
|
|
||||||
- group policies (if the session key maps to a group or channel)
|
|
||||||
- subagent policy (when invoking with a subagent session key)
|
|
||||||
|
|
||||||
If a tool is not allowed by policy, the endpoint returns **404**.
|
|
||||||
|
|
||||||
To help group policies resolve context, you can optionally set:
|
|
||||||
- `x-clawdbot-message-channel: <channel>` (example: `slack`, `telegram`)
|
|
||||||
- `x-clawdbot-account-id: <accountId>` (when multiple accounts exist)
|
|
||||||
|
|
||||||
## Responses
|
|
||||||
|
|
||||||
- `200` → `{ ok: true, result }`
|
|
||||||
- `400` → `{ ok: false, error: { type, message } }` (invalid request or tool error)
|
|
||||||
- `401` → unauthorized
|
|
||||||
- `404` → tool not available (not found or not allowlisted)
|
|
||||||
- `405` → method not allowed
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sS http://127.0.0.1:18789/tools/invoke \
|
|
||||||
-H 'Authorization: Bearer YOUR_TOKEN' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"tool": "sessions_list",
|
|
||||||
"action": "json",
|
|
||||||
"args": {}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
@@ -7,7 +7,7 @@ read_when:
|
|||||||
|
|
||||||
When Clawdbot misbehaves, here's how to fix it.
|
When Clawdbot misbehaves, here's how to fix it.
|
||||||
|
|
||||||
Start with the FAQ’s [First 60 seconds](/help/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics.
|
Start with the FAQ’s [First 60 seconds](/start/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics.
|
||||||
|
|
||||||
Provider-specific shortcuts: [/channels/troubleshooting](/channels/troubleshooting)
|
Provider-specific shortcuts: [/channels/troubleshooting](/channels/troubleshooting)
|
||||||
|
|
||||||
@@ -31,34 +31,6 @@ See also: [Health checks](/gateway/health) and [Logging](/logging).
|
|||||||
|
|
||||||
## Common Issues
|
## Common Issues
|
||||||
|
|
||||||
### OAuth token refresh failed (Anthropic Claude subscription)
|
|
||||||
|
|
||||||
This means the stored Anthropic OAuth token expired and the refresh failed.
|
|
||||||
If you’re on a Claude subscription (no API key), the most reliable fix is to
|
|
||||||
switch to a **Claude Code setup-token** or re-sync Claude Code CLI OAuth on the
|
|
||||||
**gateway host**.
|
|
||||||
|
|
||||||
**Recommended (setup-token):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run on the gateway host (runs Claude Code CLI)
|
|
||||||
clawdbot models auth setup-token --provider anthropic
|
|
||||||
clawdbot models status
|
|
||||||
```
|
|
||||||
|
|
||||||
If you generated the token elsewhere:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot models auth paste-token --provider anthropic
|
|
||||||
clawdbot models status
|
|
||||||
```
|
|
||||||
|
|
||||||
**If you want to keep OAuth reuse:**
|
|
||||||
log in with Claude Code CLI on the gateway host, then run `clawdbot models status`
|
|
||||||
to sync the refreshed token into Clawdbot’s auth store.
|
|
||||||
|
|
||||||
More detail: [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
|
|
||||||
|
|
||||||
### Control UI fails on HTTP ("device identity required" / "connect failed")
|
### Control UI fails on HTTP ("device identity required" / "connect failed")
|
||||||
|
|
||||||
If you open the dashboard over plain HTTP (e.g. `http://<lan-ip>:18789/` or
|
If you open the dashboard over plain HTTP (e.g. `http://<lan-ip>:18789/` or
|
||||||
|
|||||||
+12
-1679
File diff suppressed because it is too large.
Load diff
@@ -1,340 +0,0 @@
|
|||||||
---
|
|
||||||
title: Fly.io
|
|
||||||
description: Deploy Clawdbot on Fly.io
|
|
||||||
---
|
|
||||||
|
|
||||||
# Fly.io Deployment
|
|
||||||
|
|
||||||
**Goal:** Clawdbot Gateway running on a [Fly.io](https://fly.io) machine with persistent storage, automatic HTTPS, and Discord/channel access.
|
|
||||||
|
|
||||||
## What you need
|
|
||||||
|
|
||||||
- [flyctl CLI](https://fly.io/docs/hands-on/install-flyctl/) installed
|
|
||||||
- Fly.io account (free tier works)
|
|
||||||
- Model auth: Anthropic API key (or other provider keys)
|
|
||||||
- Channel credentials: Discord bot token, Telegram token, etc.
|
|
||||||
|
|
||||||
## Beginner quick path
|
|
||||||
|
|
||||||
1. Clone repo → customize `fly.toml`
|
|
||||||
2. Create app + volume → set secrets
|
|
||||||
3. Deploy with `fly deploy`
|
|
||||||
4. SSH in to create config or use Control UI
|
|
||||||
|
|
||||||
## 1) Create the Fly app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repo
|
|
||||||
git clone https://github.com/clawdbot/clawdbot.git
|
|
||||||
cd clawdbot
|
|
||||||
|
|
||||||
# Create a new Fly app (pick your own name)
|
|
||||||
fly apps create my-clawdbot
|
|
||||||
|
|
||||||
# Create a persistent volume (1GB is usually enough)
|
|
||||||
fly volumes create clawdbot_data --size 1 --region iad
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose).
|
|
||||||
|
|
||||||
## 2) Configure fly.toml
|
|
||||||
|
|
||||||
Edit `fly.toml` to match your app name and requirements:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
app = "my-clawdbot" # Your app name
|
|
||||||
primary_region = "iad"
|
|
||||||
|
|
||||||
[build]
|
|
||||||
dockerfile = "Dockerfile"
|
|
||||||
|
|
||||||
[env]
|
|
||||||
NODE_ENV = "production"
|
|
||||||
CLAWDBOT_PREFER_PNPM = "1"
|
|
||||||
CLAWDBOT_STATE_DIR = "/data"
|
|
||||||
NODE_OPTIONS = "--max-old-space-size=1536"
|
|
||||||
|
|
||||||
[processes]
|
|
||||||
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
|
|
||||||
|
|
||||||
[http_service]
|
|
||||||
internal_port = 3000
|
|
||||||
force_https = true
|
|
||||||
auto_stop_machines = false
|
|
||||||
auto_start_machines = true
|
|
||||||
min_machines_running = 1
|
|
||||||
processes = ["app"]
|
|
||||||
|
|
||||||
[[vm]]
|
|
||||||
size = "shared-cpu-2x"
|
|
||||||
memory = "2048mb"
|
|
||||||
|
|
||||||
[mounts]
|
|
||||||
source = "clawdbot_data"
|
|
||||||
destination = "/data"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key settings:**
|
|
||||||
|
|
||||||
| Setting | Why |
|
|
||||||
|---------|-----|
|
|
||||||
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
|
|
||||||
| `--allow-unconfigured` | Starts without a config file (you'll create one after) |
|
|
||||||
| `memory = "2048mb"` | 512MB is too small; 2GB recommended |
|
|
||||||
| `CLAWDBOT_STATE_DIR = "/data"` | Persists state on the volume |
|
|
||||||
|
|
||||||
## 3) Set secrets
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Required: Gateway token (for non-loopback binding)
|
|
||||||
fly secrets set CLAWDBOT_GATEWAY_TOKEN=$(openssl rand -hex 32)
|
|
||||||
|
|
||||||
# Model provider API keys
|
|
||||||
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
|
|
||||||
|
|
||||||
# Optional: Other providers
|
|
||||||
fly secrets set OPENAI_API_KEY=sk-...
|
|
||||||
fly secrets set GOOGLE_API_KEY=...
|
|
||||||
|
|
||||||
# Channel tokens
|
|
||||||
fly secrets set DISCORD_BOT_TOKEN=MTQ...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Notes:**
|
|
||||||
- Non-loopback binds (`--bind lan`) require `CLAWDBOT_GATEWAY_TOKEN` for security.
|
|
||||||
- Treat these tokens like passwords.
|
|
||||||
|
|
||||||
## 4) Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
fly deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
|
|
||||||
|
|
||||||
After deployment, verify:
|
|
||||||
```bash
|
|
||||||
fly status
|
|
||||||
fly logs
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see:
|
|
||||||
```
|
|
||||||
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
|
|
||||||
[discord] logged in to discord as xxx
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5) Create config file
|
|
||||||
|
|
||||||
SSH into the machine to create a proper config:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
fly ssh console
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the config directory and file:
|
|
||||||
```bash
|
|
||||||
mkdir -p /data
|
|
||||||
cat > /data/clawdbot.json << 'EOF'
|
|
||||||
{
|
|
||||||
"agents": {
|
|
||||||
"defaults": {
|
|
||||||
"model": {
|
|
||||||
"primary": "anthropic/claude-opus-4-5",
|
|
||||||
"fallbacks": ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"]
|
|
||||||
},
|
|
||||||
"maxConcurrent": 4
|
|
||||||
},
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"default": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"profiles": {
|
|
||||||
"anthropic:default": { "mode": "token", "provider": "anthropic" },
|
|
||||||
"openai:default": { "mode": "token", "provider": "openai" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bindings": [
|
|
||||||
{
|
|
||||||
"agentId": "main",
|
|
||||||
"match": { "channel": "discord" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"channels": {
|
|
||||||
"discord": {
|
|
||||||
"enabled": true,
|
|
||||||
"groupPolicy": "allowlist",
|
|
||||||
"guilds": {
|
|
||||||
"YOUR_GUILD_ID": {
|
|
||||||
"channels": { "general": { "allow": true } },
|
|
||||||
"requireMention": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gateway": {
|
|
||||||
"mode": "local",
|
|
||||||
"bind": "auto"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"lastTouchedVersion": "2026.1.23"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** With `CLAWDBOT_STATE_DIR=/data`, the config path is `/data/clawdbot.json`.
|
|
||||||
|
|
||||||
**Note:** The Discord token can come from either:
|
|
||||||
- Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets)
|
|
||||||
- Config file: `channels.discord.token`
|
|
||||||
|
|
||||||
If using env var, no need to add token to config. The gateway reads `DISCORD_BOT_TOKEN` automatically.
|
|
||||||
|
|
||||||
Restart to apply:
|
|
||||||
```bash
|
|
||||||
exit
|
|
||||||
fly machine restart <machine-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6) Access the Gateway
|
|
||||||
|
|
||||||
### Control UI
|
|
||||||
|
|
||||||
Open in browser:
|
|
||||||
```bash
|
|
||||||
fly open
|
|
||||||
```
|
|
||||||
|
|
||||||
Or visit `https://my-clawdbot.fly.dev/`
|
|
||||||
|
|
||||||
Paste your gateway token (the one from `CLAWDBOT_GATEWAY_TOKEN`) to authenticate.
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
fly logs # Live logs
|
|
||||||
fly logs --no-tail # Recent logs
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSH Console
|
|
||||||
|
|
||||||
```bash
|
|
||||||
fly ssh console
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "App is not listening on expected address"
|
|
||||||
|
|
||||||
The gateway is binding to `127.0.0.1` instead of `0.0.0.0`.
|
|
||||||
|
|
||||||
**Fix:** Add `--bind lan` to your process command in `fly.toml`.
|
|
||||||
|
|
||||||
### OOM / Memory Issues
|
|
||||||
|
|
||||||
Container keeps restarting or getting killed. Signs: `SIGABRT`, `v8::internal::Runtime_AllocateInYoungGeneration`, or silent restarts.
|
|
||||||
|
|
||||||
**Fix:** Increase memory in `fly.toml`:
|
|
||||||
```toml
|
|
||||||
[[vm]]
|
|
||||||
memory = "2048mb"
|
|
||||||
```
|
|
||||||
|
|
||||||
Or update an existing machine:
|
|
||||||
```bash
|
|
||||||
fly machine update <machine-id> --vm-memory 2048 -y
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** 512MB is too small. 1GB may work but can OOM under load or with verbose logging. **2GB is recommended.**
|
|
||||||
|
|
||||||
### Gateway Lock Issues
|
|
||||||
|
|
||||||
Gateway refuses to start with "already running" errors.
|
|
||||||
|
|
||||||
This happens when the container restarts but the PID lock file persists on the volume.
|
|
||||||
|
|
||||||
**Fix:** Delete the lock file:
|
|
||||||
```bash
|
|
||||||
fly ssh console --command "rm -f /data/gateway.*.lock"
|
|
||||||
fly machine restart <machine-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
The lock file is at `/data/gateway.*.lock` (not in a subdirectory).
|
|
||||||
|
|
||||||
### Config Not Being Read
|
|
||||||
|
|
||||||
If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/.clawdbot/clawdbot.json` should be read on restart.
|
|
||||||
|
|
||||||
Verify the config exists:
|
|
||||||
```bash
|
|
||||||
fly ssh console --command "cat /data/.clawdbot/clawdbot.json"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Writing Config via SSH
|
|
||||||
|
|
||||||
The `fly ssh console -C` command doesn't support shell redirection. To write a config file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use echo + tee (pipe from local to remote)
|
|
||||||
echo '{"your":"config"}' | fly ssh console -C "tee /data/.clawdbot/clawdbot.json"
|
|
||||||
|
|
||||||
# Or use sftp
|
|
||||||
fly sftp shell
|
|
||||||
> put /local/path/config.json /data/.clawdbot/clawdbot.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** `fly sftp` may fail if the file already exists. Delete first:
|
|
||||||
```bash
|
|
||||||
fly ssh console --command "rm /data/.clawdbot/clawdbot.json"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Updates
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull latest changes
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Redeploy
|
|
||||||
fly deploy
|
|
||||||
|
|
||||||
# Check health
|
|
||||||
fly status
|
|
||||||
fly logs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Updating Machine Command
|
|
||||||
|
|
||||||
If you need to change the startup command without a full redeploy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get machine ID
|
|
||||||
fly machines list
|
|
||||||
|
|
||||||
# Update command
|
|
||||||
fly machine update <machine-id> --command "node dist/index.js gateway --port 3000 --bind lan" -y
|
|
||||||
|
|
||||||
# Or with memory increase
|
|
||||||
fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.js gateway --port 3000 --bind lan" -y
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** After `fly deploy`, the machine command may reset to what's in `fly.toml`. If you made manual changes, re-apply them after deploy.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Fly.io uses **x86 architecture** (not ARM)
|
|
||||||
- The Dockerfile is compatible with both architectures
|
|
||||||
- For WhatsApp/Telegram onboarding, use `fly ssh console`
|
|
||||||
- Persistent data lives on the volume at `/data`
|
|
||||||
|
|
||||||
## Cost
|
|
||||||
|
|
||||||
With the recommended config (`shared-cpu-2x`, 2GB RAM):
|
|
||||||
- ~$10-15/month depending on usage
|
|
||||||
- Free tier includes some allowance
|
|
||||||
|
|
||||||
See [Fly.io pricing](https://fly.io/docs/about/pricing/) for details.
|
|
||||||
@@ -184,7 +184,7 @@ services:
|
|||||||
[
|
[
|
||||||
"node",
|
"node",
|
||||||
"dist/index.js",
|
"dist/index.js",
|
||||||
"gateway",
|
"gateway-daemon",
|
||||||
"--bind",
|
"--bind",
|
||||||
"${CLAWDBOT_GATEWAY_BIND}",
|
"${CLAWDBOT_GATEWAY_BIND}",
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
|||||||
|
|
||||||
## VPS & hosting
|
## VPS & hosting
|
||||||
|
|
||||||
- Fly.io: [Fly.io](/platforms/fly)
|
|
||||||
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
||||||
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ Plugins can register:
|
|||||||
- Background services
|
- Background services
|
||||||
- Optional config validation
|
- Optional config validation
|
||||||
- **Skills** (by listing `skills` directories in the plugin manifest)
|
- **Skills** (by listing `skills` directories in the plugin manifest)
|
||||||
- **Auto-reply commands** (execute without invoking the AI agent)
|
|
||||||
|
|
||||||
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
||||||
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
||||||
@@ -495,65 +494,6 @@ export default function (api) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Register auto-reply commands
|
|
||||||
|
|
||||||
Plugins can register custom slash commands that execute **without invoking the
|
|
||||||
AI agent**. This is useful for toggle commands, status checks, or quick actions
|
|
||||||
that don't need LLM processing.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export default function (api) {
|
|
||||||
api.registerCommand({
|
|
||||||
name: "mystatus",
|
|
||||||
description: "Show plugin status",
|
|
||||||
handler: (ctx) => ({
|
|
||||||
text: `Plugin is running! Channel: ${ctx.channel}`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Command handler context:
|
|
||||||
|
|
||||||
- `senderId`: The sender's ID (if available)
|
|
||||||
- `channel`: The channel where the command was sent
|
|
||||||
- `isAuthorizedSender`: Whether the sender is an authorized user
|
|
||||||
- `args`: Arguments passed after the command (if `acceptsArgs: true`)
|
|
||||||
- `commandBody`: The full command text
|
|
||||||
- `config`: The current Clawdbot config
|
|
||||||
|
|
||||||
Command options:
|
|
||||||
|
|
||||||
- `name`: Command name (without the leading `/`)
|
|
||||||
- `description`: Help text shown in command lists
|
|
||||||
- `acceptsArgs`: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlers
|
|
||||||
- `requireAuth`: Whether to require authorized sender (default: true)
|
|
||||||
- `handler`: Function that returns `{ text: string }` (can be async)
|
|
||||||
|
|
||||||
Example with authorization and arguments:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
api.registerCommand({
|
|
||||||
name: "setmode",
|
|
||||||
description: "Set plugin mode",
|
|
||||||
acceptsArgs: true,
|
|
||||||
requireAuth: true,
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const mode = ctx.args?.trim() || "default";
|
|
||||||
await saveMode(mode);
|
|
||||||
return { text: `Mode set to: ${mode}` };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Plugin commands are processed **before** built-in commands and the AI agent
|
|
||||||
- Commands are registered globally and work across all channels
|
|
||||||
- Command names are case-insensitive (`/MyStatus` matches `/mystatus`)
|
|
||||||
- Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
|
|
||||||
- Reserved command names (like `help`, `status`, `reset`, etc.) cannot be overridden by plugins
|
|
||||||
- Duplicate command registration across plugins will fail with a diagnostic error
|
|
||||||
|
|
||||||
### Register background services
|
### Register background services
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ clawdbot onboard --auth-choice claude-cli
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Generate the setup-token with `claude setup-token` and paste it, or run `clawdbot models auth setup-token` on the gateway host.
|
- Generate the setup-token with `claude setup-token` and paste it, or run `clawdbot models auth setup-token` on the gateway host.
|
||||||
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token or resync Claude Code CLI OAuth on the gateway host. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription).
|
|
||||||
- Clawdbot writes `auth.profiles["anthropic:claude-cli"].mode` as `"oauth"` so the profile
|
- 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
|
accepts both OAuth and setup-token credentials. Older configs using `"token"` are
|
||||||
auto-migrated on load.
|
auto-migrated on load.
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
title: Outbound Session Mirroring Refactor (Issue #1520)
|
|
||||||
description: Track outbound session mirroring refactor notes, decisions, tests, and open items.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Outbound Session Mirroring Refactor (Issue #1520)
|
|
||||||
|
|
||||||
## Status
|
|
||||||
- In progress.
|
|
||||||
- Core + plugin channel routing updated for outbound mirroring.
|
|
||||||
- Gateway send now derives target session when sessionKey is omitted.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
Outbound sends were mirrored into the *current* agent session (tool session key) rather than the target channel session. Inbound routing uses channel/peer session keys, so outbound responses landed in the wrong session and first-contact targets often lacked session entries.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
- Mirror outbound messages into the target channel session key.
|
|
||||||
- Create session entries on outbound when missing.
|
|
||||||
- Keep thread/topic scoping aligned with inbound session keys.
|
|
||||||
- Cover core channels plus bundled extensions.
|
|
||||||
|
|
||||||
## Implementation Summary
|
|
||||||
- New outbound session routing helper:
|
|
||||||
- `src/infra/outbound/outbound-session.ts`
|
|
||||||
- `resolveOutboundSessionRoute` builds target sessionKey using `buildAgentSessionKey` (dmScope + identityLinks).
|
|
||||||
- `ensureOutboundSessionEntry` writes minimal `MsgContext` via `recordSessionMetaFromInbound`.
|
|
||||||
- `runMessageAction` (send) derives target sessionKey and passes it to `executeSendAction` for mirroring.
|
|
||||||
- `message-tool` no longer mirrors directly; it only resolves agentId from the current session key.
|
|
||||||
- Plugin send path mirrors via `appendAssistantMessageToSessionTranscript` using the derived sessionKey.
|
|
||||||
- Gateway send derives a target session key when none is provided (default agent), and ensures a session entry.
|
|
||||||
|
|
||||||
## Thread/Topic Handling
|
|
||||||
- Slack: replyTo/threadId -> `resolveThreadSessionKeys` (suffix).
|
|
||||||
- Discord: threadId/replyTo -> `resolveThreadSessionKeys` with `useSuffix=false` to match inbound (thread channel id already scopes session).
|
|
||||||
- Telegram: topic IDs map to `chatId:topic:<id>` via `buildTelegramGroupPeerId`.
|
|
||||||
|
|
||||||
## Extensions Covered
|
|
||||||
- Matrix, MS Teams, Mattermost, BlueBubbles, Nextcloud Talk, Zalo, Zalo Personal, Nostr, Tlon.
|
|
||||||
- Notes:
|
|
||||||
- Mattermost targets now strip `@` for DM session key routing.
|
|
||||||
- Zalo Personal uses DM peer kind for 1:1 targets (group only when `group:` is present).
|
|
||||||
- BlueBubbles group targets strip `chat_*` prefixes to match inbound session keys.
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
- **Gateway send session derivation**: if `sessionKey` is provided, use it. If omitted, derive a sessionKey from target + default agent and mirror there.
|
|
||||||
- **Session entry creation**: always use `recordSessionMetaFromInbound` with `Provider/From/To/ChatType/AccountId/Originating*` aligned to inbound formats.
|
|
||||||
- **Target normalization**: outbound routing uses resolved targets (post `resolveChannelTarget`) when available.
|
|
||||||
- **Session key casing**: canonicalize session keys to lowercase on write and during migrations.
|
|
||||||
|
|
||||||
## Tests Added/Updated
|
|
||||||
- `src/infra/outbound/outbound-session.test.ts`
|
|
||||||
- Slack thread session key.
|
|
||||||
- Telegram topic session key.
|
|
||||||
- dmScope identityLinks with Discord.
|
|
||||||
- `src/agents/tools/message-tool.test.ts`
|
|
||||||
- Derives agentId from session key (no sessionKey passed through).
|
|
||||||
- `src/gateway/server-methods/send.test.ts`
|
|
||||||
- Derives session key when omitted and creates session entry.
|
|
||||||
|
|
||||||
## Open Items / Follow-ups
|
|
||||||
- Voice-call plugin uses custom `voice:<phone>` session keys. Outbound mapping is not standardized here; if message-tool should support voice-call sends, add explicit mapping.
|
|
||||||
- Confirm if any external plugin uses non-standard `From/To` formats beyond the bundled set.
|
|
||||||
|
|
||||||
## Files Touched
|
|
||||||
- `src/infra/outbound/outbound-session.ts`
|
|
||||||
- `src/infra/outbound/outbound-send-service.ts`
|
|
||||||
- `src/infra/outbound/message-action-runner.ts`
|
|
||||||
- `src/agents/tools/message-tool.ts`
|
|
||||||
- `src/gateway/server-methods/send.ts`
|
|
||||||
- Tests in:
|
|
||||||
- `src/infra/outbound/outbound-session.test.ts`
|
|
||||||
- `src/agents/tools/message-tool.test.ts`
|
|
||||||
- `src/gateway/server-methods/send.test.ts`
|
|
||||||
@@ -92,21 +92,6 @@ In group chats where you receive every message, be **smart about when to contrib
|
|||||||
|
|
||||||
Participate, don't dominate.
|
Participate, don't dominate.
|
||||||
|
|
||||||
### 😊 React Like a Human!
|
|
||||||
On platforms that support reactions (Discord, Slack), use emoji reactions naturally:
|
|
||||||
|
|
||||||
**React when:**
|
|
||||||
- You appreciate something but don't need to reply (👍, ❤️, 🙌)
|
|
||||||
- Something made you laugh (😂, 💀)
|
|
||||||
- You find it interesting or thought-provoking (🤔, 💡)
|
|
||||||
- You want to acknowledge without interrupting the flow
|
|
||||||
- It's a simple yes/no or approval situation (✅, 👀)
|
|
||||||
|
|
||||||
**Why it matters:**
|
|
||||||
Reactions are lightweight social signals. Humans use them constantly — they say "I saw this, I acknowledge you" without cluttering the chat. You should too.
|
|
||||||
|
|
||||||
**Don't overdo it:** One reaction per message max. Pick the one that fits best.
|
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
Skills provide your tools. When you need one, check its `SKILL.md`. Keep local notes (camera names, SSH details, voice preferences) in `TOOLS.md`.
|
Skills provide your tools. When you need one, check its `SKILL.md`. Keep local notes (camera names, SSH details, voice preferences) in `TOOLS.md`.
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ read_when:
|
|||||||
---
|
---
|
||||||
# HEARTBEAT.md
|
# HEARTBEAT.md
|
||||||
|
|
||||||
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
Keep this file empty unless you want a tiny checklist. Keep it small.
|
||||||
# Add tasks below when you want the agent to check something periodically.
|
|
||||||
@@ -7,16 +7,11 @@ read_when:
|
|||||||
|
|
||||||
*Fill this in during your first conversation. Make it yours.*
|
*Fill this in during your first conversation. Make it yours.*
|
||||||
|
|
||||||
- **Name:**
|
- **Name:** *(pick something you like)*
|
||||||
*(pick something you like)*
|
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||||
- **Creature:**
|
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
||||||
*(AI? robot? familiar? ghost in the machine? something weirder?)*
|
- **Emoji:** *(your signature — pick one that feels right)*
|
||||||
- **Vibe:**
|
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
|
||||||
*(how do you come across? sharp? warm? chaotic? calm?)*
|
|
||||||
- **Emoji:**
|
|
||||||
*(your signature — pick one that feels right)*
|
|
||||||
- **Avatar:**
|
|
||||||
*(workspace-relative path, http(s) URL, or data URI)*
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ Implementation:
|
|||||||
|
|
||||||
**OpenAI / OpenAI Codex**
|
**OpenAI / OpenAI Codex**
|
||||||
- Image sanitization only.
|
- Image sanitization only.
|
||||||
- On model switch into OpenAI Responses/Codex, drop orphaned reasoning signatures (standalone reasoning items without a following content block).
|
|
||||||
- No tool call id sanitization.
|
- No tool call id sanitization.
|
||||||
- No tool result pairing repair.
|
- No tool result pairing repair.
|
||||||
- No turn validation or reordering.
|
- No turn validation or reordering.
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ By default, Clawdbot runs a heartbeat every 30 minutes with the prompt:
|
|||||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||||
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||||
|
|
||||||
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
|
||||||
- If the file is missing, the heartbeat still runs and the model decides what to do.
|
|
||||||
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
||||||
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
||||||
|
|
||||||
|
|||||||
+1648
File diff suppressed because it is too large.
Load diff
@@ -166,19 +166,6 @@ Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting
|
|||||||
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
||||||
tokens instead of committing them to config files.
|
tokens instead of committing them to config files.
|
||||||
|
|
||||||
### Node browser proxy (zero-config default)
|
|
||||||
|
|
||||||
If you run a **node host** on the machine that has your browser, Clawdbot can
|
|
||||||
auto-route browser tool calls to that node without any custom `controlUrl`
|
|
||||||
setup. This is the default path for remote gateways.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- The node host exposes its local browser control server via a **proxy command**.
|
|
||||||
- Profiles come from the node’s own `browser.profiles` config (same as local).
|
|
||||||
- Disable if you don’t want it:
|
|
||||||
- On the node: `nodeHost.browserProxy.enabled=false`
|
|
||||||
- On the gateway: `gateway.nodes.browser.mode="off"`
|
|
||||||
|
|
||||||
### Browserless (hosted remote CDP)
|
### Browserless (hosted remote CDP)
|
||||||
|
|
||||||
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ Exec approvals are the **companion app / node host guardrail** for letting a san
|
|||||||
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
||||||
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
||||||
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
||||||
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used.
|
|
||||||
|
|
||||||
If the companion app UI is **not available**, any request that requires a prompt is
|
If the companion app UI is **not available**, any request that requires a prompt is
|
||||||
resolved by the **ask fallback** (default: deny).
|
resolved by the **ask fallback** (default: deny).
|
||||||
|
|||||||
+3
-9
@@ -22,11 +22,6 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Matching is case-insensitive.
|
|
||||||
- `*` wildcards are supported (`"*"` means all tools).
|
|
||||||
- If `tools.allow` only references unknown or unloaded plugin tool names, Clawdbot logs a warning and ignores the allowlist so core tools stay available.
|
|
||||||
|
|
||||||
## Tool profiles (base allowlist)
|
## Tool profiles (base allowlist)
|
||||||
|
|
||||||
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
||||||
@@ -161,7 +156,6 @@ alongside tools (for example, the voice-call plugin).
|
|||||||
|
|
||||||
Optional plugin tools:
|
Optional plugin tools:
|
||||||
- [Lobster](/tools/lobster): typed workflow runtime with resumable approvals (requires the Lobster CLI on the gateway host).
|
- [Lobster](/tools/lobster): typed workflow runtime with resumable approvals (requires the Lobster CLI on the gateway host).
|
||||||
- [LLM Task](/tools/llm-task): JSON-only LLM step for structured workflow output (optional schema validation).
|
|
||||||
|
|
||||||
## Tool inventory
|
## Tool inventory
|
||||||
|
|
||||||
@@ -379,10 +373,10 @@ List sessions, inspect transcript history, or send to another session.
|
|||||||
|
|
||||||
Core parameters:
|
Core parameters:
|
||||||
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
|
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
|
||||||
- `sessions_history`: `sessionKey` (or `sessionId`), `limit?`, `includeTools?`
|
- `sessions_history`: `sessionKey`, `limit?`, `includeTools?`
|
||||||
- `sessions_send`: `sessionKey` (or `sessionId`), `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
- `sessions_send`: `sessionKey`, `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
||||||
- `sessions_spawn`: `task`, `label?`, `agentId?`, `model?`, `runTimeoutSeconds?`, `cleanup?`
|
- `sessions_spawn`: `task`, `label?`, `agentId?`, `model?`, `runTimeoutSeconds?`, `cleanup?`
|
||||||
- `session_status`: `sessionKey?` (default current; accepts `sessionId`), `model?` (`default` clears override)
|
- `session_status`: `sessionKey?` (default current), `model?` (`default` clears override)
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `main` is the canonical direct-chat key; global/unknown are hidden.
|
- `main` is the canonical direct-chat key; global/unknown are hidden.
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "JSON-only LLM tasks for workflows (optional plugin tool)"
|
|
||||||
read_when:
|
|
||||||
- You want a JSON-only LLM step inside workflows
|
|
||||||
- You need schema-validated LLM output for automation
|
|
||||||
---
|
|
||||||
|
|
||||||
# LLM Task
|
|
||||||
|
|
||||||
`llm-task` is an **optional plugin tool** that runs a JSON-only LLM task and
|
|
||||||
returns structured output (optionally validated against JSON Schema).
|
|
||||||
|
|
||||||
This is ideal for workflow engines like Lobster: you can add a single LLM step
|
|
||||||
without writing custom Clawdbot code for each workflow.
|
|
||||||
|
|
||||||
## Enable the plugin
|
|
||||||
|
|
||||||
1) Enable the plugin:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"entries": {
|
|
||||||
"llm-task": { "enabled": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2) Allowlist the tool (it is registered with `optional: true`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"agents": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"tools": { "allow": ["llm-task"] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Config (optional)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"entries": {
|
|
||||||
"llm-task": {
|
|
||||||
"enabled": true,
|
|
||||||
"config": {
|
|
||||||
"defaultProvider": "openai-codex",
|
|
||||||
"defaultModel": "gpt-5.2",
|
|
||||||
"defaultAuthProfileId": "main",
|
|
||||||
"allowedModels": ["openai-codex/gpt-5.2"],
|
|
||||||
"maxTokens": 800,
|
|
||||||
"timeoutMs": 30000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`allowedModels` is an allowlist of `provider/model` strings. If set, any request
|
|
||||||
outside the list is rejected.
|
|
||||||
|
|
||||||
## Tool parameters
|
|
||||||
|
|
||||||
- `prompt` (string, required)
|
|
||||||
- `input` (any, optional)
|
|
||||||
- `schema` (object, optional JSON Schema)
|
|
||||||
- `provider` (string, optional)
|
|
||||||
- `model` (string, optional)
|
|
||||||
- `authProfileId` (string, optional)
|
|
||||||
- `temperature` (number, optional)
|
|
||||||
- `maxTokens` (number, optional)
|
|
||||||
- `timeoutMs` (number, optional)
|
|
||||||
|
|
||||||
## Output
|
|
||||||
|
|
||||||
Returns `details.json` containing the parsed JSON (and validates against
|
|
||||||
`schema` when provided).
|
|
||||||
|
|
||||||
## Example: Lobster workflow step
|
|
||||||
|
|
||||||
```lobster
|
|
||||||
clawd.invoke --tool llm-task --action json --args-json '{
|
|
||||||
"prompt": "Given the input email, return intent and draft.",
|
|
||||||
"input": {
|
|
||||||
"subject": "Hello",
|
|
||||||
"body": "Can you help?"
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"intent": { "type": "string" },
|
|
||||||
"draft": { "type": "string" }
|
|
||||||
},
|
|
||||||
"required": ["intent", "draft"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Safety notes
|
|
||||||
|
|
||||||
- The tool is **JSON-only** and instructs the model to output only JSON (no
|
|
||||||
code fences, no commentary).
|
|
||||||
- No tools are exposed to the model for this run.
|
|
||||||
- Treat output as untrusted unless you validate with `schema`.
|
|
||||||
- Put approvals before any side-effecting step (send, post, exec).
|
|
||||||
@@ -65,52 +65,6 @@ gog.gmail.search --query 'newer_than:1d' \
|
|||||||
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
|
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## JSON-only LLM steps (llm-task)
|
|
||||||
|
|
||||||
For workflows that need a **structured LLM step**, enable the optional
|
|
||||||
`llm-task` plugin tool and call it from Lobster. This keeps the workflow
|
|
||||||
deterministic while still letting you classify/summarize/draft with a model.
|
|
||||||
|
|
||||||
Enable the tool:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"entries": {
|
|
||||||
"llm-task": { "enabled": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"tools": { "allow": ["llm-task"] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use it in a pipeline:
|
|
||||||
|
|
||||||
```lobster
|
|
||||||
clawd.invoke --tool llm-task --action json --args-json '{
|
|
||||||
"prompt": "Given the input email, return intent and draft.",
|
|
||||||
"input": { "subject": "Hello", "body": "Can you help?" },
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"intent": { "type": "string" },
|
|
||||||
"draft": { "type": "string" }
|
|
||||||
},
|
|
||||||
"required": ["intent", "draft"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
See [LLM Task](/tools/llm-task) for details and configuration options.
|
|
||||||
|
|
||||||
## Workflow files (.lobster)
|
## Workflow files (.lobster)
|
||||||
|
|
||||||
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
|
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ Text + native (when enabled):
|
|||||||
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
|
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
|
||||||
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
|
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
|
||||||
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
||||||
- `/tts on|off|status|provider|limit|summary|audio` (control TTS; see [/tts](/tts))
|
|
||||||
- Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works.
|
|
||||||
- `/stop`
|
- `/stop`
|
||||||
- `/restart`
|
- `/restart`
|
||||||
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ current limits and pricing.
|
|||||||
|
|
||||||
**Environment alternative:** set `BRAVE_API_KEY` in the Gateway process
|
**Environment alternative:** set `BRAVE_API_KEY` in the Gateway process
|
||||||
environment. For a gateway install, put it in `~/.clawdbot/.env` (or your
|
environment. For a gateway install, put it in `~/.clawdbot/.env` (or your
|
||||||
service environment). See [Env vars](/help/faq#how-does-clawdbot-load-environment-variables).
|
service environment). See [Env vars](/start/faq#how-does-clawdbot-load-environment-variables).
|
||||||
|
|
||||||
## Using Perplexity (direct or via OpenRouter)
|
## Using Perplexity (direct or via OpenRouter)
|
||||||
|
|
||||||
|
|||||||
-296
@@ -1,296 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "Text-to-speech (TTS) for outbound replies"
|
|
||||||
read_when:
|
|
||||||
- Enabling text-to-speech for replies
|
|
||||||
- Configuring TTS providers or limits
|
|
||||||
- Using /tts commands
|
|
||||||
---
|
|
||||||
|
|
||||||
# Text-to-speech (TTS)
|
|
||||||
|
|
||||||
Clawdbot can convert outbound replies into audio using ElevenLabs or OpenAI.
|
|
||||||
It works anywhere Clawdbot can send audio; Telegram gets a round voice-note bubble.
|
|
||||||
|
|
||||||
## Supported services
|
|
||||||
|
|
||||||
- **ElevenLabs** (primary or fallback provider)
|
|
||||||
- **OpenAI** (primary or fallback provider; also used for summaries)
|
|
||||||
|
|
||||||
## Required keys
|
|
||||||
|
|
||||||
At least one of:
|
|
||||||
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
|
|
||||||
- `OPENAI_API_KEY`
|
|
||||||
|
|
||||||
If both are configured, the selected provider is used first and the other is a fallback.
|
|
||||||
Auto-summary uses the configured `summaryModel` (or `agents.defaults.model.primary`),
|
|
||||||
so that provider must also be authenticated if you enable summaries.
|
|
||||||
|
|
||||||
## Service links
|
|
||||||
|
|
||||||
- [OpenAI Text-to-Speech guide](https://platform.openai.com/docs/guides/text-to-speech)
|
|
||||||
- [OpenAI Audio API reference](https://platform.openai.com/docs/api-reference/audio)
|
|
||||||
- [ElevenLabs Text to Speech](https://elevenlabs.io/docs/api-reference/text-to-speech)
|
|
||||||
- [ElevenLabs Authentication](https://elevenlabs.io/docs/api-reference/authentication)
|
|
||||||
|
|
||||||
## Is it enabled by default?
|
|
||||||
|
|
||||||
No. TTS is **disabled** by default. Enable it in config or with `/tts on`,
|
|
||||||
which writes a local preference override.
|
|
||||||
|
|
||||||
## Config
|
|
||||||
|
|
||||||
TTS config lives under `messages.tts` in `clawdbot.json`.
|
|
||||||
Full schema is in [Gateway configuration](/gateway/configuration).
|
|
||||||
|
|
||||||
### Minimal config (enable + provider)
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
tts: {
|
|
||||||
enabled: true,
|
|
||||||
provider: "elevenlabs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenAI primary with ElevenLabs fallback
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
tts: {
|
|
||||||
enabled: true,
|
|
||||||
provider: "openai",
|
|
||||||
summaryModel: "openai/gpt-4.1-mini",
|
|
||||||
modelOverrides: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
openai: {
|
|
||||||
apiKey: "openai_api_key",
|
|
||||||
model: "gpt-4o-mini-tts",
|
|
||||||
voice: "alloy"
|
|
||||||
},
|
|
||||||
elevenlabs: {
|
|
||||||
apiKey: "elevenlabs_api_key",
|
|
||||||
baseUrl: "https://api.elevenlabs.io",
|
|
||||||
voiceId: "voice_id",
|
|
||||||
modelId: "eleven_multilingual_v2",
|
|
||||||
seed: 42,
|
|
||||||
applyTextNormalization: "auto",
|
|
||||||
languageCode: "en",
|
|
||||||
voiceSettings: {
|
|
||||||
stability: 0.5,
|
|
||||||
similarityBoost: 0.75,
|
|
||||||
style: 0.0,
|
|
||||||
useSpeakerBoost: true,
|
|
||||||
speed: 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom limits + prefs path
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
tts: {
|
|
||||||
enabled: true,
|
|
||||||
maxTextLength: 4000,
|
|
||||||
timeoutMs: 30000,
|
|
||||||
prefsPath: "~/.clawdbot/settings/tts.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Disable auto-summary for long replies
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
tts: {
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```
|
|
||||||
/tts summary off
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notes on fields
|
|
||||||
|
|
||||||
- `enabled`: master toggle (default `false`; local prefs can override).
|
|
||||||
- `mode`: `"final"` (default) or `"all"` (includes tool/block replies).
|
|
||||||
- `provider`: `"elevenlabs"` or `"openai"` (fallback is automatic).
|
|
||||||
- `summaryModel`: optional cheap model for auto-summary; defaults to `agents.defaults.model.primary`.
|
|
||||||
- Accepts `provider/model` or a configured model alias.
|
|
||||||
- `modelOverrides`: allow the model to emit TTS directives (on by default).
|
|
||||||
- `maxTextLength`: hard cap for TTS input (chars). `/tts audio` fails if exceeded.
|
|
||||||
- `timeoutMs`: request timeout (ms).
|
|
||||||
- `prefsPath`: override the local prefs JSON path.
|
|
||||||
- `apiKey` values fall back to env vars (`ELEVENLABS_API_KEY`/`XI_API_KEY`, `OPENAI_API_KEY`).
|
|
||||||
- `elevenlabs.baseUrl`: override ElevenLabs API base URL.
|
|
||||||
- `elevenlabs.voiceSettings`:
|
|
||||||
- `stability`, `similarityBoost`, `style`: `0..1`
|
|
||||||
- `useSpeakerBoost`: `true|false`
|
|
||||||
- `speed`: `0.5..2.0` (1.0 = normal)
|
|
||||||
- `elevenlabs.applyTextNormalization`: `auto|on|off`
|
|
||||||
- `elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
|
|
||||||
- `elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
|
|
||||||
|
|
||||||
## Model-driven overrides (default on)
|
|
||||||
|
|
||||||
By default, the model **can** emit TTS directives for a single reply.
|
|
||||||
|
|
||||||
When enabled, the model can emit `[[tts:...]]` directives to override the voice
|
|
||||||
for a single reply, plus an optional `[[tts:text]]...[[/tts:text]]` block to
|
|
||||||
provide expressive tags (laughter, singing cues, etc) that should only appear in
|
|
||||||
the audio.
|
|
||||||
|
|
||||||
Example reply payload:
|
|
||||||
|
|
||||||
```
|
|
||||||
Here you go.
|
|
||||||
|
|
||||||
[[tts:provider=elevenlabs voiceId=pMsXgVXv3BLzUgSXRplE model=eleven_v3 speed=1.1]]
|
|
||||||
[[tts:text]](laughs) Read the song once more.[[/tts:text]]
|
|
||||||
```
|
|
||||||
|
|
||||||
Available directive keys (when enabled):
|
|
||||||
- `provider` (`openai` | `elevenlabs`)
|
|
||||||
- `voice` (OpenAI voice) or `voiceId` (ElevenLabs)
|
|
||||||
- `model` (OpenAI TTS model or ElevenLabs model id)
|
|
||||||
- `stability`, `similarityBoost`, `style`, `speed`, `useSpeakerBoost`
|
|
||||||
- `applyTextNormalization` (`auto|on|off`)
|
|
||||||
- `languageCode` (ISO 639-1)
|
|
||||||
- `seed`
|
|
||||||
|
|
||||||
Disable all model overrides:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
tts: {
|
|
||||||
modelOverrides: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional allowlist (disable specific overrides while keeping tags enabled):
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
tts: {
|
|
||||||
modelOverrides: {
|
|
||||||
enabled: true,
|
|
||||||
allowProvider: false,
|
|
||||||
allowSeed: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Per-user preferences
|
|
||||||
|
|
||||||
Slash commands write local overrides to `prefsPath` (default:
|
|
||||||
`~/.clawdbot/settings/tts.json`, override with `CLAWDBOT_TTS_PREFS` or
|
|
||||||
`messages.tts.prefsPath`).
|
|
||||||
|
|
||||||
Stored fields:
|
|
||||||
- `enabled`
|
|
||||||
- `provider`
|
|
||||||
- `maxLength` (summary threshold; default 1500 chars)
|
|
||||||
- `summarize` (default `true`)
|
|
||||||
|
|
||||||
These override `messages.tts.*` for that host.
|
|
||||||
|
|
||||||
## Output formats (fixed)
|
|
||||||
|
|
||||||
- **Telegram**: Opus voice note (`opus_48000_64` from ElevenLabs, `opus` from OpenAI).
|
|
||||||
- 48kHz / 64kbps is a good voice-note tradeoff and required for the round bubble.
|
|
||||||
- **Other channels**: MP3 (`mp3_44100_128` from ElevenLabs, `mp3` from OpenAI).
|
|
||||||
- 44.1kHz / 128kbps is the default balance for speech clarity.
|
|
||||||
|
|
||||||
This is not configurable; Telegram expects Opus for voice-note UX.
|
|
||||||
|
|
||||||
## Auto-TTS behavior
|
|
||||||
|
|
||||||
When enabled, Clawdbot:
|
|
||||||
- skips TTS if the reply already contains media or a `MEDIA:` directive.
|
|
||||||
- skips very short replies (< 10 chars).
|
|
||||||
- summarizes long replies when enabled using `agents.defaults.model.primary` (or `summaryModel`).
|
|
||||||
- attaches the generated audio to the reply.
|
|
||||||
|
|
||||||
If the reply exceeds `maxLength` and summary is off (or no API key for the
|
|
||||||
summary model), audio
|
|
||||||
is skipped and the normal text reply is sent.
|
|
||||||
|
|
||||||
## Flow diagram
|
|
||||||
|
|
||||||
```
|
|
||||||
Reply -> TTS enabled?
|
|
||||||
no -> send text
|
|
||||||
yes -> has media / MEDIA: / short?
|
|
||||||
yes -> send text
|
|
||||||
no -> length > limit?
|
|
||||||
no -> TTS -> attach audio
|
|
||||||
yes -> summary enabled?
|
|
||||||
no -> send text
|
|
||||||
yes -> summarize (summaryModel or agents.defaults.model.primary)
|
|
||||||
-> TTS -> attach audio
|
|
||||||
```
|
|
||||||
|
|
||||||
## Slash command usage
|
|
||||||
|
|
||||||
There is a single command: `/tts`.
|
|
||||||
See [Slash commands](/tools/slash-commands) for enablement details.
|
|
||||||
|
|
||||||
Discord note: `/tts` is a built-in Discord command, so Clawdbot registers
|
|
||||||
`/voice` as the native command there. Text `/tts ...` still works.
|
|
||||||
|
|
||||||
```
|
|
||||||
/tts on
|
|
||||||
/tts off
|
|
||||||
/tts status
|
|
||||||
/tts provider openai
|
|
||||||
/tts limit 2000
|
|
||||||
/tts summary off
|
|
||||||
/tts audio Hello from Clawdbot
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Commands require an authorized sender (allowlist/owner rules still apply).
|
|
||||||
- `commands.text` or native command registration must be enabled.
|
|
||||||
- `limit` and `summary` are stored in local prefs, not the main config.
|
|
||||||
- `/tts audio` generates a one-off audio reply (does not toggle TTS on).
|
|
||||||
|
|
||||||
## Agent tool
|
|
||||||
|
|
||||||
The `tts` tool converts text to speech and returns a `MEDIA:` path. When the
|
|
||||||
result is Telegram-compatible, the tool includes `[[audio_as_voice]]` so
|
|
||||||
Telegram sends a voice bubble.
|
|
||||||
|
|
||||||
## Gateway RPC
|
|
||||||
|
|
||||||
Gateway methods:
|
|
||||||
- `tts.status`
|
|
||||||
- `tts.enable`
|
|
||||||
- `tts.disable`
|
|
||||||
- `tts.convert`
|
|
||||||
- `tts.setProvider`
|
|
||||||
- `tts.providers`
|
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
normalizeAccountId,
|
normalizeAccountId,
|
||||||
PAIRING_APPROVED_MESSAGE,
|
PAIRING_APPROVED_MESSAGE,
|
||||||
resolveBlueBubblesGroupRequireMention,
|
resolveBlueBubblesGroupRequireMention,
|
||||||
resolveBlueBubblesGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
||||||
resolveToolPolicy: resolveBlueBubblesGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -21,7 +21,6 @@ const bluebubblesActionSchema = z
|
|||||||
|
|
||||||
const bluebubblesGroupConfigSchema = z.object({
|
const bluebubblesGroupConfigSchema = z.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const bluebubblesAccountSchema = z.object({
|
const bluebubblesAccountSchema = z.object({
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export type GroupPolicy = "open" | "disabled" | "allowlist";
|
|||||||
export type BlueBubblesGroupConfig = {
|
export type BlueBubblesGroupConfig = {
|
||||||
/** If true, only respond in this group when mentioned. */
|
/** If true, only respond in this group when mentioned. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this group. */
|
|
||||||
tools?: { allow?: string[]; deny?: string[] };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BlueBubblesAccountConfig = {
|
export type BlueBubblesAccountConfig = {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
resolveDiscordAccount,
|
resolveDiscordAccount,
|
||||||
resolveDefaultDiscordAccountId,
|
resolveDefaultDiscordAccountId,
|
||||||
resolveDiscordGroupRequireMention,
|
resolveDiscordGroupRequireMention,
|
||||||
resolveDiscordGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
type ChannelMessageActionAdapter,
|
type ChannelMessageActionAdapter,
|
||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
@@ -145,7 +144,6 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveDiscordGroupRequireMention,
|
resolveRequireMention: resolveDiscordGroupRequireMention,
|
||||||
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
stripPatterns: () => ["<@!?\\d+>"],
|
stripPatterns: () => ["<@!?\\d+>"],
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
resolveDefaultIMessageAccountId,
|
resolveDefaultIMessageAccountId,
|
||||||
resolveIMessageAccount,
|
resolveIMessageAccount,
|
||||||
resolveIMessageGroupRequireMention,
|
resolveIMessageGroupRequireMention,
|
||||||
resolveIMessageGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
type ResolvedIMessageAccount,
|
type ResolvedIMessageAccount,
|
||||||
@@ -107,7 +106,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||||
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
messaging: {
|
messaging: {
|
||||||
targetResolver: {
|
targetResolver: {
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
# LLM Task (plugin)
|
|
||||||
|
|
||||||
Adds an **optional** agent tool `llm-task` for running **JSON-only** LLM tasks
|
|
||||||
(drafting, summarizing, classifying) with optional JSON Schema validation.
|
|
||||||
|
|
||||||
Designed to be called from workflow engines (for example, Lobster via
|
|
||||||
`clawd.invoke --each`) without adding new Clawdbot code per workflow.
|
|
||||||
|
|
||||||
## Enable
|
|
||||||
|
|
||||||
1) Enable the plugin:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"entries": {
|
|
||||||
"llm-task": { "enabled": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2) Allowlist the tool (it is registered with `optional: true`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"agents": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"tools": { "allow": ["llm-task"] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Config (optional)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"entries": {
|
|
||||||
"llm-task": {
|
|
||||||
"enabled": true,
|
|
||||||
"config": {
|
|
||||||
"defaultProvider": "openai-codex",
|
|
||||||
"defaultModel": "gpt-5.2",
|
|
||||||
"defaultAuthProfileId": "main",
|
|
||||||
"allowedModels": ["openai-codex/gpt-5.2"],
|
|
||||||
"maxTokens": 800,
|
|
||||||
"timeoutMs": 30000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`allowedModels` is an allowlist of `provider/model` strings. If set, any request
|
|
||||||
outside the list is rejected.
|
|
||||||
|
|
||||||
## Tool API
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
- `prompt` (string, required)
|
|
||||||
- `input` (any, optional)
|
|
||||||
- `schema` (object, optional JSON Schema)
|
|
||||||
- `provider` (string, optional)
|
|
||||||
- `model` (string, optional)
|
|
||||||
- `authProfileId` (string, optional)
|
|
||||||
- `temperature` (number, optional)
|
|
||||||
- `maxTokens` (number, optional)
|
|
||||||
- `timeoutMs` (number, optional)
|
|
||||||
|
|
||||||
### Output
|
|
||||||
|
|
||||||
Returns `details.json` containing the parsed JSON (and validates against
|
|
||||||
`schema` when provided).
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The tool is **JSON-only** and instructs the model to output only JSON
|
|
||||||
(no code fences, no commentary).
|
|
||||||
- No tools are exposed to the model for this run.
|
|
||||||
- Side effects should be handled outside this tool (for example, approvals in
|
|
||||||
Lobster) before calling tools that send messages/emails.
|
|
||||||
|
|
||||||
## Bundled extension note
|
|
||||||
|
|
||||||
This extension depends on Clawdbot internal modules (the embedded agent runner).
|
|
||||||
It is intended to ship as a **bundled** Clawdbot extension (like `lobster`) and
|
|
||||||
be enabled via `plugins.entries` + tool allowlists.
|
|
||||||
|
|
||||||
It is **not** currently designed to be copied into
|
|
||||||
`~/.clawdbot/extensions` as a standalone plugin directory.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "llm-task",
|
|
||||||
"name": "LLM Task",
|
|
||||||
"description": "Generic JSON-only LLM tool for structured tasks callable from workflows.",
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"defaultProvider": { "type": "string" },
|
|
||||||
"defaultModel": { "type": "string" },
|
|
||||||
"defaultAuthProfileId": { "type": "string" },
|
|
||||||
"allowedModels": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "type": "string" },
|
|
||||||
"description": "Allowlist of provider/model keys like openai-codex/gpt-5.2."
|
|
||||||
},
|
|
||||||
"maxTokens": { "type": "number" },
|
|
||||||
"timeoutMs": { "type": "number" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
|
|
||||||
|
|
||||||
import { createLlmTaskTool } from "./src/llm-task-tool.js";
|
|
||||||
|
|
||||||
export default function register(api: ClawdbotPluginApi) {
|
|
||||||
api.registerTool(createLlmTaskTool(api), { optional: true });
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@clawdbot/llm-task",
|
|
||||||
"version": "2026.1.23",
|
|
||||||
"type": "module",
|
|
||||||
"description": "Clawdbot JSON-only LLM task plugin",
|
|
||||||
"clawdbot": {
|
|
||||||
"extensions": [
|
|
||||||
"./index.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
|
|
||||||
vi.mock("../../../src/agents/pi-embedded-runner.js", () => {
|
|
||||||
return {
|
|
||||||
runEmbeddedPiAgent: vi.fn(async () => ({
|
|
||||||
meta: { startedAt: Date.now() },
|
|
||||||
payloads: [{ text: "{}" }],
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import { runEmbeddedPiAgent } from "../../../src/agents/pi-embedded-runner.js";
|
|
||||||
import { createLlmTaskTool } from "./llm-task-tool.js";
|
|
||||||
|
|
||||||
function fakeApi(overrides: any = {}) {
|
|
||||||
return {
|
|
||||||
id: "llm-task",
|
|
||||||
name: "llm-task",
|
|
||||||
source: "test",
|
|
||||||
config: { agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } } },
|
|
||||||
pluginConfig: {},
|
|
||||||
runtime: { version: "test" },
|
|
||||||
logger: { debug() {}, info() {}, warn() {}, error() {} },
|
|
||||||
registerTool() {},
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("llm-task tool (json-only)", () => {
|
|
||||||
beforeEach(() => vi.clearAllMocks());
|
|
||||||
|
|
||||||
it("returns parsed json", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
||||||
meta: {},
|
|
||||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
|
||||||
});
|
|
||||||
const tool = createLlmTaskTool(fakeApi() as any);
|
|
||||||
const res = await tool.execute("id", { prompt: "return foo" });
|
|
||||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("strips fenced json", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
||||||
meta: {},
|
|
||||||
payloads: [{ text: "```json\n{\"ok\":true}\n```" }],
|
|
||||||
});
|
|
||||||
const tool = createLlmTaskTool(fakeApi() as any);
|
|
||||||
const res = await tool.execute("id", { prompt: "return ok" });
|
|
||||||
expect((res as any).details.json).toEqual({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("validates schema", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
||||||
meta: {},
|
|
||||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
|
||||||
});
|
|
||||||
const tool = createLlmTaskTool(fakeApi() as any);
|
|
||||||
const schema = {
|
|
||||||
type: "object",
|
|
||||||
properties: { foo: { type: "string" } },
|
|
||||||
required: ["foo"],
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
const res = await tool.execute("id", { prompt: "return foo", schema });
|
|
||||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws on invalid json", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({ meta: {}, payloads: [{ text: "not-json" }] });
|
|
||||||
const tool = createLlmTaskTool(fakeApi() as any);
|
|
||||||
await expect(tool.execute("id", { prompt: "x" })).rejects.toThrow(/invalid json/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws on schema mismatch", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
||||||
meta: {},
|
|
||||||
payloads: [{ text: JSON.stringify({ foo: 1 }) }],
|
|
||||||
});
|
|
||||||
const tool = createLlmTaskTool(fakeApi() as any);
|
|
||||||
const schema = { type: "object", properties: { foo: { type: "string" } }, required: ["foo"] };
|
|
||||||
await expect(tool.execute("id", { prompt: "x", schema })).rejects.toThrow(/match schema/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("passes provider/model overrides to embedded runner", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
||||||
meta: {},
|
|
||||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
|
||||||
});
|
|
||||||
const tool = createLlmTaskTool(fakeApi() as any);
|
|
||||||
await tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" });
|
|
||||||
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
|
||||||
expect(call.provider).toBe("anthropic");
|
|
||||||
expect(call.model).toBe("claude-4-sonnet");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("enforces allowedModels", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
||||||
meta: {},
|
|
||||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
|
||||||
});
|
|
||||||
const tool = createLlmTaskTool(fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }) as any);
|
|
||||||
await expect(tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" })).rejects.toThrow(
|
|
||||||
/not allowed/i,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables tools for embedded run", async () => {
|
|
||||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
||||||
meta: {},
|
|
||||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
|
||||||
});
|
|
||||||
const tool = createLlmTaskTool(fakeApi() as any);
|
|
||||||
await tool.execute("id", { prompt: "x" });
|
|
||||||
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
|
||||||
expect(call.disableTools).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
|
|
||||||
import Ajv from "ajv";
|
|
||||||
import { Type } from "@sinclair/typebox";
|
|
||||||
|
|
||||||
// NOTE: This extension is intended to be bundled with Clawdbot.
|
|
||||||
// When running from source (tests/dev), Clawdbot internals live under src/.
|
|
||||||
// When running from a built install, internals live under dist/ (no src/ tree).
|
|
||||||
// So we resolve internal imports dynamically with src-first, dist-fallback.
|
|
||||||
|
|
||||||
import type { ClawdbotPluginApi } from "../../../src/plugins/types.js";
|
|
||||||
|
|
||||||
type RunEmbeddedPiAgentFn = (params: Record<string, unknown>) => Promise<unknown>;
|
|
||||||
|
|
||||||
async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
|
|
||||||
// Source checkout (tests/dev)
|
|
||||||
try {
|
|
||||||
const mod = await import("../../../src/agents/pi-embedded-runner.js");
|
|
||||||
if (typeof (mod as any).runEmbeddedPiAgent === "function") return (mod as any).runEmbeddedPiAgent;
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bundled install (built)
|
|
||||||
const mod = await import("../../../agents/pi-embedded-runner.js");
|
|
||||||
if (typeof (mod as any).runEmbeddedPiAgent !== "function") {
|
|
||||||
throw new Error("Internal error: runEmbeddedPiAgent not available");
|
|
||||||
}
|
|
||||||
return (mod as any).runEmbeddedPiAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripCodeFences(s: string): string {
|
|
||||||
const trimmed = s.trim();
|
|
||||||
const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
||||||
if (m) return (m[1] ?? "").trim();
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectText(payloads: Array<{ text?: string; isError?: boolean }> | undefined): string {
|
|
||||||
const texts = (payloads ?? [])
|
|
||||||
.filter((p) => !p.isError && typeof p.text === "string")
|
|
||||||
.map((p) => p.text ?? "");
|
|
||||||
return texts.join("\n").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toModelKey(provider?: string, model?: string): string | undefined {
|
|
||||||
const p = provider?.trim();
|
|
||||||
const m = model?.trim();
|
|
||||||
if (!p || !m) return undefined;
|
|
||||||
return `${p}/${m}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PluginCfg = {
|
|
||||||
defaultProvider?: string;
|
|
||||||
defaultModel?: string;
|
|
||||||
defaultAuthProfileId?: string;
|
|
||||||
allowedModels?: string[];
|
|
||||||
maxTokens?: number;
|
|
||||||
timeoutMs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createLlmTaskTool(api: ClawdbotPluginApi) {
|
|
||||||
return {
|
|
||||||
name: "llm-task",
|
|
||||||
description:
|
|
||||||
"Run a generic JSON-only LLM task and return schema-validated JSON. Designed for orchestration from Lobster workflows via clawd.invoke.",
|
|
||||||
parameters: Type.Object({
|
|
||||||
prompt: Type.String({ description: "Task instruction for the LLM." }),
|
|
||||||
input: Type.Optional(Type.Unknown({ description: "Optional input payload for the task." })),
|
|
||||||
schema: Type.Optional(Type.Unknown({ description: "Optional JSON Schema to validate the returned JSON." })),
|
|
||||||
provider: Type.Optional(Type.String({ description: "Provider override (e.g. openai-codex, anthropic)." })),
|
|
||||||
model: Type.Optional(Type.String({ description: "Model id override." })),
|
|
||||||
authProfileId: Type.Optional(Type.String({ description: "Auth profile override." })),
|
|
||||||
temperature: Type.Optional(Type.Number({ description: "Best-effort temperature override." })),
|
|
||||||
maxTokens: Type.Optional(Type.Number({ description: "Best-effort maxTokens override." })),
|
|
||||||
timeoutMs: Type.Optional(Type.Number({ description: "Timeout for the LLM run." })),
|
|
||||||
}),
|
|
||||||
|
|
||||||
async execute(_id: string, params: Record<string, unknown>) {
|
|
||||||
const prompt = String(params.prompt ?? "");
|
|
||||||
if (!prompt.trim()) throw new Error("prompt required");
|
|
||||||
|
|
||||||
const pluginCfg = (api.pluginConfig ?? {}) as PluginCfg;
|
|
||||||
|
|
||||||
const primary = api.config?.agents?.defaults?.model?.primary;
|
|
||||||
const primaryProvider = typeof primary === "string" ? primary.split("/")[0] : undefined;
|
|
||||||
const primaryModel = typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined;
|
|
||||||
|
|
||||||
const provider =
|
|
||||||
(typeof params.provider === "string" && params.provider.trim()) ||
|
|
||||||
(typeof pluginCfg.defaultProvider === "string" && pluginCfg.defaultProvider.trim()) ||
|
|
||||||
primaryProvider ||
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
const model =
|
|
||||||
(typeof params.model === "string" && params.model.trim()) ||
|
|
||||||
(typeof pluginCfg.defaultModel === "string" && pluginCfg.defaultModel.trim()) ||
|
|
||||||
primaryModel ||
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
const authProfileId =
|
|
||||||
(typeof (params as any).authProfileId === "string" && (params as any).authProfileId.trim()) ||
|
|
||||||
(typeof pluginCfg.defaultAuthProfileId === "string" && pluginCfg.defaultAuthProfileId.trim()) ||
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
const modelKey = toModelKey(provider, model);
|
|
||||||
if (!provider || !model || !modelKey) {
|
|
||||||
throw new Error(
|
|
||||||
`provider/model could not be resolved (provider=${String(provider ?? "")}, model=${String(model ?? "")})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowed = Array.isArray(pluginCfg.allowedModels) ? pluginCfg.allowedModels : undefined;
|
|
||||||
if (allowed && allowed.length > 0 && !allowed.includes(modelKey)) {
|
|
||||||
throw new Error(
|
|
||||||
`Model not allowed by llm-task plugin config: ${modelKey}. Allowed models: ${allowed.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeoutMs =
|
|
||||||
(typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : undefined) ||
|
|
||||||
(typeof pluginCfg.timeoutMs === "number" && pluginCfg.timeoutMs > 0 ? pluginCfg.timeoutMs : undefined) ||
|
|
||||||
30_000;
|
|
||||||
|
|
||||||
const streamParams = {
|
|
||||||
temperature: typeof params.temperature === "number" ? params.temperature : undefined,
|
|
||||||
maxTokens:
|
|
||||||
typeof params.maxTokens === "number"
|
|
||||||
? params.maxTokens
|
|
||||||
: typeof pluginCfg.maxTokens === "number"
|
|
||||||
? pluginCfg.maxTokens
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const input = (params as any).input as unknown;
|
|
||||||
let inputJson: string;
|
|
||||||
try {
|
|
||||||
inputJson = JSON.stringify(input ?? null, null, 2);
|
|
||||||
} catch {
|
|
||||||
throw new Error("input must be JSON-serializable");
|
|
||||||
}
|
|
||||||
|
|
||||||
const system = [
|
|
||||||
"You are a JSON-only function.",
|
|
||||||
"Return ONLY a valid JSON value.",
|
|
||||||
"Do not wrap in markdown fences.",
|
|
||||||
"Do not include commentary.",
|
|
||||||
"Do not call tools.",
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
const fullPrompt = `${system}\n\nTASK:\n${prompt}\n\nINPUT_JSON:\n${inputJson}\n`;
|
|
||||||
|
|
||||||
let tmpDir: string | null = null;
|
|
||||||
try {
|
|
||||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-llm-task-"));
|
|
||||||
const sessionId = `llm-task-${Date.now()}`;
|
|
||||||
const sessionFile = path.join(tmpDir, "session.json");
|
|
||||||
|
|
||||||
const runEmbeddedPiAgent = await loadRunEmbeddedPiAgent();
|
|
||||||
|
|
||||||
const result = await runEmbeddedPiAgent({
|
|
||||||
sessionId,
|
|
||||||
sessionFile,
|
|
||||||
workspaceDir: api.config?.agents?.defaults?.workspace ?? process.cwd(),
|
|
||||||
config: api.config,
|
|
||||||
prompt: fullPrompt,
|
|
||||||
timeoutMs,
|
|
||||||
runId: `llm-task-${Date.now()}`,
|
|
||||||
provider,
|
|
||||||
model,
|
|
||||||
authProfileId,
|
|
||||||
authProfileIdSource: authProfileId ? "user" : "auto",
|
|
||||||
streamParams,
|
|
||||||
disableTools: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const text = collectText((result as any).payloads);
|
|
||||||
if (!text) throw new Error("LLM returned empty output");
|
|
||||||
|
|
||||||
const raw = stripCodeFences(text);
|
|
||||||
let parsed: unknown;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
throw new Error("LLM returned invalid JSON");
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = (params as any).schema as unknown;
|
|
||||||
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
|
||||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
||||||
const validate = ajv.compile(schema as any);
|
|
||||||
const ok = validate(parsed);
|
|
||||||
if (!ok) {
|
|
||||||
const msg =
|
|
||||||
validate.errors?.map((e) => `${e.instancePath || "<root>"} ${e.message || "invalid"}`).join("; ") ??
|
|
||||||
"invalid";
|
|
||||||
throw new Error(`LLM JSON did not match schema: ${msg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
|
||||||
details: { json: parsed, provider, model },
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
if (tmpDir) {
|
|
||||||
try {
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -30,48 +30,6 @@ Enable it in an agent allowlist:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using `clawd.invoke` (Lobster → Clawdbot tools)
|
|
||||||
|
|
||||||
Some Lobster pipelines may include a `clawd.invoke` step to call back into Clawdbot tools/plugins (for example: `gog` for Google Workspace, `gh` for GitHub, `message.send`, etc.).
|
|
||||||
|
|
||||||
For this to work, the Clawdbot Gateway must expose the tool bridge endpoint and the target tool must be allowed by policy:
|
|
||||||
|
|
||||||
- Clawdbot provides an HTTP endpoint: `POST /tools/invoke`.
|
|
||||||
- The request is gated by **gateway auth** (e.g. `Authorization: Bearer …` when token auth is enabled).
|
|
||||||
- The invoked tool is gated by **tool policy** (global + per-agent + provider + group policy). If the tool is not allowed, Clawdbot returns `404 Tool not available`.
|
|
||||||
|
|
||||||
### Allowlisting recommended
|
|
||||||
|
|
||||||
To avoid letting workflows call arbitrary tools, set a tight allowlist on the agent that will be used by `clawd.invoke`.
|
|
||||||
|
|
||||||
Example (allow only a small set of tools):
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"agents": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"tools": {
|
|
||||||
"allow": [
|
|
||||||
"lobster",
|
|
||||||
"web_fetch",
|
|
||||||
"web_search",
|
|
||||||
"gog",
|
|
||||||
"gh"
|
|
||||||
],
|
|
||||||
"deny": ["gateway"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- If `tools.allow` is omitted or empty, it behaves like "allow everything (except denied)". For a real allowlist, set a **non-empty** `allow`.
|
|
||||||
- Tool names depend on which plugins you have installed/enabled.
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
- Runs the `lobster` executable as a local subprocess.
|
- Runs the `lobster` executable as a local subprocess.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"markdown-it": "14.1.0",
|
"markdown-it": "14.1.0",
|
||||||
"matrix-bot-sdk": "0.8.0",
|
"matrix-bot-sdk": "0.8.0",
|
||||||
"music-metadata": "^11.10.6",
|
"music-metadata": "^11.10.6",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clawdbot": "workspace:*"
|
"clawdbot": "workspace:*"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
import { matrixMessageActions } from "./actions.js";
|
import { matrixMessageActions } from "./actions.js";
|
||||||
import { MatrixConfigSchema } from "./config-schema.js";
|
import { MatrixConfigSchema } from "./config-schema.js";
|
||||||
import { resolveMatrixGroupRequireMention, resolveMatrixGroupToolPolicy } from "./group-mentions.js";
|
import { resolveMatrixGroupRequireMention } from "./group-mentions.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
import {
|
import {
|
||||||
listMatrixAccountIds,
|
listMatrixAccountIds,
|
||||||
@@ -167,7 +167,6 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveMatrixGroupRequireMention,
|
resolveRequireMention: resolveMatrixGroupRequireMention,
|
||||||
resolveToolPolicy: resolveMatrixGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) =>
|
resolveReplyToMode: ({ cfg }) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -26,7 +26,6 @@ const matrixRoomSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
autoReply: z.boolean().optional(),
|
autoReply: z.boolean().optional(),
|
||||||
users: z.array(allowFromEntry).optional(),
|
users: z.array(allowFromEntry).optional(),
|
||||||
skills: z.array(z.string()).optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
import type { ChannelGroupContext } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
@@ -32,30 +32,3 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMatrixGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const rawGroupId = params.groupId?.trim() ?? "";
|
|
||||||
let roomId = rawGroupId;
|
|
||||||
const lower = roomId.toLowerCase();
|
|
||||||
if (lower.startsWith("matrix:")) {
|
|
||||||
roomId = roomId.slice("matrix:".length).trim();
|
|
||||||
}
|
|
||||||
if (roomId.toLowerCase().startsWith("channel:")) {
|
|
||||||
roomId = roomId.slice("channel:".length).trim();
|
|
||||||
}
|
|
||||||
if (roomId.toLowerCase().startsWith("room:")) {
|
|
||||||
roomId = roomId.slice("room:".length).trim();
|
|
||||||
}
|
|
||||||
const groupChannel = params.groupChannel?.trim() ?? "";
|
|
||||||
const aliases = groupChannel ? [groupChannel] : [];
|
|
||||||
const cfg = params.cfg as CoreConfig;
|
|
||||||
const resolved = resolveMatrixRoomConfig({
|
|
||||||
rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
|
|
||||||
roomId,
|
|
||||||
aliases,
|
|
||||||
name: groupChannel || undefined,
|
|
||||||
}).config;
|
|
||||||
return resolved?.tools;
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,6 @@ export type MatrixRoomConfig = {
|
|||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
/** Require mentioning the bot to trigger replies. */
|
/** Require mentioning the bot to trigger replies. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this room. */
|
|
||||||
tools?: { allow?: string[]; deny?: string[] };
|
|
||||||
/** If true, reply without mention requirements. */
|
/** If true, reply without mention requirements. */
|
||||||
autoReply?: boolean;
|
autoReply?: boolean;
|
||||||
/** Optional allowlist for room senders (user IDs or localparts). */
|
/** Optional allowlist for room senders (user IDs or localparts). */
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
||||||
import { msteamsOutbound } from "./outbound.js";
|
import { msteamsOutbound } from "./outbound.js";
|
||||||
import { probeMSTeams } from "./probe.js";
|
import { probeMSTeams } from "./probe.js";
|
||||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
|
||||||
import {
|
import {
|
||||||
normalizeMSTeamsMessagingTarget,
|
normalizeMSTeamsMessagingTarget,
|
||||||
normalizeMSTeamsUserInput,
|
normalizeMSTeamsUserInput,
|
||||||
@@ -78,9 +77,6 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
|||||||
hasRepliedRef,
|
hasRepliedRef,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
groups: {
|
|
||||||
resolveToolPolicy: resolveMSTeamsGroupToolPolicy,
|
|
||||||
},
|
|
||||||
reload: { configPrefixes: ["channels.msteams"] },
|
reload: { configPrefixes: ["channels.msteams"] },
|
||||||
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AllowlistMatch,
|
AllowlistMatch,
|
||||||
ChannelGroupContext,
|
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
GroupToolPolicyConfig,
|
|
||||||
MSTeamsChannelConfig,
|
MSTeamsChannelConfig,
|
||||||
MSTeamsConfig,
|
MSTeamsConfig,
|
||||||
MSTeamsReplyStyle,
|
MSTeamsReplyStyle,
|
||||||
@@ -88,50 +86,6 @@ export function resolveMSTeamsRouteConfig(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMSTeamsGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const cfg = params.cfg.channels?.msteams;
|
|
||||||
if (!cfg) return undefined;
|
|
||||||
const groupId = params.groupId?.trim();
|
|
||||||
const groupChannel = params.groupChannel?.trim();
|
|
||||||
const groupSpace = params.groupSpace?.trim();
|
|
||||||
|
|
||||||
const resolved = resolveMSTeamsRouteConfig({
|
|
||||||
cfg,
|
|
||||||
teamId: groupSpace,
|
|
||||||
teamName: groupSpace,
|
|
||||||
conversationId: groupId,
|
|
||||||
channelName: groupChannel,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resolved.channelConfig) {
|
|
||||||
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools;
|
|
||||||
}
|
|
||||||
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
|
|
||||||
|
|
||||||
if (!groupId) return undefined;
|
|
||||||
|
|
||||||
const channelCandidates = buildChannelKeyCandidates(
|
|
||||||
groupId,
|
|
||||||
groupChannel,
|
|
||||||
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
|
||||||
);
|
|
||||||
for (const teamConfig of Object.values(cfg.teams ?? {})) {
|
|
||||||
const match = resolveChannelEntryMatchWithFallback({
|
|
||||||
entries: teamConfig?.channels ?? {},
|
|
||||||
keys: channelCandidates,
|
|
||||||
wildcardKey: "*",
|
|
||||||
normalizeKey: normalizeChannelSlug,
|
|
||||||
});
|
|
||||||
if (match.entry) {
|
|
||||||
return match.entry.tools ?? teamConfig?.tools;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MSTeamsReplyPolicy = {
|
export type MSTeamsReplyPolicy = {
|
||||||
requireMention: boolean;
|
requireMention: boolean;
|
||||||
replyStyle: MSTeamsReplyStyle;
|
replyStyle: MSTeamsReplyStyle;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
|
|||||||
try {
|
try {
|
||||||
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
||||||
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
|
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
|
||||||
await tokenProvider.getAccessToken("https://api.botframework.com");
|
await tokenProvider.getAccessToken("https://api.botframework.com/.default");
|
||||||
let graph:
|
let graph:
|
||||||
| {
|
| {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
|
|||||||
import { getNextcloudTalkRuntime } from "./runtime.js";
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
||||||
import { sendMessageNextcloudTalk } from "./send.js";
|
import { sendMessageNextcloudTalk } from "./send.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
id: "nextcloud-talk",
|
id: "nextcloud-talk",
|
||||||
@@ -160,7 +159,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
messaging: {
|
messaging: {
|
||||||
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
DmPolicySchema,
|
DmPolicySchema,
|
||||||
GroupPolicySchema,
|
GroupPolicySchema,
|
||||||
MarkdownConfigSchema,
|
MarkdownConfigSchema,
|
||||||
ToolPolicySchema,
|
|
||||||
requireOpenAllowFrom,
|
requireOpenAllowFrom,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -12,7 +11,6 @@ import { z } from "zod";
|
|||||||
export const NextcloudTalkRoomSchema = z
|
export const NextcloudTalkRoomSchema = z
|
||||||
.object({
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
skills: z.array(z.string()).optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allowFrom: z.array(z.string()).optional(),
|
allowFrom: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AllowlistMatch, ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
import type { AllowlistMatch, GroupPolicy } from "clawdbot/plugin-sdk";
|
||||||
import {
|
import {
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
normalizeChannelSlug,
|
normalizeChannelSlug,
|
||||||
@@ -86,21 +86,6 @@ export function resolveNextcloudTalkRoomMatch(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveNextcloudTalkGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const cfg = params.cfg as { channels?: { "nextcloud-talk"?: { rooms?: Record<string, NextcloudTalkRoomConfig> } } };
|
|
||||||
const roomToken = params.groupId?.trim();
|
|
||||||
if (!roomToken) return undefined;
|
|
||||||
const roomName = params.groupChannel?.trim() || undefined;
|
|
||||||
const match = resolveNextcloudTalkRoomMatch({
|
|
||||||
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
|
|
||||||
roomToken,
|
|
||||||
roomName,
|
|
||||||
});
|
|
||||||
return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveNextcloudTalkRequireMention(params: {
|
export function resolveNextcloudTalkRequireMention(params: {
|
||||||
roomConfig?: NextcloudTalkRoomConfig;
|
roomConfig?: NextcloudTalkRoomConfig;
|
||||||
wildcardConfig?: NextcloudTalkRoomConfig;
|
wildcardConfig?: NextcloudTalkRoomConfig;
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import type {
|
|||||||
|
|
||||||
export type NextcloudTalkRoomConfig = {
|
export type NextcloudTalkRoomConfig = {
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this room. */
|
|
||||||
tools?: { allow?: string[]; deny?: string[] };
|
|
||||||
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
|
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
/** If false, disable the bot for this room. */
|
/** If false, disable the bot for this room. */
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clawdbot": "workspace:*",
|
"clawdbot": "workspace:*",
|
||||||
"nostr-tools": "^2.20.0",
|
"nostr-tools": "^2.19.4",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
resolveSlackAccount,
|
resolveSlackAccount,
|
||||||
resolveSlackReplyToMode,
|
resolveSlackReplyToMode,
|
||||||
resolveSlackGroupRequireMention,
|
resolveSlackGroupRequireMention,
|
||||||
resolveSlackGroupToolPolicy,
|
|
||||||
buildSlackThreadingToolContext,
|
buildSlackThreadingToolContext,
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
slackOnboardingAdapter,
|
slackOnboardingAdapter,
|
||||||
@@ -162,7 +161,6 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveSlackGroupRequireMention,
|
resolveRequireMention: resolveSlackGroupRequireMention,
|
||||||
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
resolveDefaultTelegramAccountId,
|
resolveDefaultTelegramAccountId,
|
||||||
resolveTelegramAccount,
|
resolveTelegramAccount,
|
||||||
resolveTelegramGroupRequireMention,
|
resolveTelegramGroupRequireMention,
|
||||||
resolveTelegramGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
telegramOnboardingAdapter,
|
telegramOnboardingAdapter,
|
||||||
TelegramConfigSchema,
|
TelegramConfigSchema,
|
||||||
@@ -155,7 +154,6 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||||
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@urbit/aura": "^3.0.0",
|
"@urbit/aura": "^2.0.0",
|
||||||
"@urbit/http-api": "^3.0.0"
|
"@urbit/http-api": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sinclair/typebox": "0.34.47",
|
"@sinclair/typebox": "0.34.47",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
resolveDefaultWhatsAppAccountId,
|
resolveDefaultWhatsAppAccountId,
|
||||||
resolveWhatsAppAccount,
|
resolveWhatsAppAccount,
|
||||||
resolveWhatsAppGroupRequireMention,
|
resolveWhatsAppGroupRequireMention,
|
||||||
resolveWhatsAppGroupToolPolicy,
|
|
||||||
resolveWhatsAppHeartbeatRecipients,
|
resolveWhatsAppHeartbeatRecipients,
|
||||||
whatsappOnboardingAdapter,
|
whatsappOnboardingAdapter,
|
||||||
WhatsAppConfigSchema,
|
WhatsAppConfigSchema,
|
||||||
@@ -199,7 +198,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
||||||
resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
|
|
||||||
resolveGroupIntroHint: () =>
|
resolveGroupIntroHint: () =>
|
||||||
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import type {
|
|||||||
ChannelAccountSnapshot,
|
ChannelAccountSnapshot,
|
||||||
ChannelDirectoryEntry,
|
ChannelDirectoryEntry,
|
||||||
ChannelDock,
|
ChannelDock,
|
||||||
ChannelGroupContext,
|
|
||||||
ChannelPlugin,
|
ChannelPlugin,
|
||||||
ClawdbotConfig,
|
ClawdbotConfig,
|
||||||
GroupToolPolicyConfig,
|
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
import {
|
import {
|
||||||
applyAccountNameToChannelSection,
|
applyAccountNameToChannelSection,
|
||||||
@@ -81,26 +79,6 @@ function mapGroup(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveZalouserGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const account = resolveZalouserAccountSync({
|
|
||||||
cfg: params.cfg as ClawdbotConfig,
|
|
||||||
accountId: params.accountId ?? undefined,
|
|
||||||
});
|
|
||||||
const groups = account.config.groups ?? {};
|
|
||||||
const groupId = params.groupId?.trim();
|
|
||||||
const groupChannel = params.groupChannel?.trim();
|
|
||||||
const candidates = [groupId, groupChannel, "*"].filter(
|
|
||||||
(value): value is string => Boolean(value),
|
|
||||||
);
|
|
||||||
for (const key of candidates) {
|
|
||||||
const entry = groups[key];
|
|
||||||
if (entry?.tools) return entry.tools;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const zalouserDock: ChannelDock = {
|
export const zalouserDock: ChannelDock = {
|
||||||
id: "zalouser",
|
id: "zalouser",
|
||||||
capabilities: {
|
capabilities: {
|
||||||
@@ -123,7 +101,6 @@ export const zalouserDock: ChannelDock = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: () => true,
|
resolveRequireMention: () => true,
|
||||||
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: () => "off",
|
resolveReplyToMode: () => "off",
|
||||||
@@ -211,7 +188,6 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: () => true,
|
resolveRequireMention: () => true,
|
||||||
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: () => "off",
|
resolveReplyToMode: () => "off",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -6,7 +6,6 @@ const allowFromEntry = z.union([z.string(), z.number()]);
|
|||||||
const groupConfigSchema = z.object({
|
const groupConfigSchema = z.object({
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const zalouserAccountSchema = z.object({
|
const zalouserAccountSchema = z.object({
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export type ZalouserAccountConfig = {
|
|||||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||||
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||||
messagePrefix?: string;
|
messagePrefix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ export type ZalouserConfig = {
|
|||||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||||
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||||
messagePrefix?: string;
|
messagePrefix?: string;
|
||||||
accounts?: Record<string, ZalouserAccountConfig>;
|
accounts?: Record<string, ZalouserAccountConfig>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# Clawdbot Fly.io deployment configuration
|
|
||||||
# See https://fly.io/docs/reference/configuration/
|
|
||||||
|
|
||||||
app = "clawdbot"
|
|
||||||
primary_region = "iad" # change to your closest region
|
|
||||||
|
|
||||||
[build]
|
|
||||||
dockerfile = "Dockerfile"
|
|
||||||
|
|
||||||
[env]
|
|
||||||
NODE_ENV = "production"
|
|
||||||
# Fly uses x86, but keep this for consistency
|
|
||||||
CLAWDBOT_PREFER_PNPM = "1"
|
|
||||||
CLAWDBOT_STATE_DIR = "/data"
|
|
||||||
NODE_OPTIONS = "--max-old-space-size=1536"
|
|
||||||
|
|
||||||
[processes]
|
|
||||||
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
|
|
||||||
|
|
||||||
[http_service]
|
|
||||||
internal_port = 3000
|
|
||||||
force_https = true
|
|
||||||
auto_stop_machines = false # Keep running for persistent connections
|
|
||||||
auto_start_machines = true
|
|
||||||
min_machines_running = 1
|
|
||||||
processes = ["app"]
|
|
||||||
|
|
||||||
[[vm]]
|
|
||||||
size = "shared-cpu-2x"
|
|
||||||
memory = "2048mb"
|
|
||||||
|
|
||||||
[mounts]
|
|
||||||
source = "clawdbot_data"
|
|
||||||
destination = "/data"
|
|
||||||
+8
-9
@@ -146,8 +146,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.23.0",
|
"packageManager": "pnpm@10.23.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "0.13.1",
|
"@agentclientprotocol/sdk": "0.13.0",
|
||||||
"@aws-sdk/client-bedrock": "^3.975.0",
|
|
||||||
"@buape/carbon": "0.14.0",
|
"@buape/carbon": "0.14.0",
|
||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@grammyjs/runner": "^2.0.3",
|
"@grammyjs/runner": "^2.0.3",
|
||||||
@@ -186,7 +185,7 @@
|
|||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"osc-progress": "^0.3.0",
|
"osc-progress": "^0.3.0",
|
||||||
"pdfjs-dist": "^5.4.530",
|
"pdfjs-dist": "^5.4.530",
|
||||||
"playwright-core": "1.58.0",
|
"playwright-core": "1.57.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
@@ -196,7 +195,7 @@
|
|||||||
"undici": "^7.19.0",
|
"undici": "^7.19.0",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@napi-rs/canvas": "^0.1.88",
|
"@napi-rs/canvas": "^0.1.88",
|
||||||
@@ -214,21 +213,21 @@
|
|||||||
"@types/proper-lockfile": "^4.1.4",
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
"@types/qrcode-terminal": "^0.12.2",
|
"@types/qrcode-terminal": "^0.12.2",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20260124.1",
|
"@typescript/native-preview": "7.0.0-dev.20260120.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.17",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"lit": "^3.3.2",
|
"lit": "^3.3.2",
|
||||||
"lucide": "^0.563.0",
|
"lucide": "^0.562.0",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
"oxfmt": "0.26.0",
|
"oxfmt": "0.26.0",
|
||||||
"oxlint": "^1.41.0",
|
"oxlint": "^1.41.0",
|
||||||
"oxlint-tsgolint": "^0.11.1",
|
"oxlint-tsgolint": "^0.11.1",
|
||||||
"quicktype-core": "^23.2.6",
|
"quicktype-core": "^23.2.6",
|
||||||
"rolldown": "1.0.0-rc.1",
|
"rolldown": "1.0.0-beta.60",
|
||||||
"signal-utils": "^0.21.1",
|
"signal-utils": "^0.21.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^4.0.18",
|
"vitest": "^4.0.17",
|
||||||
"wireit": "^0.14.12"
|
"wireit": "^0.14.12"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
Generated
+570
-700
File diff suppressed because it is too large.
Load diff
@@ -6,13 +6,11 @@ WORKDIR /app
|
|||||||
|
|
||||||
ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
|
ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts vitest.e2e.config.ts ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY test ./test
|
|
||||||
COPY scripts ./scripts
|
COPY scripts ./scripts
|
||||||
COPY docs ./docs
|
COPY docs ./docs
|
||||||
COPY skills ./skills
|
COPY skills ./skills
|
||||||
COPY patches ./patches
|
|
||||||
COPY extensions/memory-core ./extensions/memory-core
|
COPY extensions/memory-core ./extensions/memory-core
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -29,23 +29,12 @@ const localWorkers = Math.max(4, Math.min(16, os.cpus().length));
|
|||||||
const perRunWorkers = Math.max(1, Math.floor(localWorkers / parallelRuns.length));
|
const perRunWorkers = Math.max(1, Math.floor(localWorkers / parallelRuns.length));
|
||||||
const maxWorkers = isCI ? null : resolvedOverride ?? perRunWorkers;
|
const maxWorkers = isCI ? null : resolvedOverride ?? perRunWorkers;
|
||||||
|
|
||||||
const WARNING_SUPPRESSION_FLAGS = [
|
|
||||||
"--disable-warning=ExperimentalWarning",
|
|
||||||
"--disable-warning=DEP0040",
|
|
||||||
"--disable-warning=DEP0060",
|
|
||||||
];
|
|
||||||
|
|
||||||
const run = (entry) =>
|
const run = (entry) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
const args = maxWorkers ? [...entry.args, "--maxWorkers", String(maxWorkers)] : entry.args;
|
const args = maxWorkers ? [...entry.args, "--maxWorkers", String(maxWorkers)] : entry.args;
|
||||||
const nodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
||||||
const nextNodeOptions = WARNING_SUPPRESSION_FLAGS.reduce(
|
|
||||||
(acc, flag) => (acc.includes(flag) ? acc : `${acc} ${flag}`.trim()),
|
|
||||||
nodeOptions,
|
|
||||||
);
|
|
||||||
const child = spawn(pnpm, args, {
|
const child = spawn(pnpm, args, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
env: { ...process.env, VITEST_GROUP: entry.name, NODE_OPTIONS: nextNodeOptions },
|
env: { ...process.env, VITEST_GROUP: entry.name },
|
||||||
shell: process.platform === "win32",
|
shell: process.platform === "win32",
|
||||||
});
|
});
|
||||||
children.add(child);
|
children.add(child);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: bird
|
name: bird
|
||||||
description: X/Twitter CLI for reading, searching, posting, and engagement via cookies.
|
description: X/Twitter CLI for reading, searching, posting, and engagement via cookies.
|
||||||
homepage: https://bird.fast
|
homepage: https://bird.fast
|
||||||
metadata: {"clawdbot":{"emoji":"🐦","requires":{"bins":["bird"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/bird","bins":["bird"],"label":"Install bird (brew)","os":["darwin"]},{"id":"npm","kind":"node","package":"@steipete/bird","bins":["bird"],"label":"Install bird (npm)"}]}}
|
metadata: {"clawdbot":{"emoji":"🐦","requires":{"bins":["bird"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/bird","bins":["bird"],"label":"Install bird (brew)"},{"id":"npm","kind":"node","package":"@steipete/bird","bins":["bird"],"label":"Install bird (npm)"}]}}
|
||||||
---
|
---
|
||||||
|
|
||||||
# bird 🐦
|
# bird 🐦
|
||||||
|
|||||||
@@ -70,8 +70,7 @@ export function resolveSessionAgentIds(params: { sessionKey?: string; config?: C
|
|||||||
} {
|
} {
|
||||||
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
|
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
|
||||||
const sessionKey = params.sessionKey?.trim();
|
const sessionKey = params.sessionKey?.trim();
|
||||||
const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined;
|
const parsed = sessionKey ? parseAgentSessionKey(sessionKey) : null;
|
||||||
const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null;
|
|
||||||
const sessionAgentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId;
|
const sessionAgentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId;
|
||||||
return { defaultAgentId, sessionAgentId };
|
return { defaultAgentId, sessionAgentId };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core";
|
|
||||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
||||||
|
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
|
||||||
import { parseBooleanValue } from "../utils/boolean.js";
|
|
||||||
import { resolveUserPath } from "../utils.js";
|
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
||||||
|
|
||||||
type PayloadLogStage = "request" | "usage";
|
|
||||||
|
|
||||||
type PayloadLogEvent = {
|
|
||||||
ts: string;
|
|
||||||
stage: PayloadLogStage;
|
|
||||||
runId?: string;
|
|
||||||
sessionId?: string;
|
|
||||||
sessionKey?: string;
|
|
||||||
provider?: string;
|
|
||||||
modelId?: string;
|
|
||||||
modelApi?: string | null;
|
|
||||||
workspaceDir?: string;
|
|
||||||
payload?: unknown;
|
|
||||||
usage?: Record<string, unknown>;
|
|
||||||
error?: string;
|
|
||||||
payloadDigest?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PayloadLogConfig = {
|
|
||||||
enabled: boolean;
|
|
||||||
filePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PayloadLogWriter = {
|
|
||||||
filePath: string;
|
|
||||||
write: (line: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const writers = new Map<string, PayloadLogWriter>();
|
|
||||||
const log = createSubsystemLogger("agent/anthropic-payload");
|
|
||||||
|
|
||||||
function resolvePayloadLogConfig(env: NodeJS.ProcessEnv): PayloadLogConfig {
|
|
||||||
const enabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG) ?? false;
|
|
||||||
const fileOverride = env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim();
|
|
||||||
const filePath = fileOverride
|
|
||||||
? resolveUserPath(fileOverride)
|
|
||||||
: path.join(resolveStateDir(env), "logs", "anthropic-payload.jsonl");
|
|
||||||
return { enabled, filePath };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWriter(filePath: string): PayloadLogWriter {
|
|
||||||
const existing = writers.get(filePath);
|
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
const dir = path.dirname(filePath);
|
|
||||||
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
|
|
||||||
let queue = Promise.resolve();
|
|
||||||
|
|
||||||
const writer: PayloadLogWriter = {
|
|
||||||
filePath,
|
|
||||||
write: (line: string) => {
|
|
||||||
queue = queue
|
|
||||||
.then(() => ready)
|
|
||||||
.then(() => fs.appendFile(filePath, line, "utf8"))
|
|
||||||
.catch(() => undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
writers.set(filePath, writer);
|
|
||||||
return writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeJsonStringify(value: unknown): string | null {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value, (_key, val) => {
|
|
||||||
if (typeof val === "bigint") return val.toString();
|
|
||||||
if (typeof val === "function") return "[Function]";
|
|
||||||
if (val instanceof Error) {
|
|
||||||
return { name: val.name, message: val.message, stack: val.stack };
|
|
||||||
}
|
|
||||||
if (val instanceof Uint8Array) {
|
|
||||||
return { type: "Uint8Array", data: Buffer.from(val).toString("base64") };
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatError(error: unknown): string | undefined {
|
|
||||||
if (error instanceof Error) return error.message;
|
|
||||||
if (typeof error === "string") return error;
|
|
||||||
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
|
|
||||||
return String(error);
|
|
||||||
}
|
|
||||||
if (error && typeof error === "object") {
|
|
||||||
return safeJsonStringify(error) ?? "unknown error";
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function digest(value: unknown): string | undefined {
|
|
||||||
const serialized = safeJsonStringify(value);
|
|
||||||
if (!serialized) return undefined;
|
|
||||||
return crypto.createHash("sha256").update(serialized).digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAnthropicModel(model: Model<Api> | undefined | null): boolean {
|
|
||||||
return (model as { api?: unknown })?.api === "anthropic-messages";
|
|
||||||
}
|
|
||||||
|
|
||||||
function findLastAssistantUsage(messages: AgentMessage[]): Record<string, unknown> | null {
|
|
||||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
||||||
const msg = messages[i] as { role?: unknown; usage?: unknown };
|
|
||||||
if (msg?.role === "assistant" && msg.usage && typeof msg.usage === "object") {
|
|
||||||
return msg.usage as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnthropicPayloadLogger = {
|
|
||||||
enabled: true;
|
|
||||||
wrapStreamFn: (streamFn: StreamFn) => StreamFn;
|
|
||||||
recordUsage: (messages: AgentMessage[], error?: unknown) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createAnthropicPayloadLogger(params: {
|
|
||||||
env?: NodeJS.ProcessEnv;
|
|
||||||
runId?: string;
|
|
||||||
sessionId?: string;
|
|
||||||
sessionKey?: string;
|
|
||||||
provider?: string;
|
|
||||||
modelId?: string;
|
|
||||||
modelApi?: string | null;
|
|
||||||
workspaceDir?: string;
|
|
||||||
}): AnthropicPayloadLogger | null {
|
|
||||||
const env = params.env ?? process.env;
|
|
||||||
const cfg = resolvePayloadLogConfig(env);
|
|
||||||
if (!cfg.enabled) return null;
|
|
||||||
|
|
||||||
const writer = getWriter(cfg.filePath);
|
|
||||||
const base: Omit<PayloadLogEvent, "ts" | "stage"> = {
|
|
||||||
runId: params.runId,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
sessionKey: params.sessionKey,
|
|
||||||
provider: params.provider,
|
|
||||||
modelId: params.modelId,
|
|
||||||
modelApi: params.modelApi,
|
|
||||||
workspaceDir: params.workspaceDir,
|
|
||||||
};
|
|
||||||
|
|
||||||
const record = (event: PayloadLogEvent) => {
|
|
||||||
const line = safeJsonStringify(event);
|
|
||||||
if (!line) return;
|
|
||||||
writer.write(`${line}\n`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapStreamFn: AnthropicPayloadLogger["wrapStreamFn"] = (streamFn) => {
|
|
||||||
const wrapped: StreamFn = (model, context, options) => {
|
|
||||||
if (!isAnthropicModel(model as Model<Api>)) {
|
|
||||||
return streamFn(model, context, options);
|
|
||||||
}
|
|
||||||
const nextOnPayload = (payload: unknown) => {
|
|
||||||
record({
|
|
||||||
...base,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
stage: "request",
|
|
||||||
payload,
|
|
||||||
payloadDigest: digest(payload),
|
|
||||||
});
|
|
||||||
options?.onPayload?.(payload);
|
|
||||||
};
|
|
||||||
return streamFn(model, context, {
|
|
||||||
...options,
|
|
||||||
onPayload: nextOnPayload,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return wrapped;
|
|
||||||
};
|
|
||||||
|
|
||||||
const recordUsage: AnthropicPayloadLogger["recordUsage"] = (messages, error) => {
|
|
||||||
const usage = findLastAssistantUsage(messages);
|
|
||||||
const errorMessage = formatError(error);
|
|
||||||
if (!usage) {
|
|
||||||
if (errorMessage) {
|
|
||||||
record({
|
|
||||||
...base,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
stage: "usage",
|
|
||||||
error: errorMessage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
record({
|
|
||||||
...base,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
stage: "usage",
|
|
||||||
usage,
|
|
||||||
error: errorMessage,
|
|
||||||
});
|
|
||||||
log.info("anthropic usage", {
|
|
||||||
runId: params.runId,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
usage,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
log.info("anthropic payload logger enabled", { filePath: writer.filePath });
|
|
||||||
return { enabled: true, wrapStreamFn, recordUsage };
|
|
||||||
}
|
|
||||||
@@ -129,25 +129,4 @@ describe("exec approvals", () => {
|
|||||||
expect(calls).toContain("node.invoke");
|
expect(calls).toContain("node.invoke");
|
||||||
expect(calls).not.toContain("exec.approval.request");
|
expect(calls).not.toContain("exec.approval.request");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honors ask=off for elevated gateway exec without prompting", async () => {
|
|
||||||
const { callGatewayTool } = await import("./tools/gateway.js");
|
|
||||||
const calls: string[] = [];
|
|
||||||
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
|
|
||||||
calls.push(method);
|
|
||||||
return { ok: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
const { createExecTool } = await import("./bash-tools.exec.js");
|
|
||||||
const tool = createExecTool({
|
|
||||||
ask: "off",
|
|
||||||
security: "full",
|
|
||||||
approvalRunningNoticeMs: 0,
|
|
||||||
elevated: { enabled: true, allowed: true, defaultLevel: "ask" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tool.execute("call3", { command: "echo ok", elevated: true });
|
|
||||||
expect(result.details.status).toBe("completed");
|
|
||||||
expect(calls).not.toContain("exec.approval.request");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -838,7 +838,10 @@ export function createExecTool(
|
|||||||
applyPathPrepend(env, defaultPathPrepend);
|
applyPathPrepend(env, defaultPathPrepend);
|
||||||
|
|
||||||
if (host === "node") {
|
if (host === "node") {
|
||||||
const approvals = resolveExecApprovals(agentId, { security, ask });
|
const approvals = resolveExecApprovals(
|
||||||
|
agentId,
|
||||||
|
host === "node" ? { security: "allowlist" } : undefined,
|
||||||
|
);
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
@@ -1109,7 +1112,7 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (host === "gateway" && !bypassApprovals) {
|
if (host === "gateway" && !bypassApprovals) {
|
||||||
const approvals = resolveExecApprovals(agentId, { security, ask });
|
const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
import type { BedrockClient } from "@aws-sdk/client-bedrock";
|
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
const sendMock = vi.fn();
|
|
||||||
const clientFactory = () => ({ send: sendMock }) as unknown as BedrockClient;
|
|
||||||
|
|
||||||
describe("bedrock discovery", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
sendMock.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters to active streaming text models and maps modalities", async () => {
|
|
||||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
|
||||||
await import("./bedrock-discovery.js");
|
|
||||||
resetBedrockDiscoveryCacheForTest();
|
|
||||||
|
|
||||||
sendMock.mockResolvedValueOnce({
|
|
||||||
modelSummaries: [
|
|
||||||
{
|
|
||||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
||||||
modelName: "Claude 3.7 Sonnet",
|
|
||||||
providerName: "anthropic",
|
|
||||||
inputModalities: ["TEXT", "IMAGE"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
modelId: "anthropic.claude-3-haiku-20240307-v1:0",
|
|
||||||
modelName: "Claude 3 Haiku",
|
|
||||||
providerName: "anthropic",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: false,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
modelId: "meta.llama3-8b-instruct-v1:0",
|
|
||||||
modelName: "Llama 3 8B",
|
|
||||||
providerName: "meta",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "INACTIVE" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
modelId: "amazon.titan-embed-text-v1",
|
|
||||||
modelName: "Titan Embed",
|
|
||||||
providerName: "amazon",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["EMBEDDING"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
|
|
||||||
expect(models).toHaveLength(1);
|
|
||||||
expect(models[0]).toMatchObject({
|
|
||||||
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
||||||
name: "Claude 3.7 Sonnet",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text", "image"],
|
|
||||||
contextWindow: 32000,
|
|
||||||
maxTokens: 4096,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies provider filter", async () => {
|
|
||||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
|
||||||
await import("./bedrock-discovery.js");
|
|
||||||
resetBedrockDiscoveryCacheForTest();
|
|
||||||
|
|
||||||
sendMock.mockResolvedValueOnce({
|
|
||||||
modelSummaries: [
|
|
||||||
{
|
|
||||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
||||||
modelName: "Claude 3.7 Sonnet",
|
|
||||||
providerName: "anthropic",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const models = await discoverBedrockModels({
|
|
||||||
region: "us-east-1",
|
|
||||||
config: { providerFilter: ["amazon"] },
|
|
||||||
clientFactory,
|
|
||||||
});
|
|
||||||
expect(models).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses configured defaults for context and max tokens", async () => {
|
|
||||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
|
||||||
await import("./bedrock-discovery.js");
|
|
||||||
resetBedrockDiscoveryCacheForTest();
|
|
||||||
|
|
||||||
sendMock.mockResolvedValueOnce({
|
|
||||||
modelSummaries: [
|
|
||||||
{
|
|
||||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
||||||
modelName: "Claude 3.7 Sonnet",
|
|
||||||
providerName: "anthropic",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const models = await discoverBedrockModels({
|
|
||||||
region: "us-east-1",
|
|
||||||
config: { defaultContextWindow: 64000, defaultMaxTokens: 8192 },
|
|
||||||
clientFactory,
|
|
||||||
});
|
|
||||||
expect(models[0]).toMatchObject({ contextWindow: 64000, maxTokens: 8192 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("caches results when refreshInterval is enabled", async () => {
|
|
||||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
|
||||||
await import("./bedrock-discovery.js");
|
|
||||||
resetBedrockDiscoveryCacheForTest();
|
|
||||||
|
|
||||||
sendMock.mockResolvedValueOnce({
|
|
||||||
modelSummaries: [
|
|
||||||
{
|
|
||||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
||||||
modelName: "Claude 3.7 Sonnet",
|
|
||||||
providerName: "anthropic",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await discoverBedrockModels({ region: "us-east-1", clientFactory });
|
|
||||||
await discoverBedrockModels({ region: "us-east-1", clientFactory });
|
|
||||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips cache when refreshInterval is 0", async () => {
|
|
||||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
|
||||||
await import("./bedrock-discovery.js");
|
|
||||||
resetBedrockDiscoveryCacheForTest();
|
|
||||||
|
|
||||||
sendMock
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
modelSummaries: [
|
|
||||||
{
|
|
||||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
||||||
modelName: "Claude 3.7 Sonnet",
|
|
||||||
providerName: "anthropic",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
modelSummaries: [
|
|
||||||
{
|
|
||||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
||||||
modelName: "Claude 3.7 Sonnet",
|
|
||||||
providerName: "anthropic",
|
|
||||||
inputModalities: ["TEXT"],
|
|
||||||
outputModalities: ["TEXT"],
|
|
||||||
responseStreamingSupported: true,
|
|
||||||
modelLifecycle: { status: "ACTIVE" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await discoverBedrockModels({
|
|
||||||
region: "us-east-1",
|
|
||||||
config: { refreshInterval: 0 },
|
|
||||||
clientFactory,
|
|
||||||
});
|
|
||||||
await discoverBedrockModels({
|
|
||||||
region: "us-east-1",
|
|
||||||
config: { refreshInterval: 0 },
|
|
||||||
clientFactory,
|
|
||||||
});
|
|
||||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import {
|
|
||||||
BedrockClient,
|
|
||||||
ListFoundationModelsCommand,
|
|
||||||
type ListFoundationModelsCommandOutput,
|
|
||||||
} from "@aws-sdk/client-bedrock";
|
|
||||||
|
|
||||||
import type { BedrockDiscoveryConfig, ModelDefinitionConfig } from "../config/types.js";
|
|
||||||
|
|
||||||
const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600;
|
|
||||||
const DEFAULT_CONTEXT_WINDOW = 32000;
|
|
||||||
const DEFAULT_MAX_TOKENS = 4096;
|
|
||||||
const DEFAULT_COST = {
|
|
||||||
input: 0,
|
|
||||||
output: 0,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
type BedrockModelSummary = NonNullable<ListFoundationModelsCommandOutput["modelSummaries"]>[number];
|
|
||||||
|
|
||||||
type BedrockDiscoveryCacheEntry = {
|
|
||||||
expiresAt: number;
|
|
||||||
value?: ModelDefinitionConfig[];
|
|
||||||
inFlight?: Promise<ModelDefinitionConfig[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const discoveryCache = new Map<string, BedrockDiscoveryCacheEntry>();
|
|
||||||
let hasLoggedBedrockError = false;
|
|
||||||
|
|
||||||
function normalizeProviderFilter(filter?: string[]): string[] {
|
|
||||||
if (!filter || filter.length === 0) return [];
|
|
||||||
const normalized = new Set(
|
|
||||||
filter.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0),
|
|
||||||
);
|
|
||||||
return Array.from(normalized).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCacheKey(params: {
|
|
||||||
region: string;
|
|
||||||
providerFilter: string[];
|
|
||||||
refreshIntervalSeconds: number;
|
|
||||||
defaultContextWindow: number;
|
|
||||||
defaultMaxTokens: number;
|
|
||||||
}): string {
|
|
||||||
return JSON.stringify(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
function includesTextModalities(modalities?: Array<string>): boolean {
|
|
||||||
return (modalities ?? []).some((entry) => entry.toLowerCase() === "text");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActive(summary: BedrockModelSummary): boolean {
|
|
||||||
const status = summary.modelLifecycle?.status;
|
|
||||||
return typeof status === "string" ? status.toUpperCase() === "ACTIVE" : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image"> {
|
|
||||||
const inputs = summary.inputModalities ?? [];
|
|
||||||
const mapped = new Set<"text" | "image">();
|
|
||||||
for (const modality of inputs) {
|
|
||||||
const lower = modality.toLowerCase();
|
|
||||||
if (lower === "text") mapped.add("text");
|
|
||||||
if (lower === "image") mapped.add("image");
|
|
||||||
}
|
|
||||||
if (mapped.size === 0) mapped.add("text");
|
|
||||||
return Array.from(mapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferReasoningSupport(summary: BedrockModelSummary): boolean {
|
|
||||||
const haystack = `${summary.modelId ?? ""} ${summary.modelName ?? ""}`.toLowerCase();
|
|
||||||
return haystack.includes("reasoning") || haystack.includes("thinking");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveDefaultContextWindow(config?: BedrockDiscoveryConfig): number {
|
|
||||||
const value = Math.floor(config?.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW);
|
|
||||||
return value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveDefaultMaxTokens(config?: BedrockDiscoveryConfig): number {
|
|
||||||
const value = Math.floor(config?.defaultMaxTokens ?? DEFAULT_MAX_TOKENS);
|
|
||||||
return value > 0 ? value : DEFAULT_MAX_TOKENS;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesProviderFilter(summary: BedrockModelSummary, filter: string[]): boolean {
|
|
||||||
if (filter.length === 0) return true;
|
|
||||||
const providerName =
|
|
||||||
summary.providerName ??
|
|
||||||
(typeof summary.modelId === "string" ? summary.modelId.split(".")[0] : undefined);
|
|
||||||
const normalized = providerName?.trim().toLowerCase();
|
|
||||||
if (!normalized) return false;
|
|
||||||
return filter.includes(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIncludeSummary(summary: BedrockModelSummary, filter: string[]): boolean {
|
|
||||||
if (!summary.modelId?.trim()) return false;
|
|
||||||
if (!matchesProviderFilter(summary, filter)) return false;
|
|
||||||
if (summary.responseStreamingSupported !== true) return false;
|
|
||||||
if (!includesTextModalities(summary.outputModalities)) return false;
|
|
||||||
if (!isActive(summary)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toModelDefinition(
|
|
||||||
summary: BedrockModelSummary,
|
|
||||||
defaults: { contextWindow: number; maxTokens: number },
|
|
||||||
): ModelDefinitionConfig {
|
|
||||||
const id = summary.modelId?.trim() ?? "";
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: summary.modelName?.trim() || id,
|
|
||||||
reasoning: inferReasoningSupport(summary),
|
|
||||||
input: mapInputModalities(summary),
|
|
||||||
cost: DEFAULT_COST,
|
|
||||||
contextWindow: defaults.contextWindow,
|
|
||||||
maxTokens: defaults.maxTokens,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetBedrockDiscoveryCacheForTest(): void {
|
|
||||||
discoveryCache.clear();
|
|
||||||
hasLoggedBedrockError = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function discoverBedrockModels(params: {
|
|
||||||
region: string;
|
|
||||||
config?: BedrockDiscoveryConfig;
|
|
||||||
now?: () => number;
|
|
||||||
clientFactory?: (region: string) => BedrockClient;
|
|
||||||
}): Promise<ModelDefinitionConfig[]> {
|
|
||||||
const refreshIntervalSeconds = Math.max(
|
|
||||||
0,
|
|
||||||
Math.floor(params.config?.refreshInterval ?? DEFAULT_REFRESH_INTERVAL_SECONDS),
|
|
||||||
);
|
|
||||||
const providerFilter = normalizeProviderFilter(params.config?.providerFilter);
|
|
||||||
const defaultContextWindow = resolveDefaultContextWindow(params.config);
|
|
||||||
const defaultMaxTokens = resolveDefaultMaxTokens(params.config);
|
|
||||||
const cacheKey = buildCacheKey({
|
|
||||||
region: params.region,
|
|
||||||
providerFilter,
|
|
||||||
refreshIntervalSeconds,
|
|
||||||
defaultContextWindow,
|
|
||||||
defaultMaxTokens,
|
|
||||||
});
|
|
||||||
const now = params.now?.() ?? Date.now();
|
|
||||||
|
|
||||||
if (refreshIntervalSeconds > 0) {
|
|
||||||
const cached = discoveryCache.get(cacheKey);
|
|
||||||
if (cached?.value && cached.expiresAt > now) {
|
|
||||||
return cached.value;
|
|
||||||
}
|
|
||||||
if (cached?.inFlight) {
|
|
||||||
return cached.inFlight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientFactory = params.clientFactory ?? ((region: string) => new BedrockClient({ region }));
|
|
||||||
const client = clientFactory(params.region);
|
|
||||||
|
|
||||||
const discoveryPromise = (async () => {
|
|
||||||
const response = await client.send(new ListFoundationModelsCommand({}));
|
|
||||||
const discovered: ModelDefinitionConfig[] = [];
|
|
||||||
for (const summary of response.modelSummaries ?? []) {
|
|
||||||
if (!shouldIncludeSummary(summary, providerFilter)) continue;
|
|
||||||
discovered.push(
|
|
||||||
toModelDefinition(summary, {
|
|
||||||
contextWindow: defaultContextWindow,
|
|
||||||
maxTokens: defaultMaxTokens,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return discovered.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (refreshIntervalSeconds > 0) {
|
|
||||||
discoveryCache.set(cacheKey, {
|
|
||||||
expiresAt: now + refreshIntervalSeconds * 1000,
|
|
||||||
inFlight: discoveryPromise,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const value = await discoveryPromise;
|
|
||||||
if (refreshIntervalSeconds > 0) {
|
|
||||||
discoveryCache.set(cacheKey, {
|
|
||||||
expiresAt: now + refreshIntervalSeconds * 1000,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} catch (error) {
|
|
||||||
if (refreshIntervalSeconds > 0) {
|
|
||||||
discoveryCache.delete(cacheKey);
|
|
||||||
}
|
|
||||||
if (!hasLoggedBedrockError) {
|
|
||||||
hasLoggedBedrockError = true;
|
|
||||||
console.warn(`[bedrock-discovery] Failed to list models: ${String(error)}`);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,8 +17,7 @@ vi.mock("../config/sessions.js", async (importOriginal) => {
|
|||||||
updateSessionStoreMock(storePath, store);
|
updateSessionStoreMock(storePath, store);
|
||||||
return store;
|
return store;
|
||||||
},
|
},
|
||||||
resolveStorePath: (_store: string | undefined, opts?: { agentId?: string }) =>
|
resolveStorePath: () => "/tmp/sessions.json",
|
||||||
opts?.agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json",
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,118 +117,11 @@ describe("session_status tool", () => {
|
|||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) throw new Error("missing session_status tool");
|
||||||
|
|
||||||
await expect(tool.execute("call2", { sessionKey: "nope" })).rejects.toThrow(
|
await expect(tool.execute("call2", { sessionKey: "nope" })).rejects.toThrow(
|
||||||
"Unknown sessionId",
|
"Unknown sessionKey",
|
||||||
);
|
);
|
||||||
expect(updateSessionStoreMock).not.toHaveBeenCalled();
|
expect(updateSessionStoreMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves sessionId inputs", async () => {
|
|
||||||
loadSessionStoreMock.mockReset();
|
|
||||||
updateSessionStoreMock.mockReset();
|
|
||||||
const sessionId = "sess-main";
|
|
||||||
loadSessionStoreMock.mockReturnValue({
|
|
||||||
"agent:main:main": {
|
|
||||||
sessionId,
|
|
||||||
updatedAt: 10,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createClawdbotTools({ agentSessionKey: "main" }).find(
|
|
||||||
(candidate) => candidate.name === "session_status",
|
|
||||||
);
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
|
||||||
|
|
||||||
const result = await tool.execute("call3", { sessionKey: sessionId });
|
|
||||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
|
||||||
expect(details.ok).toBe(true);
|
|
||||||
expect(details.sessionKey).toBe("agent:main:main");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses non-standard session keys without sessionId resolution", async () => {
|
|
||||||
loadSessionStoreMock.mockReset();
|
|
||||||
updateSessionStoreMock.mockReset();
|
|
||||||
loadSessionStoreMock.mockReturnValue({
|
|
||||||
"temp:slug-generator": {
|
|
||||||
sessionId: "sess-temp",
|
|
||||||
updatedAt: 10,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createClawdbotTools({ agentSessionKey: "main" }).find(
|
|
||||||
(candidate) => candidate.name === "session_status",
|
|
||||||
);
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
|
||||||
|
|
||||||
const result = await tool.execute("call4", { sessionKey: "temp:slug-generator" });
|
|
||||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
|
||||||
expect(details.ok).toBe(true);
|
|
||||||
expect(details.sessionKey).toBe("temp:slug-generator");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("blocks cross-agent session_status without agent-to-agent access", async () => {
|
|
||||||
loadSessionStoreMock.mockReset();
|
|
||||||
updateSessionStoreMock.mockReset();
|
|
||||||
loadSessionStoreMock.mockReturnValue({
|
|
||||||
"agent:other:main": {
|
|
||||||
sessionId: "s2",
|
|
||||||
updatedAt: 10,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createClawdbotTools({ agentSessionKey: "agent:main:main" }).find(
|
|
||||||
(candidate) => candidate.name === "session_status",
|
|
||||||
);
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
|
||||||
|
|
||||||
await expect(tool.execute("call5", { sessionKey: "agent:other:main" })).rejects.toThrow(
|
|
||||||
"Agent-to-agent status is disabled",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("scopes bare session keys to the requester agent", async () => {
|
|
||||||
loadSessionStoreMock.mockReset();
|
|
||||||
updateSessionStoreMock.mockReset();
|
|
||||||
const stores = new Map<string, Record<string, unknown>>([
|
|
||||||
[
|
|
||||||
"/tmp/main/sessions.json",
|
|
||||||
{
|
|
||||||
"agent:main:main": { sessionId: "s-main", updatedAt: 10 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"/tmp/support/sessions.json",
|
|
||||||
{
|
|
||||||
main: { sessionId: "s-support", updatedAt: 20 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
loadSessionStoreMock.mockImplementation((storePath: string) => {
|
|
||||||
return stores.get(storePath) ?? {};
|
|
||||||
});
|
|
||||||
updateSessionStoreMock.mockImplementation(
|
|
||||||
(_storePath: string, store: Record<string, unknown>) => {
|
|
||||||
// Keep map in sync for resolveSessionEntry fallbacks if needed.
|
|
||||||
if (_storePath) {
|
|
||||||
stores.set(_storePath, store);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const tool = createClawdbotTools({ agentSessionKey: "agent:support:main" }).find(
|
|
||||||
(candidate) => candidate.name === "session_status",
|
|
||||||
);
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
|
||||||
|
|
||||||
const result = await tool.execute("call6", { sessionKey: "main" });
|
|
||||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
|
||||||
expect(details.ok).toBe(true);
|
|
||||||
expect(details.sessionKey).toBe("main");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets per-session model override via model=default", async () => {
|
it("resets per-session model override via model=default", async () => {
|
||||||
loadSessionStoreMock.mockReset();
|
loadSessionStoreMock.mockReset();
|
||||||
updateSessionStoreMock.mockReset();
|
updateSessionStoreMock.mockReset();
|
||||||
|
|||||||
@@ -172,62 +172,6 @@ describe("sessions tools", () => {
|
|||||||
expect(withToolsDetails.messages).toHaveLength(2);
|
expect(withToolsDetails.messages).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sessions_history resolves sessionId inputs", async () => {
|
|
||||||
callGatewayMock.mockReset();
|
|
||||||
const sessionId = "sess-group";
|
|
||||||
const targetKey = "agent:main:discord:channel:1457165743010611293";
|
|
||||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
|
||||||
const request = opts as { method?: string; params?: Record<string, unknown> };
|
|
||||||
if (request.method === "sessions.resolve") {
|
|
||||||
return {
|
|
||||||
key: targetKey,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (request.method === "chat.history") {
|
|
||||||
return {
|
|
||||||
messages: [{ role: "assistant", content: [{ type: "text", text: "ok" }] }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history");
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing sessions_history tool");
|
|
||||||
|
|
||||||
const result = await tool.execute("call5", { sessionKey: sessionId });
|
|
||||||
const details = result.details as { messages?: unknown[] };
|
|
||||||
expect(details.messages).toHaveLength(1);
|
|
||||||
const historyCall = callGatewayMock.mock.calls.find(
|
|
||||||
(call) => (call[0] as { method?: string }).method === "chat.history",
|
|
||||||
);
|
|
||||||
expect(historyCall?.[0]).toMatchObject({
|
|
||||||
method: "chat.history",
|
|
||||||
params: { sessionKey: targetKey },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sessions_history errors on missing sessionId", async () => {
|
|
||||||
callGatewayMock.mockReset();
|
|
||||||
const sessionId = "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa";
|
|
||||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
|
||||||
const request = opts as { method?: string };
|
|
||||||
if (request.method === "sessions.resolve") {
|
|
||||||
throw new Error("No session found");
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history");
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing sessions_history tool");
|
|
||||||
|
|
||||||
const result = await tool.execute("call6", { sessionKey: sessionId });
|
|
||||||
const details = result.details as { status?: string; error?: string };
|
|
||||||
expect(details.status).toBe("error");
|
|
||||||
expect(details.error).toMatch(/Session not found|No session found/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sessions_send supports fire-and-forget and wait", async () => {
|
it("sessions_send supports fire-and-forget and wait", async () => {
|
||||||
callGatewayMock.mockReset();
|
callGatewayMock.mockReset();
|
||||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||||
@@ -369,50 +313,6 @@ describe("sessions tools", () => {
|
|||||||
expect(sendCallCount).toBe(0);
|
expect(sendCallCount).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sessions_send resolves sessionId inputs", async () => {
|
|
||||||
callGatewayMock.mockReset();
|
|
||||||
const sessionId = "sess-send";
|
|
||||||
const targetKey = "agent:main:discord:channel:123";
|
|
||||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
|
||||||
const request = opts as { method?: string; params?: Record<string, unknown> };
|
|
||||||
if (request.method === "sessions.resolve") {
|
|
||||||
return { key: targetKey };
|
|
||||||
}
|
|
||||||
if (request.method === "agent") {
|
|
||||||
return { runId: "run-1", acceptedAt: 123 };
|
|
||||||
}
|
|
||||||
if (request.method === "agent.wait") {
|
|
||||||
return { status: "ok" };
|
|
||||||
}
|
|
||||||
if (request.method === "chat.history") {
|
|
||||||
return { messages: [] };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createClawdbotTools({
|
|
||||||
agentSessionKey: "main",
|
|
||||||
agentChannel: "discord",
|
|
||||||
}).find((candidate) => candidate.name === "sessions_send");
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing sessions_send tool");
|
|
||||||
|
|
||||||
const result = await tool.execute("call7", {
|
|
||||||
sessionKey: sessionId,
|
|
||||||
message: "ping",
|
|
||||||
timeoutSeconds: 0,
|
|
||||||
});
|
|
||||||
const details = result.details as { status?: string };
|
|
||||||
expect(details.status).toBe("accepted");
|
|
||||||
const agentCall = callGatewayMock.mock.calls.find(
|
|
||||||
(call) => (call[0] as { method?: string }).method === "agent",
|
|
||||||
);
|
|
||||||
expect(agentCall?.[0]).toMatchObject({
|
|
||||||
method: "agent",
|
|
||||||
params: { sessionKey: targetKey },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sessions_send runs ping-pong then announces", async () => {
|
it("sessions_send runs ping-pong then announces", async () => {
|
||||||
callGatewayMock.mockReset();
|
callGatewayMock.mockReset();
|
||||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { createSessionsListTool } from "./tools/sessions-list-tool.js";
|
|||||||
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
||||||
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
||||||
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
|
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
|
||||||
import { createTtsTool } from "./tools/tts-tool.js";
|
|
||||||
|
|
||||||
export function createClawdbotTools(options?: {
|
export function createClawdbotTools(options?: {
|
||||||
browserControlUrl?: string;
|
browserControlUrl?: string;
|
||||||
@@ -32,12 +31,6 @@ export function createClawdbotTools(options?: {
|
|||||||
agentTo?: string;
|
agentTo?: string;
|
||||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||||
agentThreadId?: string | number;
|
agentThreadId?: string | number;
|
||||||
/** Group id for channel-level tool policy inheritance. */
|
|
||||||
agentGroupId?: string | null;
|
|
||||||
/** Group channel label for channel-level tool policy inheritance. */
|
|
||||||
agentGroupChannel?: string | null;
|
|
||||||
/** Group space label for channel-level tool policy inheritance. */
|
|
||||||
agentGroupSpace?: string | null;
|
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
sandboxRoot?: string;
|
sandboxRoot?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
@@ -97,10 +90,6 @@ export function createClawdbotTools(options?: {
|
|||||||
replyToMode: options?.replyToMode,
|
replyToMode: options?.replyToMode,
|
||||||
hasRepliedRef: options?.hasRepliedRef,
|
hasRepliedRef: options?.hasRepliedRef,
|
||||||
}),
|
}),
|
||||||
createTtsTool({
|
|
||||||
agentChannel: options?.agentChannel,
|
|
||||||
config: options?.config,
|
|
||||||
}),
|
|
||||||
createGatewayTool({
|
createGatewayTool({
|
||||||
agentSessionKey: options?.agentSessionKey,
|
agentSessionKey: options?.agentSessionKey,
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
@@ -125,9 +114,6 @@ export function createClawdbotTools(options?: {
|
|||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
agentTo: options?.agentTo,
|
agentTo: options?.agentTo,
|
||||||
agentThreadId: options?.agentThreadId,
|
agentThreadId: options?.agentThreadId,
|
||||||
agentGroupId: options?.agentGroupId,
|
|
||||||
agentGroupChannel: options?.agentGroupChannel,
|
|
||||||
agentGroupSpace: options?.agentGroupSpace,
|
|
||||||
sandboxed: options?.sandboxed,
|
sandboxed: options?.sandboxed,
|
||||||
}),
|
}),
|
||||||
createSessionStatusTool({
|
createSessionStatusTool({
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
|||||||
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
||||||
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||||
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
||||||
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
|
|
||||||
|
|
||||||
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
|
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
|
||||||
|
|
||||||
@@ -195,7 +194,6 @@ export function buildSystemPrompt(params: {
|
|||||||
defaultModel: defaultModelLabel,
|
defaultModel: defaultModelLabel,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
|
|
||||||
return buildAgentSystemPrompt({
|
return buildAgentSystemPrompt({
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
defaultThinkLevel: params.defaultThinkLevel,
|
defaultThinkLevel: params.defaultThinkLevel,
|
||||||
@@ -211,7 +209,6 @@ export function buildSystemPrompt(params: {
|
|||||||
userTime,
|
userTime,
|
||||||
userTimeFormat,
|
userTimeFormat,
|
||||||
contextFiles: params.contextFiles,
|
contextFiles: params.contextFiles,
|
||||||
ttsHint,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { classifyFailoverReason, type FailoverReason } from "./pi-embedded-helpers.js";
|
import { classifyFailoverReason, type FailoverReason } from "./pi-embedded-helpers.js";
|
||||||
|
|
||||||
const TIMEOUT_HINT_RE = /timeout|timed out|deadline exceeded|context deadline exceeded/i;
|
const TIMEOUT_HINT_RE = /timeout|timed out|deadline exceeded|context deadline exceeded/i;
|
||||||
const ABORT_TIMEOUT_RE = /request was aborted|request aborted/i;
|
|
||||||
|
|
||||||
export class FailoverError extends Error {
|
export class FailoverError extends Error {
|
||||||
readonly reason: FailoverReason;
|
readonly reason: FailoverReason;
|
||||||
@@ -105,8 +104,6 @@ export function isTimeoutError(err: unknown): boolean {
|
|||||||
if (hasTimeoutHint(err)) return true;
|
if (hasTimeoutHint(err)) return true;
|
||||||
if (!err || typeof err !== "object") return false;
|
if (!err || typeof err !== "object") return false;
|
||||||
if (getErrorName(err) !== "AbortError") return false;
|
if (getErrorName(err) !== "AbortError") return false;
|
||||||
const message = getErrorMessage(err);
|
|
||||||
if (message && ABORT_TIMEOUT_RE.test(message)) return true;
|
|
||||||
const cause = "cause" in err ? (err as { cause?: unknown }).cause : undefined;
|
const cause = "cause" in err ? (err as { cause?: unknown }).cause : undefined;
|
||||||
const reason = "reason" in err ? (err as { reason?: unknown }).reason : undefined;
|
const reason = "reason" in err ? (err as { reason?: unknown }).reason : undefined;
|
||||||
return hasTimeoutHint(cause) || hasTimeoutHint(reason);
|
return hasTimeoutHint(cause) || hasTimeoutHint(reason);
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { parseIdentityMarkdown } from "./identity-file.js";
|
|
||||||
|
|
||||||
describe("parseIdentityMarkdown", () => {
|
|
||||||
it("ignores identity template placeholders", () => {
|
|
||||||
const content = `
|
|
||||||
# IDENTITY.md - Who Am I?
|
|
||||||
|
|
||||||
- **Name:** *(pick something you like)*
|
|
||||||
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
|
||||||
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
|
||||||
- **Emoji:** *(your signature - pick one that feels right)*
|
|
||||||
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
|
|
||||||
`;
|
|
||||||
const parsed = parseIdentityMarkdown(content);
|
|
||||||
expect(parsed).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses explicit identity values", () => {
|
|
||||||
const content = `
|
|
||||||
- **Name:** Samantha
|
|
||||||
- **Creature:** Robot
|
|
||||||
- **Vibe:** Warm
|
|
||||||
- **Emoji:** :robot:
|
|
||||||
- **Avatar:** avatars/clawd.png
|
|
||||||
`;
|
|
||||||
const parsed = parseIdentityMarkdown(content);
|
|
||||||
expect(parsed).toEqual({
|
|
||||||
name: "Samantha",
|
|
||||||
creature: "Robot",
|
|
||||||
vibe: "Warm",
|
|
||||||
emoji: ":robot:",
|
|
||||||
avatar: "avatars/clawd.png",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -12,30 +12,6 @@ export type AgentIdentityFile = {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IDENTITY_PLACEHOLDER_VALUES = new Set([
|
|
||||||
"pick something you like",
|
|
||||||
"ai? robot? familiar? ghost in the machine? something weirder?",
|
|
||||||
"how do you come across? sharp? warm? chaotic? calm?",
|
|
||||||
"your signature - pick one that feels right",
|
|
||||||
"workspace-relative path, http(s) url, or data uri",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function normalizeIdentityValue(value: string): string {
|
|
||||||
let normalized = value.trim();
|
|
||||||
normalized = normalized.replace(/^[*_]+|[*_]+$/g, "").trim();
|
|
||||||
if (normalized.startsWith("(") && normalized.endsWith(")")) {
|
|
||||||
normalized = normalized.slice(1, -1).trim();
|
|
||||||
}
|
|
||||||
normalized = normalized.replace(/[\u2013\u2014]/g, "-");
|
|
||||||
normalized = normalized.replace(/\s+/g, " ").toLowerCase();
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIdentityPlaceholder(value: string): boolean {
|
|
||||||
const normalized = normalizeIdentityValue(value);
|
|
||||||
return IDENTITY_PLACEHOLDER_VALUES.has(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
||||||
const identity: AgentIdentityFile = {};
|
const identity: AgentIdentityFile = {};
|
||||||
const lines = content.split(/\r?\n/);
|
const lines = content.split(/\r?\n/);
|
||||||
@@ -49,7 +25,6 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
|||||||
.replace(/^[*_]+|[*_]+$/g, "")
|
.replace(/^[*_]+|[*_]+$/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
if (isIdentityPlaceholder(value)) continue;
|
|
||||||
if (label === "name") identity.name = value;
|
if (label === "name") identity.name = value;
|
||||||
if (label === "emoji") identity.emoji = value;
|
if (label === "emoji") identity.emoji = value;
|
||||||
if (label === "creature") identity.creature = value;
|
if (label === "creature") identity.creature = value;
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ function resolveEnvSourceLabel(params: {
|
|||||||
return `${prefix}${params.label}`;
|
return `${prefix}${params.label}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
export function resolveAwsSdkEnvVarName(): string | undefined {
|
||||||
if (env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
|
if (process.env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
|
||||||
if (env[AWS_ACCESS_KEY_ENV]?.trim() && env[AWS_SECRET_KEY_ENV]?.trim()) {
|
if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) {
|
||||||
return AWS_ACCESS_KEY_ENV;
|
return AWS_ACCESS_KEY_ENV;
|
||||||
}
|
}
|
||||||
if (env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
|
if (process.env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -346,28 +346,6 @@ describe("runWithModelFallback", () => {
|
|||||||
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
|
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back on provider abort errors with request-aborted messages", async () => {
|
|
||||||
const cfg = makeCfg();
|
|
||||||
const run = vi
|
|
||||||
.fn()
|
|
||||||
.mockRejectedValueOnce(
|
|
||||||
Object.assign(new Error("Request was aborted"), { name: "AbortError" }),
|
|
||||||
)
|
|
||||||
.mockResolvedValueOnce("ok");
|
|
||||||
|
|
||||||
const result = await runWithModelFallback({
|
|
||||||
cfg,
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-4.1-mini",
|
|
||||||
run,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.result).toBe("ok");
|
|
||||||
expect(run).toHaveBeenCalledTimes(2);
|
|
||||||
expect(run.mock.calls[1]?.[0]).toBe("anthropic");
|
|
||||||
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not fall back on user aborts", async () => {
|
it("does not fall back on user aborts", async () => {
|
||||||
const cfg = makeCfg();
|
const cfg = makeCfg();
|
||||||
const run = vi
|
const run = vi
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
} from "../providers/github-copilot-token.js";
|
} from "../providers/github-copilot-token.js";
|
||||||
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
||||||
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
||||||
import { discoverBedrockModels } from "./bedrock-discovery.js";
|
|
||||||
import {
|
import {
|
||||||
buildSyntheticModelDefinition,
|
buildSyntheticModelDefinition,
|
||||||
SYNTHETIC_BASE_URL,
|
SYNTHETIC_BASE_URL,
|
||||||
@@ -376,27 +375,3 @@ export async function resolveImplicitCopilotProvider(params: {
|
|||||||
models: [],
|
models: [],
|
||||||
} satisfies ProviderConfig;
|
} satisfies ProviderConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveImplicitBedrockProvider(params: {
|
|
||||||
agentDir: string;
|
|
||||||
config?: ClawdbotConfig;
|
|
||||||
env?: NodeJS.ProcessEnv;
|
|
||||||
}): Promise<ProviderConfig | null> {
|
|
||||||
const env = params.env ?? process.env;
|
|
||||||
const discoveryConfig = params.config?.models?.bedrockDiscovery;
|
|
||||||
const enabled = discoveryConfig?.enabled;
|
|
||||||
const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined;
|
|
||||||
if (enabled === false) return null;
|
|
||||||
if (enabled !== true && !hasAwsCreds) return null;
|
|
||||||
|
|
||||||
const region = discoveryConfig?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
|
|
||||||
const models = await discoverBedrockModels({ region, config: discoveryConfig });
|
|
||||||
if (models.length === 0) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: `https://bedrock-runtime.${region}.amazonaws.com`,
|
|
||||||
api: "bedrock-converse-stream",
|
|
||||||
auth: "aws-sdk",
|
|
||||||
models,
|
|
||||||
} satisfies ProviderConfig;
|
|
||||||
}
|
|
||||||
Loaded 100 of 354 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user