Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a36dc285d6 | ||
|
|
704a3ba51c |
No files matched your search
+1
-16
@@ -5,32 +5,22 @@ Docs: https://docs.clawd.bot
|
|||||||
## 2026.1.23 (Unreleased)
|
## 2026.1.23 (Unreleased)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- 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).
|
|
||||||
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
|
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
|
||||||
- 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.
|
- 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.
|
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
|
- Logging: guard console settings resolution to avoid recursion on config warnings. (#1555) Thanks @travisp.
|
||||||
- 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.
|
- 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.
|
||||||
- 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.
|
||||||
@@ -43,20 +33,15 @@ Docs: https://docs.clawd.bot
|
|||||||
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
- 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: drop the em dash when the banner tagline wraps to a second line.
|
||||||
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
- 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: 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.
|
- Daemon: use platform PATH delimiters when building minimal service paths.
|
||||||
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
- 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.
|
||||||
- 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/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a>
|
||||||
<a href="https://github.com/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/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a>
|
||||||
<a href="https://github.com/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/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a>
|
||||||
<a href="https://github.com/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/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a>
|
||||||
<a href="https://github.com/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/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a>
|
||||||
<a href="https://github.com/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/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a>
|
||||||
<a href="https://github.com/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/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a>
|
||||||
<a href="https://github.com/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/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a>
|
||||||
<a href="https://github.com/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/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a>
|
||||||
<a href="https://github.com/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/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a>
|
||||||
<a href="https://github.com/search?q=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/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a>
|
||||||
<a href="https://github.com/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/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a>
|
||||||
<a href="https://github.com/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/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a>
|
||||||
<a href="https://github.com/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/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a>
|
||||||
<a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
|
<a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a>
|
||||||
<a href="https://github.com/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/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a>
|
||||||
<a href="https://github.com/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/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a>
|
||||||
<a href="https://github.com/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/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a>
|
||||||
<a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
|
<a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
|
||||||
<a href="https://github.com/search?q=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=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a>
|
||||||
<a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
|
<a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a>
|
||||||
<a href="https://github.com/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=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a>
|
||||||
<a href="https://github.com/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/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||||
<a href="https://github.com/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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+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
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -850,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",
|
||||||
|
|||||||
@@ -162,10 +162,6 @@ 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`:
|
||||||
@@ -199,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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)*
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -971,10 +971,6 @@ Heartbeats run every **30m** by default. Tune or disable them:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
||||||
|
|
||||||
### Do I need to add a “bot account” to a WhatsApp group?
|
### Do I need to add a “bot account” to a WhatsApp group?
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
|
|||||||
Notes:
|
Notes:
|
||||||
- Matching is case-insensitive.
|
- Matching is case-insensitive.
|
||||||
- `*` wildcards are supported (`"*"` means all tools).
|
- `*` 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -31,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;
|
||||||
@@ -120,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({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ const makeAttempt = (overrides: Partial<EmbeddedRunAttemptResult>): EmbeddedRunA
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotConfig =>
|
const makeConfig = (): ClawdbotConfig =>
|
||||||
({
|
({
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
model: {
|
model: {
|
||||||
fallbacks: opts?.fallbacks ?? [],
|
fallbacks: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
|
|||||||
providers: {
|
providers: {
|
||||||
openai: {
|
openai: {
|
||||||
api: "openai-responses",
|
api: "openai-responses",
|
||||||
apiKey: opts?.apiKey ?? "sk-test",
|
apiKey: "sk-test",
|
||||||
baseUrl: "https://example.com",
|
baseUrl: "https://example.com",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
@@ -94,13 +94,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
|
|||||||
},
|
},
|
||||||
}) satisfies ClawdbotConfig;
|
}) satisfies ClawdbotConfig;
|
||||||
|
|
||||||
const writeAuthStore = async (
|
const writeAuthStore = async (agentDir: string, opts?: { includeAnthropic?: boolean }) => {
|
||||||
agentDir: string,
|
|
||||||
opts?: {
|
|
||||||
includeAnthropic?: boolean;
|
|
||||||
usageStats?: Record<string, { lastUsed?: number; cooldownUntil?: number }>;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
const payload = {
|
const payload = {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -111,12 +105,10 @@ const writeAuthStore = async (
|
|||||||
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
|
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
usageStats:
|
usageStats: {
|
||||||
opts?.usageStats ??
|
"openai:p1": { lastUsed: 1 },
|
||||||
({
|
"openai:p2": { lastUsed: 2 },
|
||||||
"openai:p1": { lastUsed: 1 },
|
},
|
||||||
"openai:p2": { lastUsed: 2 },
|
|
||||||
} as Record<string, { lastUsed?: number }>),
|
|
||||||
};
|
};
|
||||||
await fs.writeFile(authPath, JSON.stringify(payload));
|
await fs.writeFile(authPath, JSON.stringify(payload));
|
||||||
};
|
};
|
||||||
@@ -392,92 +384,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails over when all profiles are in cooldown and fallbacks are configured", async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
try {
|
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
|
||||||
const now = Date.now();
|
|
||||||
vi.setSystemTime(now);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await writeAuthStore(agentDir, {
|
|
||||||
usageStats: {
|
|
||||||
"openai:p1": { lastUsed: 1, cooldownUntil: now + 60 * 60 * 1000 },
|
|
||||||
"openai:p2": { lastUsed: 2, cooldownUntil: now + 60 * 60 * 1000 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
runEmbeddedPiAgent({
|
|
||||||
sessionId: "session:test",
|
|
||||||
sessionKey: "agent:test:cooldown-failover",
|
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
|
||||||
workspaceDir,
|
|
||||||
agentDir,
|
|
||||||
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
|
|
||||||
prompt: "hello",
|
|
||||||
provider: "openai",
|
|
||||||
model: "mock-1",
|
|
||||||
authProfileIdSource: "auto",
|
|
||||||
timeoutMs: 5_000,
|
|
||||||
runId: "run:cooldown-failover",
|
|
||||||
}),
|
|
||||||
).rejects.toMatchObject({
|
|
||||||
name: "FailoverError",
|
|
||||||
reason: "rate_limit",
|
|
||||||
provider: "openai",
|
|
||||||
model: "mock-1",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
|
||||||
} finally {
|
|
||||||
await fs.rm(agentDir, { recursive: true, force: true });
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
vi.useRealTimers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails over when auth is unavailable and fallbacks are configured", async () => {
|
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
|
||||||
const previousOpenAiKey = process.env.OPENAI_API_KEY;
|
|
||||||
delete process.env.OPENAI_API_KEY;
|
|
||||||
try {
|
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
|
||||||
await fs.writeFile(authPath, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
runEmbeddedPiAgent({
|
|
||||||
sessionId: "session:test",
|
|
||||||
sessionKey: "agent:test:auth-unavailable",
|
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
|
||||||
workspaceDir,
|
|
||||||
agentDir,
|
|
||||||
config: makeConfig({ fallbacks: ["openai/mock-2"], apiKey: "" }),
|
|
||||||
prompt: "hello",
|
|
||||||
provider: "openai",
|
|
||||||
model: "mock-1",
|
|
||||||
authProfileIdSource: "auto",
|
|
||||||
timeoutMs: 5_000,
|
|
||||||
runId: "run:auth-unavailable",
|
|
||||||
}),
|
|
||||||
).rejects.toMatchObject({ name: "FailoverError", reason: "auth" });
|
|
||||||
|
|
||||||
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
|
||||||
} finally {
|
|
||||||
if (previousOpenAiKey === undefined) {
|
|
||||||
delete process.env.OPENAI_API_KEY;
|
|
||||||
} else {
|
|
||||||
process.env.OPENAI_API_KEY = previousOpenAiKey;
|
|
||||||
}
|
|
||||||
await fs.rm(agentDir, { recursive: true, force: true });
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips profiles in cooldown when rotating after failure", async () => {
|
it("skips profiles in cooldown when rotating after failure", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -73,14 +73,6 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
messageChannel?: string;
|
messageChannel?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
@@ -215,10 +207,6 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
groupId: params.groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
spawnedBy: params.spawnedBy,
|
|
||||||
agentDir,
|
agentDir,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
isRateLimitAssistantError,
|
isRateLimitAssistantError,
|
||||||
isTimeoutErrorMessage,
|
isTimeoutErrorMessage,
|
||||||
pickFallbackThinkingLevel,
|
pickFallbackThinkingLevel,
|
||||||
type FailoverReason,
|
|
||||||
} from "../pi-embedded-helpers.js";
|
} from "../pi-embedded-helpers.js";
|
||||||
import { normalizeUsage, type UsageLike } from "../usage.js";
|
import { normalizeUsage, type UsageLike } from "../usage.js";
|
||||||
|
|
||||||
@@ -93,8 +92,6 @@ export async function runEmbeddedPiAgent(
|
|||||||
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||||
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
||||||
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
||||||
const fallbackConfigured =
|
|
||||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
|
||||||
await ensureClawdbotModelsJson(params.config, agentDir);
|
await ensureClawdbotModelsJson(params.config, agentDir);
|
||||||
|
|
||||||
const { model, error, authStorage, modelRegistry } = resolveModel(
|
const { model, error, authStorage, modelRegistry } = resolveModel(
|
||||||
@@ -168,42 +165,6 @@ export async function runEmbeddedPiAgent(
|
|||||||
let apiKeyInfo: ApiKeyInfo | null = null;
|
let apiKeyInfo: ApiKeyInfo | null = null;
|
||||||
let lastProfileId: string | undefined;
|
let lastProfileId: string | undefined;
|
||||||
|
|
||||||
const resolveAuthProfileFailoverReason = (params: {
|
|
||||||
allInCooldown: boolean;
|
|
||||||
message: string;
|
|
||||||
}): FailoverReason => {
|
|
||||||
if (params.allInCooldown) return "rate_limit";
|
|
||||||
const classified = classifyFailoverReason(params.message);
|
|
||||||
return classified ?? "auth";
|
|
||||||
};
|
|
||||||
|
|
||||||
const throwAuthProfileFailover = (params: {
|
|
||||||
allInCooldown: boolean;
|
|
||||||
message?: string;
|
|
||||||
error?: unknown;
|
|
||||||
}): never => {
|
|
||||||
const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
|
|
||||||
const message =
|
|
||||||
params.message?.trim() ||
|
|
||||||
(params.error ? describeUnknownError(params.error).trim() : "") ||
|
|
||||||
fallbackMessage;
|
|
||||||
const reason = resolveAuthProfileFailoverReason({
|
|
||||||
allInCooldown: params.allInCooldown,
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
if (fallbackConfigured) {
|
|
||||||
throw new FailoverError(message, {
|
|
||||||
reason,
|
|
||||||
provider,
|
|
||||||
model: modelId,
|
|
||||||
status: resolveFailoverStatus(reason),
|
|
||||||
cause: params.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (params.error instanceof Error) throw params.error;
|
|
||||||
throw new Error(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveApiKeyForCandidate = async (candidate?: string) => {
|
const resolveApiKeyForCandidate = async (candidate?: string) => {
|
||||||
return getApiKeyForModel({
|
return getApiKeyForModel({
|
||||||
model,
|
model,
|
||||||
@@ -277,17 +238,14 @@ export async function runEmbeddedPiAgent(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (profileIndex >= profileCandidates.length) {
|
if (profileIndex >= profileCandidates.length) {
|
||||||
throwAuthProfileFailover({ allInCooldown: true });
|
throw new Error(
|
||||||
|
`No available auth profile for ${provider} (all in cooldown or unavailable).`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof FailoverError) throw err;
|
if (profileCandidates[profileIndex] === lockedProfileId) throw err;
|
||||||
if (profileCandidates[profileIndex] === lockedProfileId) {
|
|
||||||
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
|
||||||
}
|
|
||||||
const advanced = await advanceAuthProfile();
|
const advanced = await advanceAuthProfile();
|
||||||
if (!advanced) {
|
if (!advanced) throw err;
|
||||||
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -306,10 +264,6 @@ export async function runEmbeddedPiAgent(
|
|||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
messageTo: params.messageTo,
|
messageTo: params.messageTo,
|
||||||
messageThreadId: params.messageThreadId,
|
messageThreadId: params.messageThreadId,
|
||||||
groupId: params.groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
spawnedBy: params.spawnedBy,
|
|
||||||
currentChannelId: params.currentChannelId,
|
currentChannelId: params.currentChannelId,
|
||||||
currentThreadTs: params.currentThreadTs,
|
currentThreadTs: params.currentThreadTs,
|
||||||
replyToMode: params.replyToMode,
|
replyToMode: params.replyToMode,
|
||||||
@@ -435,7 +389,9 @@ export async function runEmbeddedPiAgent(
|
|||||||
}
|
}
|
||||||
// FIX: Throw FailoverError for prompt errors when fallbacks configured
|
// FIX: Throw FailoverError for prompt errors when fallbacks configured
|
||||||
// This enables model fallback for quota/rate limit errors during prompt submission
|
// This enables model fallback for quota/rate limit errors during prompt submission
|
||||||
if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
|
const promptFallbackConfigured =
|
||||||
|
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||||
|
if (promptFallbackConfigured && isFailoverErrorMessage(errorText)) {
|
||||||
throw new FailoverError(errorText, {
|
throw new FailoverError(errorText, {
|
||||||
reason: promptFailoverReason ?? "unknown",
|
reason: promptFailoverReason ?? "unknown",
|
||||||
provider,
|
provider,
|
||||||
@@ -459,6 +415,8 @@ export async function runEmbeddedPiAgent(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fallbackConfigured =
|
||||||
|
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||||
const authFailure = isAuthAssistantError(lastAssistant);
|
const authFailure = isAuthAssistantError(lastAssistant);
|
||||||
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
||||||
const failoverFailure = isFailoverAssistantError(lastAssistant);
|
const failoverFailure = isFailoverAssistantError(lastAssistant);
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
|||||||
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||||
import { resolveUserPath } from "../../../utils.js";
|
import { resolveUserPath } from "../../../utils.js";
|
||||||
import { createCacheTrace } from "../../cache-trace.js";
|
import { createCacheTrace } from "../../cache-trace.js";
|
||||||
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
|
||||||
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
||||||
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||||
@@ -209,10 +208,6 @@ export async function runEmbeddedAttempt(
|
|||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
messageTo: params.messageTo,
|
messageTo: params.messageTo,
|
||||||
messageThreadId: params.messageThreadId,
|
messageThreadId: params.messageThreadId,
|
||||||
groupId: params.groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
spawnedBy: params.spawnedBy,
|
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
agentDir,
|
agentDir,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
@@ -459,16 +454,6 @@ export async function runEmbeddedAttempt(
|
|||||||
modelApi: params.model.api,
|
modelApi: params.model.api,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
});
|
});
|
||||||
const anthropicPayloadLogger = createAnthropicPayloadLogger({
|
|
||||||
env: process.env,
|
|
||||||
runId: params.runId,
|
|
||||||
sessionId: activeSession.sessionId,
|
|
||||||
sessionKey: params.sessionKey,
|
|
||||||
provider: params.provider,
|
|
||||||
modelId: params.modelId,
|
|
||||||
modelApi: params.model.api,
|
|
||||||
workspaceDir: params.workspaceDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai.
|
// Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai.
|
||||||
activeSession.agent.streamFn = streamSimple;
|
activeSession.agent.streamFn = streamSimple;
|
||||||
@@ -489,11 +474,6 @@ export async function runEmbeddedAttempt(
|
|||||||
});
|
});
|
||||||
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
|
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
|
||||||
}
|
}
|
||||||
if (anthropicPayloadLogger) {
|
|
||||||
activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(
|
|
||||||
activeSession.agent.streamFn,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prior = await sanitizeSessionHistory({
|
const prior = await sanitizeSessionHistory({
|
||||||
@@ -788,7 +768,6 @@ export async function runEmbeddedAttempt(
|
|||||||
messages: messagesSnapshot,
|
messages: messagesSnapshot,
|
||||||
note: promptError ? "prompt error" : undefined,
|
note: promptError ? "prompt error" : undefined,
|
||||||
});
|
});
|
||||||
anthropicPayloadLogger?.recordUsage(messagesSnapshot, promptError);
|
|
||||||
|
|
||||||
// Run agent_end hooks to allow plugins to analyze the conversation
|
// Run agent_end hooks to allow plugins to analyze the conversation
|
||||||
// This is fire-and-forget, so we don't await
|
// This is fire-and-forget, so we don't await
|
||||||
|
|||||||
@@ -27,14 +27,6 @@ export type RunEmbeddedPiAgentParams = {
|
|||||||
messageTo?: string;
|
messageTo?: string;
|
||||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||||
messageThreadId?: string | number;
|
messageThreadId?: string | number;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
/** Current channel ID for auto-threading (Slack). */
|
/** Current channel ID for auto-threading (Slack). */
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
/** Current thread timestamp for auto-threading (Slack). */
|
/** Current thread timestamp for auto-threading (Slack). */
|
||||||
|
|||||||
@@ -23,14 +23,6 @@ export type EmbeddedRunAttemptParams = {
|
|||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
messageTo?: string;
|
messageTo?: string;
|
||||||
messageThreadId?: string | number;
|
messageThreadId?: string | number;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function abortEmbeddedPiRun(sessionId: string): boolean {
|
|||||||
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
|
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
diag.debug(`aborting run: sessionId=${sessionId}`);
|
diag.info(`aborting run: sessionId=${sessionId}`);
|
||||||
handle.abort();
|
handle.abort();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ export function setActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueueH
|
|||||||
reason: wasActive ? "run_replaced" : "run_started",
|
reason: wasActive ? "run_replaced" : "run_started",
|
||||||
});
|
});
|
||||||
if (!sessionId.startsWith("probe-")) {
|
if (!sessionId.startsWith("probe-")) {
|
||||||
diag.debug(`run registered: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
diag.info(`run registered: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export function clearActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueu
|
|||||||
ACTIVE_EMBEDDED_RUNS.delete(sessionId);
|
ACTIVE_EMBEDDED_RUNS.delete(sessionId);
|
||||||
logSessionStateChange({ sessionId, state: "idle", reason: "run_completed" });
|
logSessionStateChange({ sessionId, state: "idle", reason: "run_completed" });
|
||||||
if (!sessionId.startsWith("probe-")) {
|
if (!sessionId.startsWith("probe-")) {
|
||||||
diag.debug(`run cleared: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
diag.info(`run cleared: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
||||||
}
|
}
|
||||||
notifyEmbeddedRunEnded(sessionId);
|
notifyEmbeddedRunEnded(sessionId);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -231,95 +231,6 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
expect(familyToolNames).not.toContain("apply_patch");
|
expect(familyToolNames).not.toContain("apply_patch");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should apply group tool policy overrides (group-specific beats wildcard)", () => {
|
|
||||||
const cfg: ClawdbotConfig = {
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
groups: {
|
|
||||||
"*": {
|
|
||||||
tools: { allow: ["read"] },
|
|
||||||
},
|
|
||||||
trusted: {
|
|
||||||
tools: { allow: ["read", "exec"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const trustedTools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:whatsapp:group:trusted",
|
|
||||||
messageProvider: "whatsapp",
|
|
||||||
workspaceDir: "/tmp/test-group-trusted",
|
|
||||||
agentDir: "/tmp/agent-group",
|
|
||||||
});
|
|
||||||
const trustedNames = trustedTools.map((t) => t.name);
|
|
||||||
expect(trustedNames).toContain("read");
|
|
||||||
expect(trustedNames).toContain("exec");
|
|
||||||
|
|
||||||
const defaultTools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:whatsapp:group:unknown",
|
|
||||||
messageProvider: "whatsapp",
|
|
||||||
workspaceDir: "/tmp/test-group-default",
|
|
||||||
agentDir: "/tmp/agent-group",
|
|
||||||
});
|
|
||||||
const defaultNames = defaultTools.map((t) => t.name);
|
|
||||||
expect(defaultNames).toContain("read");
|
|
||||||
expect(defaultNames).not.toContain("exec");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve telegram group tool policy for topic session keys", () => {
|
|
||||||
const cfg: ClawdbotConfig = {
|
|
||||||
channels: {
|
|
||||||
telegram: {
|
|
||||||
groups: {
|
|
||||||
"123": {
|
|
||||||
tools: { allow: ["read"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const tools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:telegram:group:123:topic:456",
|
|
||||||
messageProvider: "telegram",
|
|
||||||
workspaceDir: "/tmp/test-telegram-topic",
|
|
||||||
agentDir: "/tmp/agent-telegram",
|
|
||||||
});
|
|
||||||
const names = tools.map((t) => t.name);
|
|
||||||
expect(names).toContain("read");
|
|
||||||
expect(names).not.toContain("exec");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should inherit group tool policy for subagents from spawnedBy session keys", () => {
|
|
||||||
const cfg: ClawdbotConfig = {
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
groups: {
|
|
||||||
trusted: {
|
|
||||||
tools: { allow: ["read"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const tools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:subagent:test",
|
|
||||||
spawnedBy: "agent:main:whatsapp:group:trusted",
|
|
||||||
workspaceDir: "/tmp/test-subagent-group",
|
|
||||||
agentDir: "/tmp/agent-subagent",
|
|
||||||
});
|
|
||||||
const names = tools.map((t) => t.name);
|
|
||||||
expect(names).toContain("read");
|
|
||||||
expect(names).not.toContain("exec");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should apply global tool policy before agent-specific policy", () => {
|
it("should apply global tool policy before agent-specific policy", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
tools: {
|
tools: {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { getChannelDock } from "../channels/dock.js";
|
|
||||||
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
|
|
||||||
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
||||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||||
import type { SandboxToolPolicy } from "./sandbox.js";
|
import type { SandboxToolPolicy } from "./sandbox.js";
|
||||||
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
||||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
|
||||||
import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js";
|
|
||||||
|
|
||||||
type CompiledPattern =
|
type CompiledPattern =
|
||||||
| { kind: "all" }
|
| { kind: "all" }
|
||||||
@@ -112,26 +108,6 @@ function normalizeProviderKey(value: string): string {
|
|||||||
return value.trim().toLowerCase();
|
return value.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveGroupContextFromSessionKey(sessionKey?: string | null): {
|
|
||||||
channel?: string;
|
|
||||||
groupId?: string;
|
|
||||||
} {
|
|
||||||
const raw = (sessionKey ?? "").trim();
|
|
||||||
if (!raw) return {};
|
|
||||||
const base = resolveThreadParentSessionKey(raw) ?? raw;
|
|
||||||
const parts = base.split(":").filter(Boolean);
|
|
||||||
let body = parts[0] === "agent" ? parts.slice(2) : parts;
|
|
||||||
if (body[0] === "subagent") {
|
|
||||||
body = body.slice(1);
|
|
||||||
}
|
|
||||||
if (body.length < 3) return {};
|
|
||||||
const [channel, kind, ...rest] = body;
|
|
||||||
if (kind !== "group" && kind !== "channel") return {};
|
|
||||||
const groupId = rest.join(":").trim();
|
|
||||||
if (!groupId) return {};
|
|
||||||
return { channel: channel.trim().toLowerCase(), groupId };
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveProviderToolPolicy(params: {
|
function resolveProviderToolPolicy(params: {
|
||||||
byProvider?: Record<string, ToolPolicyConfig>;
|
byProvider?: Record<string, ToolPolicyConfig>;
|
||||||
modelProvider?: string;
|
modelProvider?: string;
|
||||||
@@ -198,47 +174,6 @@ export function resolveEffectiveToolPolicy(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGroupToolPolicy(params: {
|
|
||||||
config?: ClawdbotConfig;
|
|
||||||
sessionKey?: string;
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
messageProvider?: string;
|
|
||||||
groupId?: string | null;
|
|
||||||
groupChannel?: string | null;
|
|
||||||
groupSpace?: string | null;
|
|
||||||
accountId?: string | null;
|
|
||||||
}): SandboxToolPolicy | undefined {
|
|
||||||
if (!params.config) return undefined;
|
|
||||||
const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey);
|
|
||||||
const spawnedContext = resolveGroupContextFromSessionKey(params.spawnedBy);
|
|
||||||
const groupId = params.groupId ?? sessionContext.groupId ?? spawnedContext.groupId;
|
|
||||||
if (!groupId) return undefined;
|
|
||||||
const channelRaw = params.messageProvider ?? sessionContext.channel ?? spawnedContext.channel;
|
|
||||||
const channel = normalizeMessageChannel(channelRaw);
|
|
||||||
if (!channel) return undefined;
|
|
||||||
let dock;
|
|
||||||
try {
|
|
||||||
dock = getChannelDock(channel);
|
|
||||||
} catch {
|
|
||||||
dock = undefined;
|
|
||||||
}
|
|
||||||
const toolsConfig =
|
|
||||||
dock?.groups?.resolveToolPolicy?.({
|
|
||||||
cfg: params.config,
|
|
||||||
groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
accountId: params.accountId,
|
|
||||||
}) ??
|
|
||||||
resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.config,
|
|
||||||
channel,
|
|
||||||
groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
return pickToolPolicy(toolsConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isToolAllowedByPolicies(
|
export function isToolAllowedByPolicies(
|
||||||
name: string,
|
name: string,
|
||||||
policies: Array<SandboxToolPolicy | undefined>,
|
policies: Array<SandboxToolPolicy | undefined>,
|
||||||
|
|||||||
+28
-63
@@ -23,7 +23,6 @@ import {
|
|||||||
filterToolsByPolicy,
|
filterToolsByPolicy,
|
||||||
isToolAllowedByPolicies,
|
isToolAllowedByPolicies,
|
||||||
resolveEffectiveToolPolicy,
|
resolveEffectiveToolPolicy,
|
||||||
resolveGroupToolPolicy,
|
|
||||||
resolveSubagentToolPolicy,
|
resolveSubagentToolPolicy,
|
||||||
} from "./pi-tools.policy.js";
|
} from "./pi-tools.policy.js";
|
||||||
import {
|
import {
|
||||||
@@ -44,12 +43,10 @@ import {
|
|||||||
buildPluginToolGroups,
|
buildPluginToolGroups,
|
||||||
collectExplicitAllowlist,
|
collectExplicitAllowlist,
|
||||||
expandPolicyWithPluginGroups,
|
expandPolicyWithPluginGroups,
|
||||||
normalizeToolName,
|
|
||||||
resolveToolProfilePolicy,
|
resolveToolProfilePolicy,
|
||||||
stripPluginOnlyAllowlist,
|
stripPluginOnlyAllowlist,
|
||||||
} from "./tool-policy.js";
|
} from "./tool-policy.js";
|
||||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||||
import { logWarn } from "../logger.js";
|
|
||||||
|
|
||||||
function isOpenAIProvider(provider?: string) {
|
function isOpenAIProvider(provider?: string) {
|
||||||
const normalized = provider?.trim().toLowerCase();
|
const normalized = provider?.trim().toLowerCase();
|
||||||
@@ -131,14 +128,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
/** Current thread timestamp for auto-threading (Slack). */
|
/** Current thread timestamp for auto-threading (Slack). */
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent group policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
/** Reply-to mode for Slack auto-threading. */
|
/** Reply-to mode for Slack auto-threading. */
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||||
@@ -162,16 +151,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
modelProvider: options?.modelProvider,
|
modelProvider: options?.modelProvider,
|
||||||
modelId: options?.modelId,
|
modelId: options?.modelId,
|
||||||
});
|
});
|
||||||
const groupPolicy = resolveGroupToolPolicy({
|
|
||||||
config: options?.config,
|
|
||||||
sessionKey: options?.sessionKey,
|
|
||||||
spawnedBy: options?.spawnedBy,
|
|
||||||
messageProvider: options?.messageProvider,
|
|
||||||
groupId: options?.groupId,
|
|
||||||
groupChannel: options?.groupChannel,
|
|
||||||
groupSpace: options?.groupSpace,
|
|
||||||
accountId: options?.agentAccountId,
|
|
||||||
});
|
|
||||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||||
@@ -186,7 +165,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
globalProviderPolicy,
|
globalProviderPolicy,
|
||||||
agentPolicy,
|
agentPolicy,
|
||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
groupPolicy,
|
|
||||||
sandbox?.tools,
|
sandbox?.tools,
|
||||||
subagentPolicy,
|
subagentPolicy,
|
||||||
]);
|
]);
|
||||||
@@ -255,6 +233,11 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
const bashTool = {
|
||||||
|
...(execTool as unknown as AnyAgentTool),
|
||||||
|
name: "bash",
|
||||||
|
label: "bash",
|
||||||
|
} satisfies AnyAgentTool;
|
||||||
const processTool = createProcessTool({
|
const processTool = createProcessTool({
|
||||||
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
|
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
|
||||||
scopeKey,
|
scopeKey,
|
||||||
@@ -275,6 +258,7 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
: []),
|
: []),
|
||||||
...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
|
...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
|
||||||
execTool as unknown as AnyAgentTool,
|
execTool as unknown as AnyAgentTool,
|
||||||
|
bashTool,
|
||||||
processTool as unknown as AnyAgentTool,
|
processTool as unknown as AnyAgentTool,
|
||||||
// Channel docking: include channel-defined agent tools (login, etc.).
|
// Channel docking: include channel-defined agent tools (login, etc.).
|
||||||
...listChannelAgentTools({ cfg: options?.config }),
|
...listChannelAgentTools({ cfg: options?.config }),
|
||||||
@@ -289,9 +273,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
agentTo: options?.messageTo,
|
agentTo: options?.messageTo,
|
||||||
agentThreadId: options?.messageThreadId,
|
agentThreadId: options?.messageThreadId,
|
||||||
agentGroupId: options?.groupId ?? null,
|
|
||||||
agentGroupChannel: options?.groupChannel ?? null,
|
|
||||||
agentGroupSpace: options?.groupSpace ?? null,
|
|
||||||
agentDir: options?.agentDir,
|
agentDir: options?.agentDir,
|
||||||
sandboxRoot,
|
sandboxRoot,
|
||||||
workspaceDir: options?.workspaceDir,
|
workspaceDir: options?.workspaceDir,
|
||||||
@@ -304,7 +285,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
globalProviderPolicy,
|
globalProviderPolicy,
|
||||||
agentPolicy,
|
agentPolicy,
|
||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
groupPolicy,
|
|
||||||
sandbox?.tools,
|
sandbox?.tools,
|
||||||
subagentPolicy,
|
subagentPolicy,
|
||||||
]),
|
]),
|
||||||
@@ -315,46 +295,34 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
modelHasVision: options?.modelHasVision,
|
modelHasVision: options?.modelHasVision,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
const coreToolNames = new Set(
|
|
||||||
tools
|
|
||||||
.filter((tool) => !getPluginToolMeta(tool as AnyAgentTool))
|
|
||||||
.map((tool) => normalizeToolName(tool.name))
|
|
||||||
.filter(Boolean),
|
|
||||||
);
|
|
||||||
const pluginGroups = buildPluginToolGroups({
|
const pluginGroups = buildPluginToolGroups({
|
||||||
tools,
|
tools,
|
||||||
toolMeta: (tool) => getPluginToolMeta(tool as AnyAgentTool),
|
toolMeta: (tool) => getPluginToolMeta(tool as AnyAgentTool),
|
||||||
});
|
});
|
||||||
const resolvePolicy = (policy: typeof profilePolicy, label: string) => {
|
const profilePolicyExpanded = expandPolicyWithPluginGroups(
|
||||||
const resolved = stripPluginOnlyAllowlist(policy, pluginGroups, coreToolNames);
|
stripPluginOnlyAllowlist(profilePolicy, pluginGroups),
|
||||||
if (resolved.unknownAllowlist.length > 0) {
|
pluginGroups,
|
||||||
const entries = resolved.unknownAllowlist.join(", ");
|
|
||||||
const suffix = resolved.strippedAllowlist
|
|
||||||
? "Ignoring allowlist so core tools remain available."
|
|
||||||
: "These entries won't match any tool unless the plugin is enabled.";
|
|
||||||
logWarn(`tools: ${label} allowlist contains unknown entries (${entries}). ${suffix}`);
|
|
||||||
}
|
|
||||||
return expandPolicyWithPluginGroups(resolved.policy, pluginGroups);
|
|
||||||
};
|
|
||||||
const profilePolicyExpanded = resolvePolicy(
|
|
||||||
profilePolicy,
|
|
||||||
profile ? `tools.profile (${profile})` : "tools.profile",
|
|
||||||
);
|
);
|
||||||
const providerProfileExpanded = resolvePolicy(
|
const providerProfileExpanded = expandPolicyWithPluginGroups(
|
||||||
providerProfilePolicy,
|
stripPluginOnlyAllowlist(providerProfilePolicy, pluginGroups),
|
||||||
providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile",
|
pluginGroups,
|
||||||
);
|
);
|
||||||
const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow");
|
const globalPolicyExpanded = expandPolicyWithPluginGroups(
|
||||||
const globalProviderExpanded = resolvePolicy(globalProviderPolicy, "tools.byProvider.allow");
|
stripPluginOnlyAllowlist(globalPolicy, pluginGroups),
|
||||||
const agentPolicyExpanded = resolvePolicy(
|
pluginGroups,
|
||||||
agentPolicy,
|
|
||||||
agentId ? `agents.${agentId}.tools.allow` : "agent tools.allow",
|
|
||||||
);
|
);
|
||||||
const agentProviderExpanded = resolvePolicy(
|
const globalProviderExpanded = expandPolicyWithPluginGroups(
|
||||||
agentProviderPolicy,
|
stripPluginOnlyAllowlist(globalProviderPolicy, pluginGroups),
|
||||||
agentId ? `agents.${agentId}.tools.byProvider.allow` : "agent tools.byProvider.allow",
|
pluginGroups,
|
||||||
|
);
|
||||||
|
const agentPolicyExpanded = expandPolicyWithPluginGroups(
|
||||||
|
stripPluginOnlyAllowlist(agentPolicy, pluginGroups),
|
||||||
|
pluginGroups,
|
||||||
|
);
|
||||||
|
const agentProviderExpanded = expandPolicyWithPluginGroups(
|
||||||
|
stripPluginOnlyAllowlist(agentProviderPolicy, pluginGroups),
|
||||||
|
pluginGroups,
|
||||||
);
|
);
|
||||||
const groupPolicyExpanded = resolvePolicy(groupPolicy, "group tools.allow");
|
|
||||||
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
||||||
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
||||||
|
|
||||||
@@ -376,12 +344,9 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
const agentProviderFiltered = agentProviderExpanded
|
const agentProviderFiltered = agentProviderExpanded
|
||||||
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
||||||
: agentFiltered;
|
: agentFiltered;
|
||||||
const groupFiltered = groupPolicyExpanded
|
|
||||||
? filterToolsByPolicy(agentProviderFiltered, groupPolicyExpanded)
|
|
||||||
: agentProviderFiltered;
|
|
||||||
const sandboxed = sandboxPolicyExpanded
|
const sandboxed = sandboxPolicyExpanded
|
||||||
? filterToolsByPolicy(groupFiltered, sandboxPolicyExpanded)
|
? filterToolsByPolicy(agentProviderFiltered, sandboxPolicyExpanded)
|
||||||
: groupFiltered;
|
: agentProviderFiltered;
|
||||||
const subagentFiltered = subagentPolicyExpanded
|
const subagentFiltered = subagentPolicyExpanded
|
||||||
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
||||||
: sandboxed;
|
: sandboxed;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
expect(prompt).toContain("Reminder: commit your changes in this workspace after edits.");
|
expect(prompt).toContain("Reminder: commit your changes in this workspace after edits.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes user timezone when provided (12-hour)", () => {
|
it("includes user time when provided (12-hour)", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
userTimezone: "America/Chicago",
|
userTimezone: "America/Chicago",
|
||||||
@@ -133,10 +133,11 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Current Date & Time");
|
expect(prompt).toContain("## Current Date & Time");
|
||||||
expect(prompt).toContain("Time zone: America/Chicago");
|
expect(prompt).toContain("Monday, January 5th, 2026 — 3:26 PM (America/Chicago)");
|
||||||
|
expect(prompt).toContain("Time format: 12-hour");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes user timezone when provided (24-hour)", () => {
|
it("includes user time when provided (24-hour)", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
userTimezone: "America/Chicago",
|
userTimezone: "America/Chicago",
|
||||||
@@ -145,10 +146,11 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Current Date & Time");
|
expect(prompt).toContain("## Current Date & Time");
|
||||||
expect(prompt).toContain("Time zone: America/Chicago");
|
expect(prompt).toContain("Monday, January 5th, 2026 — 15:26 (America/Chicago)");
|
||||||
|
expect(prompt).toContain("Time format: 24-hour");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows timezone when only timezone is provided", () => {
|
it("shows UTC fallback when only timezone is provided", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
userTimezone: "America/Chicago",
|
userTimezone: "America/Chicago",
|
||||||
@@ -156,7 +158,9 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Current Date & Time");
|
expect(prompt).toContain("## Current Date & Time");
|
||||||
expect(prompt).toContain("Time zone: America/Chicago");
|
expect(prompt).toContain(
|
||||||
|
"Time zone: America/Chicago. Current time unknown; assume UTC for date/time references.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes model alias guidance when aliases are provided", () => {
|
it("includes model alias guidance when aliases are provided", () => {
|
||||||
|
|||||||
@@ -49,9 +49,22 @@ function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: bool
|
|||||||
return ["## User Identity", ownerLine, ""];
|
return ["## User Identity", ownerLine, ""];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTimeSection(params: { userTimezone?: string }) {
|
function buildTimeSection(params: {
|
||||||
if (!params.userTimezone) return [];
|
userTimezone?: string;
|
||||||
return ["## Current Date & Time", `Time zone: ${params.userTimezone}`, ""];
|
userTime?: string;
|
||||||
|
userTimeFormat?: ResolvedTimeFormat;
|
||||||
|
}) {
|
||||||
|
if (!params.userTimezone && !params.userTime) return [];
|
||||||
|
return [
|
||||||
|
"## Current Date & Time",
|
||||||
|
params.userTime
|
||||||
|
? `${params.userTime} (${params.userTimezone ?? "unknown"})`
|
||||||
|
: `Time zone: ${params.userTimezone}. Current time unknown; assume UTC for date/time references.`,
|
||||||
|
params.userTimeFormat
|
||||||
|
? `Time format: ${params.userTimeFormat === "24" ? "24-hour" : "12-hour"}`
|
||||||
|
: "",
|
||||||
|
"",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReplyTagsSection(isMinimal: boolean) {
|
function buildReplyTagsSection(isMinimal: boolean) {
|
||||||
@@ -199,7 +212,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
sessions_send: "Send a message to another session/sub-agent",
|
sessions_send: "Send a message to another session/sub-agent",
|
||||||
sessions_spawn: "Spawn a sub-agent session",
|
sessions_spawn: "Spawn a sub-agent session",
|
||||||
session_status:
|
session_status:
|
||||||
"Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
"Show a /status-equivalent status card (usage + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
||||||
image: "Analyze an image with the configured image model",
|
image: "Analyze an image with the configured image model",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,6 +302,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const reasoningLevel = params.reasoningLevel ?? "off";
|
const reasoningLevel = params.reasoningLevel ?? "off";
|
||||||
const userTimezone = params.userTimezone?.trim();
|
const userTimezone = params.userTimezone?.trim();
|
||||||
|
const userTime = params.userTime?.trim();
|
||||||
const skillsPrompt = params.skillsPrompt?.trim();
|
const skillsPrompt = params.skillsPrompt?.trim();
|
||||||
const heartbeatPrompt = params.heartbeatPrompt?.trim();
|
const heartbeatPrompt = params.heartbeatPrompt?.trim();
|
||||||
const heartbeatPromptLine = heartbeatPrompt
|
const heartbeatPromptLine = heartbeatPrompt
|
||||||
@@ -451,6 +465,8 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
...buildUserIdentitySection(ownerLine, isMinimal),
|
...buildUserIdentitySection(ownerLine, isMinimal),
|
||||||
...buildTimeSection({
|
...buildTimeSection({
|
||||||
userTimezone,
|
userTimezone,
|
||||||
|
userTime,
|
||||||
|
userTimeFormat: params.userTimeFormat,
|
||||||
}),
|
}),
|
||||||
"## Workspace Files (injected)",
|
"## Workspace Files (injected)",
|
||||||
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
||||||
|
|||||||
@@ -30,6 +30,12 @@
|
|||||||
"title": "Exec",
|
"title": "Exec",
|
||||||
"detailKeys": ["command"]
|
"detailKeys": ["command"]
|
||||||
},
|
},
|
||||||
|
"bash": {
|
||||||
|
"emoji": "🛠️",
|
||||||
|
"title": "Exec",
|
||||||
|
"label": "exec",
|
||||||
|
"detailKeys": ["command"]
|
||||||
|
},
|
||||||
"process": {
|
"process": {
|
||||||
"emoji": "🧰",
|
"emoji": "🧰",
|
||||||
"title": "Process",
|
"title": "Process",
|
||||||
|
|||||||
@@ -6,46 +6,20 @@ const pluginGroups: PluginToolGroups = {
|
|||||||
all: ["lobster", "workflow_tool"],
|
all: ["lobster", "workflow_tool"],
|
||||||
byPlugin: new Map([["lobster", ["lobster", "workflow_tool"]]]),
|
byPlugin: new Map([["lobster", ["lobster", "workflow_tool"]]]),
|
||||||
};
|
};
|
||||||
const coreTools = new Set(["read", "write", "exec", "session_status"]);
|
|
||||||
|
|
||||||
describe("stripPluginOnlyAllowlist", () => {
|
describe("stripPluginOnlyAllowlist", () => {
|
||||||
it("strips allowlist when it only targets plugin tools", () => {
|
it("strips allowlist when it only targets plugin tools", () => {
|
||||||
const policy = stripPluginOnlyAllowlist({ allow: ["lobster"] }, pluginGroups, coreTools);
|
const policy = stripPluginOnlyAllowlist({ allow: ["lobster"] }, pluginGroups);
|
||||||
expect(policy.policy?.allow).toBeUndefined();
|
expect(policy?.allow).toBeUndefined();
|
||||||
expect(policy.unknownAllowlist).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips allowlist when it only targets plugin groups", () => {
|
it("strips allowlist when it only targets plugin groups", () => {
|
||||||
const policy = stripPluginOnlyAllowlist({ allow: ["group:plugins"] }, pluginGroups, coreTools);
|
const policy = stripPluginOnlyAllowlist({ allow: ["group:plugins"] }, pluginGroups);
|
||||||
expect(policy.policy?.allow).toBeUndefined();
|
expect(policy?.allow).toBeUndefined();
|
||||||
expect(policy.unknownAllowlist).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps allowlist when it mixes plugin and core entries", () => {
|
it("keeps allowlist when it mixes plugin and core entries", () => {
|
||||||
const policy = stripPluginOnlyAllowlist(
|
const policy = stripPluginOnlyAllowlist({ allow: ["lobster", "read"] }, pluginGroups);
|
||||||
{ allow: ["lobster", "read"] },
|
expect(policy?.allow).toEqual(["lobster", "read"]);
|
||||||
pluginGroups,
|
|
||||||
coreTools,
|
|
||||||
);
|
|
||||||
expect(policy.policy?.allow).toEqual(["lobster", "read"]);
|
|
||||||
expect(policy.unknownAllowlist).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("strips allowlist with unknown entries when no core tools match", () => {
|
|
||||||
const emptyPlugins: PluginToolGroups = { all: [], byPlugin: new Map() };
|
|
||||||
const policy = stripPluginOnlyAllowlist({ allow: ["lobster"] }, emptyPlugins, coreTools);
|
|
||||||
expect(policy.policy?.allow).toBeUndefined();
|
|
||||||
expect(policy.unknownAllowlist).toEqual(["lobster"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps allowlist with core tools and reports unknown entries", () => {
|
|
||||||
const emptyPlugins: PluginToolGroups = { all: [], byPlugin: new Map() };
|
|
||||||
const policy = stripPluginOnlyAllowlist(
|
|
||||||
{ allow: ["read", "lobster"] },
|
|
||||||
emptyPlugins,
|
|
||||||
coreTools,
|
|
||||||
);
|
|
||||||
expect(policy.policy?.allow).toEqual(["read", "lobster"]);
|
|
||||||
expect(policy.unknownAllowlist).toEqual(["lobster"]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -6,8 +6,8 @@ describe("tool-policy", () => {
|
|||||||
const expanded = expandToolGroups(["group:runtime", "BASH", "apply-patch", "group:fs"]);
|
const expanded = expandToolGroups(["group:runtime", "BASH", "apply-patch", "group:fs"]);
|
||||||
const set = new Set(expanded);
|
const set = new Set(expanded);
|
||||||
expect(set.has("exec")).toBe(true);
|
expect(set.has("exec")).toBe(true);
|
||||||
|
expect(set.has("bash")).toBe(true);
|
||||||
expect(set.has("process")).toBe(true);
|
expect(set.has("process")).toBe(true);
|
||||||
expect(set.has("bash")).toBe(false);
|
|
||||||
expect(set.has("apply_patch")).toBe(true);
|
expect(set.has("apply_patch")).toBe(true);
|
||||||
expect(set.has("read")).toBe(true);
|
expect(set.has("read")).toBe(true);
|
||||||
expect(set.has("write")).toBe(true);
|
expect(set.has("write")).toBe(true);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
|
|||||||
// Basic workspace/file tools
|
// Basic workspace/file tools
|
||||||
"group:fs": ["read", "write", "edit", "apply_patch"],
|
"group:fs": ["read", "write", "edit", "apply_patch"],
|
||||||
// Host/runtime execution tools
|
// Host/runtime execution tools
|
||||||
"group:runtime": ["exec", "process"],
|
"group:runtime": ["exec", "bash", "process"],
|
||||||
// Session management tools
|
// Session management tools
|
||||||
"group:sessions": [
|
"group:sessions": [
|
||||||
"sessions_list",
|
"sessions_list",
|
||||||
@@ -95,12 +95,6 @@ export type PluginToolGroups = {
|
|||||||
byPlugin: Map<string, string[]>;
|
byPlugin: Map<string, string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AllowlistResolution = {
|
|
||||||
policy: ToolPolicyLike | undefined;
|
|
||||||
unknownAllowlist: string[];
|
|
||||||
strippedAllowlist: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function expandToolGroups(list?: string[]) {
|
export function expandToolGroups(list?: string[]) {
|
||||||
const normalized = normalizeToolList(list);
|
const normalized = normalizeToolList(list);
|
||||||
const expanded: string[] = [];
|
const expanded: string[] = [];
|
||||||
@@ -187,33 +181,17 @@ export function expandPolicyWithPluginGroups(
|
|||||||
export function stripPluginOnlyAllowlist(
|
export function stripPluginOnlyAllowlist(
|
||||||
policy: ToolPolicyLike | undefined,
|
policy: ToolPolicyLike | undefined,
|
||||||
groups: PluginToolGroups,
|
groups: PluginToolGroups,
|
||||||
coreTools: Set<string>,
|
): ToolPolicyLike | undefined {
|
||||||
): AllowlistResolution {
|
if (!policy?.allow || policy.allow.length === 0) return policy;
|
||||||
if (!policy?.allow || policy.allow.length === 0) {
|
|
||||||
return { policy, unknownAllowlist: [], strippedAllowlist: false };
|
|
||||||
}
|
|
||||||
const normalized = normalizeToolList(policy.allow);
|
const normalized = normalizeToolList(policy.allow);
|
||||||
if (normalized.length === 0) {
|
if (normalized.length === 0) return policy;
|
||||||
return { policy, unknownAllowlist: [], strippedAllowlist: false };
|
|
||||||
}
|
|
||||||
const pluginIds = new Set(groups.byPlugin.keys());
|
const pluginIds = new Set(groups.byPlugin.keys());
|
||||||
const pluginTools = new Set(groups.all);
|
const pluginTools = new Set(groups.all);
|
||||||
const unknownAllowlist: string[] = [];
|
const isPluginEntry = (entry: string) =>
|
||||||
let hasCoreEntry = false;
|
entry === "group:plugins" || pluginIds.has(entry) || pluginTools.has(entry);
|
||||||
for (const entry of normalized) {
|
const isPluginOnly = normalized.every((entry) => isPluginEntry(entry));
|
||||||
const isPluginEntry =
|
if (!isPluginOnly) return policy;
|
||||||
entry === "group:plugins" || pluginIds.has(entry) || pluginTools.has(entry);
|
return { ...policy, allow: undefined };
|
||||||
const expanded = expandToolGroups([entry]);
|
|
||||||
const isCoreEntry = expanded.some((tool) => coreTools.has(tool));
|
|
||||||
if (isCoreEntry) hasCoreEntry = true;
|
|
||||||
if (!isCoreEntry && !isPluginEntry) unknownAllowlist.push(entry);
|
|
||||||
}
|
|
||||||
const strippedAllowlist = !hasCoreEntry;
|
|
||||||
return {
|
|
||||||
policy: strippedAllowlist ? { ...policy, allow: undefined } : policy,
|
|
||||||
unknownAllowlist: Array.from(new Set(unknownAllowlist)),
|
|
||||||
strippedAllowlist,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined {
|
export function resolveToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const BROWSER_TOOL_ACTIONS = [
|
|||||||
"act",
|
"act",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const BROWSER_TARGETS = ["sandbox", "host", "custom", "node"] as const;
|
const BROWSER_TARGETS = ["sandbox", "host", "custom"] as const;
|
||||||
|
|
||||||
const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"] as const;
|
const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"] as const;
|
||||||
const BROWSER_SNAPSHOT_MODES = ["efficient"] as const;
|
const BROWSER_SNAPSHOT_MODES = ["efficient"] as const;
|
||||||
@@ -84,7 +84,6 @@ const BrowserActSchema = Type.Object({
|
|||||||
export const BrowserToolSchema = Type.Object({
|
export const BrowserToolSchema = Type.Object({
|
||||||
action: stringEnum(BROWSER_TOOL_ACTIONS),
|
action: stringEnum(BROWSER_TOOL_ACTIONS),
|
||||||
target: optionalStringEnum(BROWSER_TARGETS),
|
target: optionalStringEnum(BROWSER_TARGETS),
|
||||||
node: Type.Optional(Type.String()),
|
|
||||||
profile: Type.Optional(Type.String()),
|
profile: Type.Optional(Type.String()),
|
||||||
controlUrl: Type.Optional(Type.String()),
|
controlUrl: Type.Optional(Type.String()),
|
||||||
targetUrl: Type.Optional(Type.String()),
|
targetUrl: Type.Optional(Type.String()),
|
||||||
|
|||||||
@@ -49,25 +49,6 @@ const browserConfigMocks = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock("../../browser/config.js", () => browserConfigMocks);
|
vi.mock("../../browser/config.js", () => browserConfigMocks);
|
||||||
|
|
||||||
const nodesUtilsMocks = vi.hoisted(() => ({
|
|
||||||
listNodes: vi.fn(async () => []),
|
|
||||||
}));
|
|
||||||
vi.mock("./nodes-utils.js", async () => {
|
|
||||||
const actual = await vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js");
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
listNodes: nodesUtilsMocks.listNodes,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const gatewayMocks = vi.hoisted(() => ({
|
|
||||||
callGatewayTool: vi.fn(async () => ({
|
|
||||||
ok: true,
|
|
||||||
payload: { result: { ok: true, running: true } },
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
vi.mock("./gateway.js", () => gatewayMocks);
|
|
||||||
|
|
||||||
const configMocks = vi.hoisted(() => ({
|
const configMocks = vi.hoisted(() => ({
|
||||||
loadConfig: vi.fn(() => ({ browser: {} })),
|
loadConfig: vi.fn(() => ({ browser: {} })),
|
||||||
}));
|
}));
|
||||||
@@ -91,7 +72,6 @@ describe("browser tool snapshot maxChars", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
configMocks.loadConfig.mockReturnValue({ browser: {} });
|
configMocks.loadConfig.mockReturnValue({ browser: {} });
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies the default ai snapshot limit", async () => {
|
it("applies the default ai snapshot limit", async () => {
|
||||||
@@ -195,70 +175,6 @@ describe("browser tool snapshot maxChars", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("routes to node proxy when target=node", async () => {
|
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
||||||
{
|
|
||||||
nodeId: "node-1",
|
|
||||||
displayName: "Browser Node",
|
|
||||||
connected: true,
|
|
||||||
caps: ["browser"],
|
|
||||||
commands: ["browser.proxy"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const tool = createBrowserTool();
|
|
||||||
await tool.execute?.(null, { action: "status", target: "node" });
|
|
||||||
|
|
||||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
|
||||||
"node.invoke",
|
|
||||||
{ timeoutMs: 20000 },
|
|
||||||
expect.objectContaining({
|
|
||||||
nodeId: "node-1",
|
|
||||||
command: "browser.proxy",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps sandbox control url when node proxy is available", async () => {
|
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
||||||
{
|
|
||||||
nodeId: "node-1",
|
|
||||||
displayName: "Browser Node",
|
|
||||||
connected: true,
|
|
||||||
caps: ["browser"],
|
|
||||||
commands: ["browser.proxy"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const tool = createBrowserTool({ defaultControlUrl: "http://127.0.0.1:9999" });
|
|
||||||
await tool.execute?.(null, { action: "status" });
|
|
||||||
|
|
||||||
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
|
||||||
"http://127.0.0.1:9999",
|
|
||||||
expect.objectContaining({ profile: undefined }),
|
|
||||||
);
|
|
||||||
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps chrome profile on host when node proxy is available", async () => {
|
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
||||||
{
|
|
||||||
nodeId: "node-1",
|
|
||||||
displayName: "Browser Node",
|
|
||||||
connected: true,
|
|
||||||
caps: ["browser"],
|
|
||||||
commands: ["browser.proxy"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const tool = createBrowserTool();
|
|
||||||
await tool.execute?.(null, { action: "status", profile: "chrome" });
|
|
||||||
|
|
||||||
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
|
||||||
"http://127.0.0.1:18791",
|
|
||||||
expect.objectContaining({ profile: "chrome" }),
|
|
||||||
);
|
|
||||||
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("browser tool snapshot labels", () => {
|
describe("browser tool snapshot labels", () => {
|
||||||
|
|||||||
@@ -18,173 +18,11 @@ import {
|
|||||||
browserPdfSave,
|
browserPdfSave,
|
||||||
browserScreenshotAction,
|
browserScreenshotAction,
|
||||||
} from "../../browser/client-actions.js";
|
} from "../../browser/client-actions.js";
|
||||||
import crypto from "node:crypto";
|
|
||||||
|
|
||||||
import { resolveBrowserConfig } from "../../browser/config.js";
|
import { resolveBrowserConfig } from "../../browser/config.js";
|
||||||
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
|
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
import { saveMediaBuffer } from "../../media/store.js";
|
|
||||||
import { listNodes, resolveNodeIdFromList, type NodeListNode } from "./nodes-utils.js";
|
|
||||||
import { BrowserToolSchema } from "./browser-tool.schema.js";
|
import { BrowserToolSchema } from "./browser-tool.schema.js";
|
||||||
import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "./common.js";
|
import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "./common.js";
|
||||||
import { callGatewayTool } from "./gateway.js";
|
|
||||||
|
|
||||||
type BrowserProxyFile = {
|
|
||||||
path: string;
|
|
||||||
base64: string;
|
|
||||||
mimeType?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BrowserProxyResult = {
|
|
||||||
result: unknown;
|
|
||||||
files?: BrowserProxyFile[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_BROWSER_PROXY_TIMEOUT_MS = 20_000;
|
|
||||||
|
|
||||||
type BrowserNodeTarget = {
|
|
||||||
nodeId: string;
|
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isBrowserNode(node: NodeListNode) {
|
|
||||||
const caps = Array.isArray(node.caps) ? node.caps : [];
|
|
||||||
const commands = Array.isArray(node.commands) ? node.commands : [];
|
|
||||||
return caps.includes("browser") || commands.includes("browser.proxy");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveBrowserNodeTarget(params: {
|
|
||||||
requestedNode?: string;
|
|
||||||
target?: "sandbox" | "host" | "custom" | "node";
|
|
||||||
controlUrl?: string;
|
|
||||||
defaultControlUrl?: string;
|
|
||||||
}): Promise<BrowserNodeTarget | null> {
|
|
||||||
const cfg = loadConfig();
|
|
||||||
const policy = cfg.gateway?.nodes?.browser;
|
|
||||||
const mode = policy?.mode ?? "auto";
|
|
||||||
if (mode === "off") {
|
|
||||||
if (params.target === "node" || params.requestedNode) {
|
|
||||||
throw new Error("Node browser proxy is disabled (gateway.nodes.browser.mode=off).");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (params.defaultControlUrl?.trim() && params.target !== "node" && !params.requestedNode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (params.controlUrl?.trim()) return null;
|
|
||||||
if (params.target && params.target !== "node") return null;
|
|
||||||
if (mode === "manual" && params.target !== "node" && !params.requestedNode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = await listNodes({});
|
|
||||||
const browserNodes = nodes.filter((node) => node.connected && isBrowserNode(node));
|
|
||||||
if (browserNodes.length === 0) {
|
|
||||||
if (params.target === "node" || params.requestedNode) {
|
|
||||||
throw new Error("No connected browser-capable nodes.");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requested = params.requestedNode?.trim() || policy?.node?.trim();
|
|
||||||
if (requested) {
|
|
||||||
const nodeId = resolveNodeIdFromList(browserNodes, requested, false);
|
|
||||||
const node = browserNodes.find((entry) => entry.nodeId === nodeId);
|
|
||||||
return { nodeId, label: node?.displayName ?? node?.remoteIp ?? nodeId };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.target === "node") {
|
|
||||||
if (browserNodes.length === 1) {
|
|
||||||
const node = browserNodes[0]!;
|
|
||||||
return { nodeId: node.nodeId, label: node.displayName ?? node.remoteIp ?? node.nodeId };
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`Multiple browser-capable nodes connected (${browserNodes.length}). Set gateway.nodes.browser.node or pass node=<id>.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "manual") return null;
|
|
||||||
|
|
||||||
if (browserNodes.length === 1) {
|
|
||||||
const node = browserNodes[0]!;
|
|
||||||
return { nodeId: node.nodeId, label: node.displayName ?? node.remoteIp ?? node.nodeId };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function callBrowserProxy(params: {
|
|
||||||
nodeId: string;
|
|
||||||
method: string;
|
|
||||||
path: string;
|
|
||||||
query?: Record<string, string | number | boolean | undefined>;
|
|
||||||
body?: unknown;
|
|
||||||
timeoutMs?: number;
|
|
||||||
profile?: string;
|
|
||||||
}): Promise<BrowserProxyResult> {
|
|
||||||
const gatewayTimeoutMs =
|
|
||||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
|
||||||
? Math.max(1, Math.floor(params.timeoutMs))
|
|
||||||
: DEFAULT_BROWSER_PROXY_TIMEOUT_MS;
|
|
||||||
const payload = (await callGatewayTool(
|
|
||||||
"node.invoke",
|
|
||||||
{ timeoutMs: gatewayTimeoutMs },
|
|
||||||
{
|
|
||||||
nodeId: params.nodeId,
|
|
||||||
command: "browser.proxy",
|
|
||||||
params: {
|
|
||||||
method: params.method,
|
|
||||||
path: params.path,
|
|
||||||
query: params.query,
|
|
||||||
body: params.body,
|
|
||||||
timeoutMs: params.timeoutMs,
|
|
||||||
profile: params.profile,
|
|
||||||
},
|
|
||||||
idempotencyKey: crypto.randomUUID(),
|
|
||||||
},
|
|
||||||
)) as {
|
|
||||||
ok?: boolean;
|
|
||||||
payload?: BrowserProxyResult;
|
|
||||||
payloadJSON?: string | null;
|
|
||||||
};
|
|
||||||
const parsed =
|
|
||||||
payload?.payload ??
|
|
||||||
(typeof payload?.payloadJSON === "string" && payload.payloadJSON
|
|
||||||
? (JSON.parse(payload.payloadJSON) as BrowserProxyResult)
|
|
||||||
: null);
|
|
||||||
if (!parsed || typeof parsed !== "object") {
|
|
||||||
throw new Error("browser proxy failed");
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
|
|
||||||
if (!files || files.length === 0) return new Map<string, string>();
|
|
||||||
const mapping = new Map<string, string>();
|
|
||||||
for (const file of files) {
|
|
||||||
const buffer = Buffer.from(file.base64, "base64");
|
|
||||||
const saved = await saveMediaBuffer(buffer, file.mimeType, "browser", buffer.byteLength);
|
|
||||||
mapping.set(file.path, saved.path);
|
|
||||||
}
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyProxyPaths(result: unknown, mapping: Map<string, string>) {
|
|
||||||
if (!result || typeof result !== "object") return;
|
|
||||||
const obj = result as Record<string, unknown>;
|
|
||||||
if (typeof obj.path === "string" && mapping.has(obj.path)) {
|
|
||||||
obj.path = mapping.get(obj.path);
|
|
||||||
}
|
|
||||||
if (typeof obj.imagePath === "string" && mapping.has(obj.imagePath)) {
|
|
||||||
obj.imagePath = mapping.get(obj.imagePath);
|
|
||||||
}
|
|
||||||
const download = obj.download;
|
|
||||||
if (download && typeof download === "object") {
|
|
||||||
const d = download as Record<string, unknown>;
|
|
||||||
if (typeof d.path === "string" && mapping.has(d.path)) {
|
|
||||||
d.path = mapping.get(d.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveBrowserBaseUrl(params: {
|
function resolveBrowserBaseUrl(params: {
|
||||||
target?: "sandbox" | "host" | "custom";
|
target?: "sandbox" | "host" | "custom";
|
||||||
@@ -289,12 +127,11 @@ export function createBrowserTool(opts?: {
|
|||||||
"Control the browser via Clawdbot's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
|
"Control the browser via Clawdbot's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
|
||||||
'Profiles: use profile="chrome" for Chrome extension relay takeover (your existing Chrome tabs). Use profile="clawd" for the isolated clawd-managed browser.',
|
'Profiles: use profile="chrome" for Chrome extension relay takeover (your existing Chrome tabs). Use profile="clawd" for the isolated clawd-managed browser.',
|
||||||
'If the user mentions the Chrome extension / Browser Relay / toolbar button / “attach tab”, ALWAYS use profile="chrome" (do not ask which profile).',
|
'If the user mentions the Chrome extension / Browser Relay / toolbar button / “attach tab”, ALWAYS use profile="chrome" (do not ask which profile).',
|
||||||
'When a node-hosted browser proxy is available, the tool may auto-route to it. Pin a node with node=<id|name> or target="node".',
|
|
||||||
"Chrome extension relay needs an attached tab: user must click the Clawdbot Browser Relay toolbar icon on the tab (badge ON). If no tab is connected, ask them to attach it.",
|
"Chrome extension relay needs an attached tab: user must click the Clawdbot Browser Relay toolbar icon on the tab (badge ON). If no tab is connected, ask them to attach it.",
|
||||||
"When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).",
|
"When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).",
|
||||||
'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
|
'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
|
||||||
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
|
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
|
||||||
`target selects browser location (sandbox|host|custom|node). Default: ${targetDefault}.`,
|
`target selects browser location (sandbox|host|custom). Default: ${targetDefault}.`,
|
||||||
"controlUrl implies target=custom (remote control server).",
|
"controlUrl implies target=custom (remote control server).",
|
||||||
hostHint,
|
hostHint,
|
||||||
allowlistHint,
|
allowlistHint,
|
||||||
@@ -305,184 +142,49 @@ export function createBrowserTool(opts?: {
|
|||||||
const action = readStringParam(params, "action", { required: true });
|
const action = readStringParam(params, "action", { required: true });
|
||||||
const controlUrl = readStringParam(params, "controlUrl");
|
const controlUrl = readStringParam(params, "controlUrl");
|
||||||
const profile = readStringParam(params, "profile");
|
const profile = readStringParam(params, "profile");
|
||||||
const requestedNode = readStringParam(params, "node");
|
let target = readStringParam(params, "target") as "sandbox" | "host" | "custom" | undefined;
|
||||||
let target = readStringParam(params, "target") as
|
if (profile === "chrome" && !target && !controlUrl?.trim()) {
|
||||||
| "sandbox"
|
// Chrome extension relay takeover is a host Chrome feature; default to host even in sandboxed sessions.
|
||||||
| "host"
|
|
||||||
| "custom"
|
|
||||||
| "node"
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (controlUrl?.trim() && (target === "node" || requestedNode)) {
|
|
||||||
throw new Error('controlUrl is not supported with target="node".');
|
|
||||||
}
|
|
||||||
if (target === "custom" && requestedNode) {
|
|
||||||
throw new Error('node is not supported with target="custom".');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!target && !controlUrl?.trim() && !requestedNode && profile === "chrome") {
|
|
||||||
// Chrome extension relay takeover is a host Chrome feature; prefer host unless explicitly targeting a node.
|
|
||||||
target = "host";
|
target = "host";
|
||||||
}
|
}
|
||||||
|
const baseUrl = resolveBrowserBaseUrl({
|
||||||
const nodeTarget = await resolveBrowserNodeTarget({
|
|
||||||
requestedNode: requestedNode ?? undefined,
|
|
||||||
target,
|
target,
|
||||||
controlUrl,
|
controlUrl,
|
||||||
defaultControlUrl: opts?.defaultControlUrl,
|
defaultControlUrl: opts?.defaultControlUrl,
|
||||||
|
allowHostControl: opts?.allowHostControl,
|
||||||
|
allowedControlUrls: opts?.allowedControlUrls,
|
||||||
|
allowedControlHosts: opts?.allowedControlHosts,
|
||||||
|
allowedControlPorts: opts?.allowedControlPorts,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedTarget = target === "node" ? undefined : target;
|
|
||||||
const baseUrl = nodeTarget
|
|
||||||
? ""
|
|
||||||
: resolveBrowserBaseUrl({
|
|
||||||
target: resolvedTarget,
|
|
||||||
controlUrl,
|
|
||||||
defaultControlUrl: opts?.defaultControlUrl,
|
|
||||||
allowHostControl: opts?.allowHostControl,
|
|
||||||
allowedControlUrls: opts?.allowedControlUrls,
|
|
||||||
allowedControlHosts: opts?.allowedControlHosts,
|
|
||||||
allowedControlPorts: opts?.allowedControlPorts,
|
|
||||||
});
|
|
||||||
|
|
||||||
const proxyRequest = nodeTarget
|
|
||||||
? async (opts: {
|
|
||||||
method: string;
|
|
||||||
path: string;
|
|
||||||
query?: Record<string, string | number | boolean | undefined>;
|
|
||||||
body?: unknown;
|
|
||||||
timeoutMs?: number;
|
|
||||||
profile?: string;
|
|
||||||
}) => {
|
|
||||||
const proxy = await callBrowserProxy({
|
|
||||||
nodeId: nodeTarget.nodeId,
|
|
||||||
method: opts.method,
|
|
||||||
path: opts.path,
|
|
||||||
query: opts.query,
|
|
||||||
body: opts.body,
|
|
||||||
timeoutMs: opts.timeoutMs,
|
|
||||||
profile: opts.profile,
|
|
||||||
});
|
|
||||||
const mapping = await persistProxyFiles(proxy.files);
|
|
||||||
applyProxyPaths(proxy.result, mapping);
|
|
||||||
return proxy.result;
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "status":
|
case "status":
|
||||||
if (proxyRequest) {
|
|
||||||
return jsonResult(
|
|
||||||
await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/",
|
|
||||||
profile,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||||
case "start":
|
case "start":
|
||||||
if (proxyRequest) {
|
|
||||||
await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/start",
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return jsonResult(
|
|
||||||
await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/",
|
|
||||||
profile,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await browserStart(baseUrl, { profile });
|
await browserStart(baseUrl, { profile });
|
||||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||||
case "stop":
|
case "stop":
|
||||||
if (proxyRequest) {
|
|
||||||
await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/stop",
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return jsonResult(
|
|
||||||
await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/",
|
|
||||||
profile,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await browserStop(baseUrl, { profile });
|
await browserStop(baseUrl, { profile });
|
||||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||||
case "profiles":
|
case "profiles":
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/profiles",
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult({ profiles: await browserProfiles(baseUrl) });
|
return jsonResult({ profiles: await browserProfiles(baseUrl) });
|
||||||
case "tabs":
|
case "tabs":
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/tabs",
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
const tabs = (result as { tabs?: unknown[] }).tabs ?? [];
|
|
||||||
return jsonResult({ tabs });
|
|
||||||
}
|
|
||||||
return jsonResult({ tabs: await browserTabs(baseUrl, { profile }) });
|
return jsonResult({ tabs: await browserTabs(baseUrl, { profile }) });
|
||||||
case "open": {
|
case "open": {
|
||||||
const targetUrl = readStringParam(params, "targetUrl", {
|
const targetUrl = readStringParam(params, "targetUrl", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/tabs/open",
|
|
||||||
profile,
|
|
||||||
body: { url: targetUrl },
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
|
return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
|
||||||
}
|
}
|
||||||
case "focus": {
|
case "focus": {
|
||||||
const targetId = readStringParam(params, "targetId", {
|
const targetId = readStringParam(params, "targetId", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/tabs/focus",
|
|
||||||
profile,
|
|
||||||
body: { targetId },
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
await browserFocusTab(baseUrl, targetId, { profile });
|
await browserFocusTab(baseUrl, targetId, { profile });
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
case "close": {
|
case "close": {
|
||||||
const targetId = readStringParam(params, "targetId");
|
const targetId = readStringParam(params, "targetId");
|
||||||
if (proxyRequest) {
|
|
||||||
const result = targetId
|
|
||||||
? await proxyRequest({
|
|
||||||
method: "DELETE",
|
|
||||||
path: `/tabs/${encodeURIComponent(targetId)}`,
|
|
||||||
profile,
|
|
||||||
})
|
|
||||||
: await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/act",
|
|
||||||
profile,
|
|
||||||
body: { kind: "close" },
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
if (targetId) await browserCloseTab(baseUrl, targetId, { profile });
|
if (targetId) await browserCloseTab(baseUrl, targetId, { profile });
|
||||||
else await browserAct(baseUrl, { kind: "close" }, { profile });
|
else await browserAct(baseUrl, { kind: "close" }, { profile });
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
@@ -530,41 +232,21 @@ export function createBrowserTool(opts?: {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const selector = typeof params.selector === "string" ? params.selector.trim() : undefined;
|
const selector = typeof params.selector === "string" ? params.selector.trim() : undefined;
|
||||||
const frame = typeof params.frame === "string" ? params.frame.trim() : undefined;
|
const frame = typeof params.frame === "string" ? params.frame.trim() : undefined;
|
||||||
const snapshot = proxyRequest
|
const snapshot = await browserSnapshot(baseUrl, {
|
||||||
? ((await proxyRequest({
|
format,
|
||||||
method: "GET",
|
targetId,
|
||||||
path: "/snapshot",
|
limit,
|
||||||
profile,
|
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
||||||
query: {
|
refs,
|
||||||
format,
|
interactive,
|
||||||
targetId,
|
compact,
|
||||||
limit,
|
depth,
|
||||||
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
selector,
|
||||||
refs,
|
frame,
|
||||||
interactive,
|
labels,
|
||||||
compact,
|
mode,
|
||||||
depth,
|
profile,
|
||||||
selector,
|
});
|
||||||
frame,
|
|
||||||
labels,
|
|
||||||
mode,
|
|
||||||
},
|
|
||||||
})) as Awaited<ReturnType<typeof browserSnapshot>>)
|
|
||||||
: await browserSnapshot(baseUrl, {
|
|
||||||
format,
|
|
||||||
targetId,
|
|
||||||
limit,
|
|
||||||
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
|
||||||
refs,
|
|
||||||
interactive,
|
|
||||||
compact,
|
|
||||||
depth,
|
|
||||||
selector,
|
|
||||||
frame,
|
|
||||||
labels,
|
|
||||||
mode,
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
if (snapshot.format === "ai") {
|
if (snapshot.format === "ai") {
|
||||||
if (labels && snapshot.imagePath) {
|
if (labels && snapshot.imagePath) {
|
||||||
return await imageResultFromFile({
|
return await imageResultFromFile({
|
||||||
@@ -587,27 +269,14 @@ export function createBrowserTool(opts?: {
|
|||||||
const ref = readStringParam(params, "ref");
|
const ref = readStringParam(params, "ref");
|
||||||
const element = readStringParam(params, "element");
|
const element = readStringParam(params, "element");
|
||||||
const type = params.type === "jpeg" ? "jpeg" : "png";
|
const type = params.type === "jpeg" ? "jpeg" : "png";
|
||||||
const result = proxyRequest
|
const result = await browserScreenshotAction(baseUrl, {
|
||||||
? ((await proxyRequest({
|
targetId,
|
||||||
method: "POST",
|
fullPage,
|
||||||
path: "/screenshot",
|
ref,
|
||||||
profile,
|
element,
|
||||||
body: {
|
type,
|
||||||
targetId,
|
profile,
|
||||||
fullPage,
|
});
|
||||||
ref,
|
|
||||||
element,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
})) as Awaited<ReturnType<typeof browserScreenshotAction>>)
|
|
||||||
: await browserScreenshotAction(baseUrl, {
|
|
||||||
targetId,
|
|
||||||
fullPage,
|
|
||||||
ref,
|
|
||||||
element,
|
|
||||||
type,
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return await imageResultFromFile({
|
return await imageResultFromFile({
|
||||||
label: "browser:screenshot",
|
label: "browser:screenshot",
|
||||||
path: result.path,
|
path: result.path,
|
||||||
@@ -619,18 +288,6 @@ export function createBrowserTool(opts?: {
|
|||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
const targetId = readStringParam(params, "targetId");
|
const targetId = readStringParam(params, "targetId");
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/navigate",
|
|
||||||
profile,
|
|
||||||
body: {
|
|
||||||
url: targetUrl,
|
|
||||||
targetId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(
|
return jsonResult(
|
||||||
await browserNavigate(baseUrl, {
|
await browserNavigate(baseUrl, {
|
||||||
url: targetUrl,
|
url: targetUrl,
|
||||||
@@ -642,30 +299,11 @@ export function createBrowserTool(opts?: {
|
|||||||
case "console": {
|
case "console": {
|
||||||
const level = typeof params.level === "string" ? params.level.trim() : undefined;
|
const level = typeof params.level === "string" ? params.level.trim() : undefined;
|
||||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/console",
|
|
||||||
profile,
|
|
||||||
query: {
|
|
||||||
level,
|
|
||||||
targetId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(await browserConsoleMessages(baseUrl, { level, targetId, profile }));
|
return jsonResult(await browserConsoleMessages(baseUrl, { level, targetId, profile }));
|
||||||
}
|
}
|
||||||
case "pdf": {
|
case "pdf": {
|
||||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||||
const result = proxyRequest
|
const result = await browserPdfSave(baseUrl, { targetId, profile });
|
||||||
? ((await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/pdf",
|
|
||||||
profile,
|
|
||||||
body: { targetId },
|
|
||||||
})) as Awaited<ReturnType<typeof browserPdfSave>>)
|
|
||||||
: await browserPdfSave(baseUrl, { targetId, profile });
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `FILE:${result.path}` }],
|
content: [{ type: "text", text: `FILE:${result.path}` }],
|
||||||
details: result,
|
details: result,
|
||||||
@@ -682,22 +320,6 @@ export function createBrowserTool(opts?: {
|
|||||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||||
? params.timeoutMs
|
? params.timeoutMs
|
||||||
: undefined;
|
: undefined;
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/hooks/file-chooser",
|
|
||||||
profile,
|
|
||||||
body: {
|
|
||||||
paths,
|
|
||||||
ref,
|
|
||||||
inputRef,
|
|
||||||
element,
|
|
||||||
targetId,
|
|
||||||
timeoutMs,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(
|
return jsonResult(
|
||||||
await browserArmFileChooser(baseUrl, {
|
await browserArmFileChooser(baseUrl, {
|
||||||
paths,
|
paths,
|
||||||
@@ -718,20 +340,6 @@ export function createBrowserTool(opts?: {
|
|||||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||||
? params.timeoutMs
|
? params.timeoutMs
|
||||||
: undefined;
|
: undefined;
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/hooks/dialog",
|
|
||||||
profile,
|
|
||||||
body: {
|
|
||||||
accept,
|
|
||||||
promptText,
|
|
||||||
targetId,
|
|
||||||
timeoutMs,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(
|
return jsonResult(
|
||||||
await browserArmDialog(baseUrl, {
|
await browserArmDialog(baseUrl, {
|
||||||
accept,
|
accept,
|
||||||
@@ -748,29 +356,14 @@ export function createBrowserTool(opts?: {
|
|||||||
throw new Error("request required");
|
throw new Error("request required");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = proxyRequest
|
const result = await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
|
||||||
? await proxyRequest({
|
profile,
|
||||||
method: "POST",
|
});
|
||||||
path: "/act",
|
|
||||||
profile,
|
|
||||||
body: request,
|
|
||||||
})
|
|
||||||
: await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
return jsonResult(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = String(err);
|
const msg = String(err);
|
||||||
if (msg.includes("404:") && msg.includes("tab not found") && profile === "chrome") {
|
if (msg.includes("404:") && msg.includes("tab not found") && profile === "chrome") {
|
||||||
const tabs = proxyRequest
|
const tabs = await browserTabs(baseUrl, { profile }).catch(() => []);
|
||||||
? ((
|
|
||||||
(await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/tabs",
|
|
||||||
profile,
|
|
||||||
})) as { tabs?: unknown[] }
|
|
||||||
).tabs ?? [])
|
|
||||||
: await browserTabs(baseUrl, { profile }).catch(() => []);
|
|
||||||
if (!tabs.length) {
|
if (!tabs.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No Chrome tabs are attached via the Clawdbot Browser Relay extension. Click the toolbar icon on the tab you want to control (badge ON), then retry.",
|
"No Chrome tabs are attached via the Clawdbot Browser Relay extension. Click the toolbar icon on the tab you want to control (badge ON), then retry.",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
resolveDefaultModelForAgent,
|
resolveDefaultModelForAgent,
|
||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
|
||||||
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
|
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
|
||||||
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
|
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
|
||||||
import { buildStatusMessage } from "../../auto-reply/status.js";
|
import { buildStatusMessage } from "../../auto-reply/status.js";
|
||||||
@@ -216,7 +215,7 @@ export function createSessionStatusTool(opts?: {
|
|||||||
label: "Session Status",
|
label: "Session Status",
|
||||||
name: "session_status",
|
name: "session_status",
|
||||||
description:
|
description:
|
||||||
"Show a /status-equivalent session status card (usage + time + cost when available). Use for model-use questions (📊 session_status). Optional: set per-session model override (model=default resets overrides).",
|
"Show a /status-equivalent session status card (usage + cost when available). Use for model-use questions (📊 session_status). Optional: set per-session model override (model=default resets overrides).",
|
||||||
parameters: SessionStatusToolSchema,
|
parameters: SessionStatusToolSchema,
|
||||||
execute: async (_toolCallId, args) => {
|
execute: async (_toolCallId, args) => {
|
||||||
const params = args as Record<string, unknown>;
|
const params = args as Record<string, unknown>;
|
||||||
@@ -325,13 +324,6 @@ export function createSessionStatusTool(opts?: {
|
|||||||
resolved.entry.queueDebounceMs ?? resolved.entry.queueCap ?? resolved.entry.queueDrop,
|
resolved.entry.queueDebounceMs ?? resolved.entry.queueCap ?? resolved.entry.queueDrop,
|
||||||
);
|
);
|
||||||
|
|
||||||
const userTimezone = resolveUserTimezone(cfg.agents?.defaults?.userTimezone);
|
|
||||||
const userTimeFormat = resolveUserTimeFormat(cfg.agents?.defaults?.timeFormat);
|
|
||||||
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
|
||||||
const timeLine = userTime
|
|
||||||
? `🕒 Time: ${userTime} (${userTimezone})`
|
|
||||||
: `🕒 Time zone: ${userTimezone}`;
|
|
||||||
|
|
||||||
const agentDefaults = cfg.agents?.defaults ?? {};
|
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||||
const defaultLabel = `${configured.provider}/${configured.model}`;
|
const defaultLabel = `${configured.provider}/${configured.model}`;
|
||||||
const agentModel =
|
const agentModel =
|
||||||
@@ -354,7 +346,6 @@ export function createSessionStatusTool(opts?: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
}),
|
}),
|
||||||
usageLine,
|
usageLine,
|
||||||
timeLine,
|
|
||||||
queue: {
|
queue: {
|
||||||
mode: queueSettings.mode,
|
mode: queueSettings.mode,
|
||||||
depth: queueDepth,
|
depth: queueDepth,
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
agentTo?: string;
|
agentTo?: string;
|
||||||
agentThreadId?: string | number;
|
agentThreadId?: string | number;
|
||||||
agentGroupId?: string | null;
|
|
||||||
agentGroupChannel?: string | null;
|
|
||||||
agentGroupSpace?: string | null;
|
|
||||||
sandboxed?: boolean;
|
sandboxed?: boolean;
|
||||||
}): AnyAgentTool {
|
}): AnyAgentTool {
|
||||||
return {
|
return {
|
||||||
@@ -156,7 +153,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
||||||
const spawnedByKey = requesterInternalKey;
|
const shouldPatchSpawnedBy = opts?.sandboxed === true;
|
||||||
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
||||||
const resolvedModel =
|
const resolvedModel =
|
||||||
normalizeModelSelection(modelOverride) ??
|
normalizeModelSelection(modelOverride) ??
|
||||||
@@ -222,10 +219,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
thinking: thinkingOverride,
|
thinking: thinkingOverride,
|
||||||
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
||||||
label: label || undefined,
|
label: label || undefined,
|
||||||
spawnedBy: spawnedByKey,
|
spawnedBy: shouldPatchSpawnedBy ? requesterInternalKey : undefined,
|
||||||
groupId: opts?.agentGroupId ?? undefined,
|
|
||||||
groupChannel: opts?.agentGroupChannel ?? undefined,
|
|
||||||
groupSpace: opts?.agentGroupSpace ?? undefined,
|
|
||||||
},
|
},
|
||||||
timeoutMs: 10_000,
|
timeoutMs: 10_000,
|
||||||
})) as { runId?: string };
|
})) as { runId?: string };
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "./heartbeat.js";
|
||||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
|
||||||
isHeartbeatContentEffectivelyEmpty,
|
|
||||||
stripHeartbeatToken,
|
|
||||||
} from "./heartbeat.js";
|
|
||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||||
|
|
||||||
describe("stripHeartbeatToken", () => {
|
describe("stripHeartbeatToken", () => {
|
||||||
@@ -109,76 +105,3 @@ describe("stripHeartbeatToken", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isHeartbeatContentEffectivelyEmpty", () => {
|
|
||||||
it("returns false for undefined/null (missing file should not skip)", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(undefined)).toBe(false);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(null)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for empty string", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for whitespace only", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(" ")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("\n\n\n")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(" \n \n ")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("\t\t")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for header-only content", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n\n")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for comments only", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# Header\n# Another comment")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("## Subheader\n### Another")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for default template content (header + comment)", () => {
|
|
||||||
const defaultTemplate = `# HEARTBEAT.md
|
|
||||||
|
|
||||||
Keep this file empty unless you want a tiny checklist. Keep it small.
|
|
||||||
`;
|
|
||||||
// Note: The template has actual text content, so it's NOT effectively empty
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(defaultTemplate)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for header with only empty lines", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n\n\n")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false when actionable content exists", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("- Check email")).toBe(false);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n- Task 1")).toBe(false);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("Remind me to call mom")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for content with tasks after header", () => {
|
|
||||||
const content = `# HEARTBEAT.md
|
|
||||||
|
|
||||||
- Task 1
|
|
||||||
- Task 2
|
|
||||||
`;
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(content)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for mixed content with non-comment text", () => {
|
|
||||||
const content = `# HEARTBEAT.md
|
|
||||||
## Tasks
|
|
||||||
Check the server logs
|
|
||||||
`;
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(content)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("treats markdown headers as comments (effectively empty)", () => {
|
|
||||||
const content = `# HEARTBEAT.md
|
|
||||||
## Section 1
|
|
||||||
### Subsection
|
|
||||||
`;
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(content)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,38 +7,6 @@ export const HEARTBEAT_PROMPT =
|
|||||||
export const DEFAULT_HEARTBEAT_EVERY = "30m";
|
export const DEFAULT_HEARTBEAT_EVERY = "30m";
|
||||||
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300;
|
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300;
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if HEARTBEAT.md content is "effectively empty" - meaning it has no actionable tasks.
|
|
||||||
* This allows skipping heartbeat API calls when no tasks are configured.
|
|
||||||
*
|
|
||||||
* A file is considered effectively empty if it contains only:
|
|
||||||
* - Whitespace
|
|
||||||
* - Comment lines (lines starting with #)
|
|
||||||
* - Empty lines
|
|
||||||
*
|
|
||||||
* Note: A missing file returns false (not effectively empty) so the LLM can still
|
|
||||||
* decide what to do. This function is only for when the file exists but has no content.
|
|
||||||
*/
|
|
||||||
export function isHeartbeatContentEffectivelyEmpty(content: string | undefined | null): boolean {
|
|
||||||
if (content === undefined || content === null) return false;
|
|
||||||
if (typeof content !== "string") return false;
|
|
||||||
|
|
||||||
const lines = content.split("\n");
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
// Skip empty lines
|
|
||||||
if (!trimmed) continue;
|
|
||||||
// Skip markdown header lines (# followed by space or EOL, ## etc)
|
|
||||||
// This intentionally does NOT skip lines like "#TODO" or "#hashtag" which might be content
|
|
||||||
// (Those aren't valid markdown headers - ATX headers require space after #)
|
|
||||||
if (/^#+(\s|$)/.test(trimmed)) continue;
|
|
||||||
// Found a non-empty, non-comment line - there's actionable content
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// All lines were either empty or comments
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveHeartbeatPrompt(raw?: string): string {
|
export function resolveHeartbeatPrompt(raw?: string): string {
|
||||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||||
return trimmed || HEARTBEAT_PROMPT;
|
return trimmed || HEARTBEAT_PROMPT;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from "../../agents/pi-embedded-helpers.js";
|
} from "../../agents/pi-embedded-helpers.js";
|
||||||
import {
|
import {
|
||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
resolveGroupSessionKey,
|
|
||||||
resolveSessionTranscriptPath,
|
resolveSessionTranscriptPath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
updateSessionStore,
|
updateSessionStore,
|
||||||
@@ -215,10 +214,6 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
agentAccountId: params.sessionCtx.AccountId,
|
agentAccountId: params.sessionCtx.AccountId,
|
||||||
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
|
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
|
||||||
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
|
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
|
||||||
groupId: resolveGroupSessionKey(params.sessionCtx)?.id,
|
|
||||||
groupChannel:
|
|
||||||
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(),
|
|
||||||
groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined,
|
|
||||||
// Provider threading context for tool auto-injection
|
// Provider threading context for tool auto-injection
|
||||||
...buildThreadingToolContext({
|
...buildThreadingToolContext({
|
||||||
sessionCtx: params.sessionCtx,
|
sessionCtx: params.sessionCtx,
|
||||||
|
|||||||
@@ -67,10 +67,6 @@ export const handleCompactCommand: CommandHandler = async (params) => {
|
|||||||
sessionId,
|
sessionId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
messageChannel: params.command.channel,
|
messageChannel: params.command.channel,
|
||||||
groupId: params.sessionEntry.groupId,
|
|
||||||
groupChannel: params.sessionEntry.groupChannel,
|
|
||||||
groupSpace: params.sessionEntry.space,
|
|
||||||
spawnedBy: params.sessionEntry.spawnedBy,
|
|
||||||
sessionFile: resolveSessionFilePath(sessionId, params.sessionEntry),
|
sessionFile: resolveSessionFilePath(sessionId, params.sessionEntry),
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
|
|||||||
@@ -81,10 +81,6 @@ async function resolveContextReport(
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
messageProvider: params.command.channel,
|
messageProvider: params.command.channel,
|
||||||
groupId: params.sessionEntry?.groupId ?? undefined,
|
|
||||||
groupChannel: params.sessionEntry?.groupChannel ?? undefined,
|
|
||||||
groupSpace: params.sessionEntry?.space ?? undefined,
|
|
||||||
spawnedBy: params.sessionEntry?.spawnedBy ?? undefined,
|
|
||||||
modelProvider: params.provider,
|
modelProvider: params.provider,
|
||||||
modelId: params.model,
|
modelId: params.model,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
handleStopCommand,
|
handleStopCommand,
|
||||||
handleUsageCommand,
|
handleUsageCommand,
|
||||||
} from "./commands-session.js";
|
} from "./commands-session.js";
|
||||||
import { handlePluginCommand } from "./commands-plugin.js";
|
|
||||||
import type {
|
import type {
|
||||||
CommandHandler,
|
CommandHandler,
|
||||||
CommandHandlerResult,
|
CommandHandlerResult,
|
||||||
@@ -32,8 +31,6 @@ import type {
|
|||||||
} from "./commands-types.js";
|
} from "./commands-types.js";
|
||||||
|
|
||||||
const HANDLERS: CommandHandler[] = [
|
const HANDLERS: CommandHandler[] = [
|
||||||
// Plugin commands are processed first, before built-in commands
|
|
||||||
handlePluginCommand,
|
|
||||||
handleBashCommand,
|
handleBashCommand,
|
||||||
handleActivationCommand,
|
handleActivationCommand,
|
||||||
handleSendPolicyCommand,
|
handleSendPolicyCommand,
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* Plugin Command Handler
|
|
||||||
*
|
|
||||||
* Handles commands registered by plugins, bypassing the LLM agent.
|
|
||||||
* This handler is called before built-in command handlers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { matchPluginCommand, executePluginCommand } from "../../plugins/commands.js";
|
|
||||||
import type { CommandHandler, CommandHandlerResult } from "./commands-types.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle plugin-registered commands.
|
|
||||||
* Returns a result if a plugin command was matched and executed,
|
|
||||||
* or null to continue to the next handler.
|
|
||||||
*/
|
|
||||||
export const handlePluginCommand: CommandHandler = async (
|
|
||||||
params,
|
|
||||||
_allowTextCommands,
|
|
||||||
): Promise<CommandHandlerResult | null> => {
|
|
||||||
const { command, cfg } = params;
|
|
||||||
|
|
||||||
// Try to match a plugin command
|
|
||||||
const match = matchPluginCommand(command.commandBodyNormalized);
|
|
||||||
if (!match) return null;
|
|
||||||
|
|
||||||
// Execute the plugin command (always returns a result)
|
|
||||||
const result = await executePluginCommand({
|
|
||||||
command: match.command,
|
|
||||||
args: match.args,
|
|
||||||
senderId: command.senderId,
|
|
||||||
channel: command.channel,
|
|
||||||
isAuthorizedSender: command.isAuthorizedSender,
|
|
||||||
commandBody: command.commandBodyNormalized,
|
|
||||||
config: cfg,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldContinue: false,
|
|
||||||
reply: { text: result.text },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -147,9 +147,6 @@ export function createFollowupRunner(params: {
|
|||||||
agentAccountId: queued.run.agentAccountId,
|
agentAccountId: queued.run.agentAccountId,
|
||||||
messageTo: queued.originatingTo,
|
messageTo: queued.originatingTo,
|
||||||
messageThreadId: queued.originatingThreadId,
|
messageThreadId: queued.originatingThreadId,
|
||||||
groupId: queued.run.groupId,
|
|
||||||
groupChannel: queued.run.groupChannel,
|
|
||||||
groupSpace: queued.run.groupSpace,
|
|
||||||
sessionFile: queued.run.sessionFile,
|
sessionFile: queued.run.sessionFile,
|
||||||
workspaceDir: queued.run.workspaceDir,
|
workspaceDir: queued.run.workspaceDir,
|
||||||
config: queued.run.config,
|
config: queued.run.config,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/se
|
|||||||
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveGroupSessionKey,
|
|
||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
updateSessionStore,
|
updateSessionStore,
|
||||||
@@ -367,9 +366,6 @@ export async function runPreparedReply(
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||||
agentAccountId: sessionCtx.AccountId,
|
agentAccountId: sessionCtx.AccountId,
|
||||||
groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined,
|
|
||||||
groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(),
|
|
||||||
groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined,
|
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ export type FollowupRun = {
|
|||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
groupId?: string;
|
|
||||||
groupChannel?: string;
|
|
||||||
groupSpace?: string;
|
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
config: ClawdbotConfig;
|
config: ClawdbotConfig;
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ type StatusArgs = {
|
|||||||
resolvedElevated?: ElevatedLevel;
|
resolvedElevated?: ElevatedLevel;
|
||||||
modelAuth?: string;
|
modelAuth?: string;
|
||||||
usageLine?: string;
|
usageLine?: string;
|
||||||
timeLine?: string;
|
|
||||||
queue?: QueueStatus;
|
queue?: QueueStatus;
|
||||||
mediaDecisions?: MediaUnderstandingDecision[];
|
mediaDecisions?: MediaUnderstandingDecision[];
|
||||||
subagentsLine?: string;
|
subagentsLine?: string;
|
||||||
@@ -382,7 +381,6 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
versionLine,
|
versionLine,
|
||||||
args.timeLine,
|
|
||||||
modelLine,
|
modelLine,
|
||||||
usageCostLine,
|
usageCostLine,
|
||||||
`📚 ${contextLine}`,
|
`📚 ${contextLine}`,
|
||||||
|
|||||||
@@ -247,71 +247,4 @@ describe("chrome extension relay server", () => {
|
|||||||
cdp.close();
|
cdp.close();
|
||||||
ext.close();
|
ext.close();
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
|
|
||||||
it("rebroadcasts attach when a session id is reused for a new target", async () => {
|
|
||||||
const port = await getFreePort();
|
|
||||||
cdpUrl = `http://127.0.0.1:${port}`;
|
|
||||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
|
||||||
|
|
||||||
const ext = new WebSocket(`ws://127.0.0.1:${port}/extension`);
|
|
||||||
await waitForOpen(ext);
|
|
||||||
|
|
||||||
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`);
|
|
||||||
await waitForOpen(cdp);
|
|
||||||
const q = createMessageQueue(cdp);
|
|
||||||
|
|
||||||
ext.send(
|
|
||||||
JSON.stringify({
|
|
||||||
method: "forwardCDPEvent",
|
|
||||||
params: {
|
|
||||||
method: "Target.attachedToTarget",
|
|
||||||
params: {
|
|
||||||
sessionId: "shared-session",
|
|
||||||
targetInfo: {
|
|
||||||
targetId: "t1",
|
|
||||||
type: "page",
|
|
||||||
title: "First",
|
|
||||||
url: "https://example.com",
|
|
||||||
},
|
|
||||||
waitingForDebugger: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const first = JSON.parse(await q.next()) as { method?: string; params?: unknown };
|
|
||||||
expect(first.method).toBe("Target.attachedToTarget");
|
|
||||||
expect(JSON.stringify(first.params ?? {})).toContain("t1");
|
|
||||||
|
|
||||||
ext.send(
|
|
||||||
JSON.stringify({
|
|
||||||
method: "forwardCDPEvent",
|
|
||||||
params: {
|
|
||||||
method: "Target.attachedToTarget",
|
|
||||||
params: {
|
|
||||||
sessionId: "shared-session",
|
|
||||||
targetInfo: {
|
|
||||||
targetId: "t2",
|
|
||||||
type: "page",
|
|
||||||
title: "Second",
|
|
||||||
url: "https://example.org",
|
|
||||||
},
|
|
||||||
waitingForDebugger: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const received: Array<{ method?: string; params?: unknown }> = [];
|
|
||||||
received.push(JSON.parse(await q.next()) as never);
|
|
||||||
received.push(JSON.parse(await q.next()) as never);
|
|
||||||
|
|
||||||
const detached = received.find((m) => m.method === "Target.detachedFromTarget");
|
|
||||||
const attached = received.find((m) => m.method === "Target.attachedToTarget");
|
|
||||||
expect(JSON.stringify(detached?.params ?? {})).toContain("t1");
|
|
||||||
expect(JSON.stringify(attached?.params ?? {})).toContain("t2");
|
|
||||||
|
|
||||||
cdp.close();
|
|
||||||
ext.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -477,23 +477,13 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
|||||||
const targetType = attached?.targetInfo?.type ?? "page";
|
const targetType = attached?.targetInfo?.type ?? "page";
|
||||||
if (targetType !== "page") return;
|
if (targetType !== "page") return;
|
||||||
if (attached?.sessionId && attached?.targetInfo?.targetId) {
|
if (attached?.sessionId && attached?.targetInfo?.targetId) {
|
||||||
const prev = connectedTargets.get(attached.sessionId);
|
const already = connectedTargets.has(attached.sessionId);
|
||||||
const nextTargetId = attached.targetInfo.targetId;
|
|
||||||
const prevTargetId = prev?.targetId;
|
|
||||||
const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId);
|
|
||||||
connectedTargets.set(attached.sessionId, {
|
connectedTargets.set(attached.sessionId, {
|
||||||
sessionId: attached.sessionId,
|
sessionId: attached.sessionId,
|
||||||
targetId: nextTargetId,
|
targetId: attached.targetInfo.targetId,
|
||||||
targetInfo: attached.targetInfo,
|
targetInfo: attached.targetInfo,
|
||||||
});
|
});
|
||||||
if (changedTarget && prevTargetId) {
|
if (!already) {
|
||||||
broadcastToCdpClients({
|
|
||||||
method: "Target.detachedFromTarget",
|
|
||||||
params: { sessionId: attached.sessionId, targetId: prevTargetId },
|
|
||||||
sessionId: attached.sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!prev || changedTarget) {
|
|
||||||
broadcastToCdpClients({ method, params, sessionId });
|
broadcastToCdpClients({ method, params, sessionId });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,15 +11,10 @@ import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js";
|
|||||||
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
import {
|
import {
|
||||||
resolveDiscordGroupRequireMention,
|
resolveDiscordGroupRequireMention,
|
||||||
resolveDiscordGroupToolPolicy,
|
|
||||||
resolveIMessageGroupRequireMention,
|
resolveIMessageGroupRequireMention,
|
||||||
resolveIMessageGroupToolPolicy,
|
|
||||||
resolveSlackGroupRequireMention,
|
resolveSlackGroupRequireMention,
|
||||||
resolveSlackGroupToolPolicy,
|
|
||||||
resolveTelegramGroupRequireMention,
|
resolveTelegramGroupRequireMention,
|
||||||
resolveTelegramGroupToolPolicy,
|
|
||||||
resolveWhatsAppGroupRequireMention,
|
resolveWhatsAppGroupRequireMention,
|
||||||
resolveWhatsAppGroupToolPolicy,
|
|
||||||
} from "./plugins/group-mentions.js";
|
} from "./plugins/group-mentions.js";
|
||||||
import type {
|
import type {
|
||||||
ChannelCapabilities,
|
ChannelCapabilities,
|
||||||
@@ -108,7 +103,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||||
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
||||||
@@ -147,7 +141,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
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).",
|
||||||
},
|
},
|
||||||
@@ -196,7 +189,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveDiscordGroupRequireMention,
|
resolveRequireMention: resolveDiscordGroupRequireMention,
|
||||||
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
stripPatterns: () => ["<@!?\\d+>"],
|
stripPatterns: () => ["<@!?\\d+>"],
|
||||||
@@ -230,7 +222,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveSlackGroupRequireMention,
|
resolveRequireMention: resolveSlackGroupRequireMention,
|
||||||
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
||||||
@@ -293,7 +284,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||||
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
buildToolContext: ({ context, hasRepliedRef }) => {
|
buildToolContext: ({ context, hasRepliedRef }) => {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
|
||||||
resolveChannelGroupRequireMention,
|
|
||||||
resolveChannelGroupToolsPolicy,
|
|
||||||
} from "../../config/group-policy.js";
|
|
||||||
import type { DiscordConfig } from "../../config/types.js";
|
import type { DiscordConfig } from "../../config/types.js";
|
||||||
import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
|
|
||||||
import { resolveSlackAccount } from "../../slack/accounts.js";
|
import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||||
|
|
||||||
type GroupMentionParams = {
|
type GroupMentionParams = {
|
||||||
@@ -196,103 +192,3 @@ export function resolveBlueBubblesGroupRequireMention(params: GroupMentionParams
|
|||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTelegramGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const { chatId } = parseTelegramGroupId(params.groupId);
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "telegram",
|
|
||||||
groupId: chatId ?? params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveWhatsAppGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "whatsapp",
|
|
||||||
groupId: params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveIMessageGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "imessage",
|
|
||||||
groupId: params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveDiscordGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const guildEntry = resolveDiscordGuildEntry(
|
|
||||||
params.cfg.channels?.discord?.guilds,
|
|
||||||
params.groupSpace,
|
|
||||||
);
|
|
||||||
const channelEntries = guildEntry?.channels;
|
|
||||||
if (channelEntries && Object.keys(channelEntries).length > 0) {
|
|
||||||
const groupChannel = params.groupChannel;
|
|
||||||
const channelSlug = normalizeDiscordSlug(groupChannel);
|
|
||||||
const entry =
|
|
||||||
(params.groupId ? channelEntries[params.groupId] : undefined) ??
|
|
||||||
(channelSlug
|
|
||||||
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
|
|
||||||
: undefined) ??
|
|
||||||
(groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined);
|
|
||||||
if (entry?.tools) return entry.tools;
|
|
||||||
}
|
|
||||||
if (guildEntry?.tools) return guildEntry.tools;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveSlackGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const account = resolveSlackAccount({
|
|
||||||
cfg: params.cfg,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
const channels = account.channels ?? {};
|
|
||||||
const keys = Object.keys(channels);
|
|
||||||
if (keys.length === 0) return undefined;
|
|
||||||
const channelId = params.groupId?.trim();
|
|
||||||
const groupChannel = params.groupChannel;
|
|
||||||
const channelName = groupChannel?.replace(/^#/, "");
|
|
||||||
const normalizedName = normalizeSlackSlug(channelName);
|
|
||||||
const candidates = [
|
|
||||||
channelId ?? "",
|
|
||||||
channelName ? `#${channelName}` : "",
|
|
||||||
channelName ?? "",
|
|
||||||
normalizedName,
|
|
||||||
].filter(Boolean);
|
|
||||||
let matched: { tools?: GroupToolPolicyConfig } | undefined;
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (candidate && channels[candidate]) {
|
|
||||||
matched = channels[candidate];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const resolved = matched ?? channels["*"];
|
|
||||||
if (resolved?.tools) return resolved.tools;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveBlueBubblesGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "bluebubbles",
|
|
||||||
groupId: params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
|
|
||||||
import type { OutboundDeliveryResult, OutboundSendDeps } from "../../infra/outbound/deliver.js";
|
import type { OutboundDeliveryResult, OutboundSendDeps } from "../../infra/outbound/deliver.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -66,7 +65,6 @@ export type ChannelConfigAdapter<ResolvedAccount> = {
|
|||||||
export type ChannelGroupAdapter = {
|
export type ChannelGroupAdapter = {
|
||||||
resolveRequireMention?: (params: ChannelGroupContext) => boolean | undefined;
|
resolveRequireMention?: (params: ChannelGroupContext) => boolean | undefined;
|
||||||
resolveGroupIntroHint?: (params: ChannelGroupContext) => string | undefined;
|
resolveGroupIntroHint?: (params: ChannelGroupContext) => string | undefined;
|
||||||
resolveToolPolicy?: (params: ChannelGroupContext) => GroupToolPolicyConfig | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelOutboundContext = {
|
export type ChannelOutboundContext = {
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import {
|
|||||||
} from "./register.cron-add.js";
|
} from "./register.cron-add.js";
|
||||||
import { registerCronEditCommand } from "./register.cron-edit.js";
|
import { registerCronEditCommand } from "./register.cron-edit.js";
|
||||||
import { registerCronSimpleCommands } from "./register.cron-simple.js";
|
import { registerCronSimpleCommands } from "./register.cron-simple.js";
|
||||||
|
import { registerWakeCommand } from "./register.wake.js";
|
||||||
|
|
||||||
export function registerCronCli(program: Command) {
|
export function registerCronCli(program: Command) {
|
||||||
|
registerWakeCommand(program);
|
||||||
|
|
||||||
const cron = program
|
const cron = program
|
||||||
.command("cron")
|
.command("cron")
|
||||||
.description("Manage cron jobs (via Gateway)")
|
.description("Manage cron jobs (via Gateway)")
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import { danger } from "../../globals.js";
|
||||||
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import { formatDocsLink } from "../../terminal/links.js";
|
||||||
|
import { theme } from "../../terminal/theme.js";
|
||||||
|
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||||
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
|
|
||||||
|
export function registerWakeCommand(program: Command) {
|
||||||
|
addGatewayClientOptions(
|
||||||
|
program
|
||||||
|
.command("wake")
|
||||||
|
.description("Enqueue a system event and optionally trigger an immediate heartbeat")
|
||||||
|
.requiredOption("--text <text>", "System event text")
|
||||||
|
.option("--mode <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
|
||||||
|
.option("--json", "Output JSON", false),
|
||||||
|
)
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/wake", "docs.clawd.bot/cli/wake")}\n`,
|
||||||
|
)
|
||||||
|
.action(async (opts: GatewayRpcOpts & { text?: string; mode?: string }) => {
|
||||||
|
try {
|
||||||
|
const result = await callGatewayFromCli(
|
||||||
|
"wake",
|
||||||
|
opts,
|
||||||
|
{ mode: opts.mode, text: opts.text },
|
||||||
|
{ expectFinal: false },
|
||||||
|
);
|
||||||
|
if (opts.json) defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||||
|
else defaultRuntime.log("ok");
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(danger(String(err)));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -248,7 +248,7 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
|||||||
const approvals = resolveExecApprovalsFromFile({
|
const approvals = resolveExecApprovalsFromFile({
|
||||||
file: approvalsFile as ExecApprovalsFile,
|
file: approvalsFile as ExecApprovalsFile,
|
||||||
agentId,
|
agentId,
|
||||||
overrides: { security, ask },
|
overrides: { 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);
|
||||||
|
|||||||
@@ -60,14 +60,6 @@ const entries: SubCliEntry[] = [
|
|||||||
mod.registerLogsCli(program);
|
mod.registerLogsCli(program);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "system",
|
|
||||||
description: "System events, heartbeat, and presence",
|
|
||||||
register: async (program) => {
|
|
||||||
const mod = await import("../system-cli.js");
|
|
||||||
mod.registerSystemCli(program);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "models",
|
name: "models",
|
||||||
description: "Model configuration",
|
description: "Model configuration",
|
||||||
|
|||||||
+2
-6
@@ -1,4 +1,3 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -83,16 +82,13 @@ function stripWindowsNodeExec(argv: string[]): string[] {
|
|||||||
const execBase = path.basename(execPath).toLowerCase();
|
const execBase = path.basename(execPath).toLowerCase();
|
||||||
const isExecPath = (value: string | undefined): boolean => {
|
const isExecPath = (value: string | undefined): boolean => {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
const normalized = normalizeCandidate(value);
|
const lower = normalizeCandidate(value).toLowerCase();
|
||||||
if (!normalized) return false;
|
|
||||||
const lower = normalized.toLowerCase();
|
|
||||||
return (
|
return (
|
||||||
lower === execPathLower ||
|
lower === execPathLower ||
|
||||||
path.basename(lower) === execBase ||
|
path.basename(lower) === execBase ||
|
||||||
lower.endsWith("\\node.exe") ||
|
lower.endsWith("\\node.exe") ||
|
||||||
lower.endsWith("/node.exe") ||
|
lower.endsWith("/node.exe") ||
|
||||||
lower.includes("node.exe") ||
|
lower.includes("node.exe")
|
||||||
(path.basename(lower) === "node.exe" && fs.existsSync(normalized))
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const filtered = argv.filter((arg, index) => index === 0 || !isExecPath(arg));
|
const filtered = argv.filter((arg, index) => index === 0 || !isExecPath(arg));
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import type { Command } from "commander";
|
|
||||||
|
|
||||||
import { danger } from "../globals.js";
|
|
||||||
import { defaultRuntime } from "../runtime.js";
|
|
||||||
import { formatDocsLink } from "../terminal/links.js";
|
|
||||||
import { theme } from "../terminal/theme.js";
|
|
||||||
import type { GatewayRpcOpts } from "./gateway-rpc.js";
|
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
|
|
||||||
|
|
||||||
type SystemEventOpts = GatewayRpcOpts & { text?: string; mode?: string; json?: boolean };
|
|
||||||
|
|
||||||
const normalizeWakeMode = (raw: unknown) => {
|
|
||||||
const mode = typeof raw === "string" ? raw.trim() : "";
|
|
||||||
if (!mode) return "next-heartbeat" as const;
|
|
||||||
if (mode === "now" || mode === "next-heartbeat") return mode;
|
|
||||||
throw new Error("--mode must be now or next-heartbeat");
|
|
||||||
};
|
|
||||||
|
|
||||||
export function registerSystemCli(program: Command) {
|
|
||||||
const system = program
|
|
||||||
.command("system")
|
|
||||||
.description("System tools (events, heartbeat, presence)")
|
|
||||||
.addHelpText(
|
|
||||||
"after",
|
|
||||||
() =>
|
|
||||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/system", "docs.clawd.bot/cli/system")}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
system
|
|
||||||
.command("event")
|
|
||||||
.description("Enqueue a system event and optionally trigger a heartbeat")
|
|
||||||
.requiredOption("--text <text>", "System event text")
|
|
||||||
.option("--mode <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: SystemEventOpts) => {
|
|
||||||
try {
|
|
||||||
const text = typeof opts.text === "string" ? opts.text.trim() : "";
|
|
||||||
if (!text) throw new Error("--text is required");
|
|
||||||
const mode = normalizeWakeMode(opts.mode);
|
|
||||||
const result = await callGatewayFromCli("wake", opts, { mode, text }, { expectFinal: false });
|
|
||||||
if (opts.json) defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
else defaultRuntime.log("ok");
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const heartbeat = system.command("heartbeat").description("Heartbeat controls");
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
heartbeat
|
|
||||||
.command("last")
|
|
||||||
.description("Show the last heartbeat event")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli("last-heartbeat", opts, undefined, {
|
|
||||||
expectFinal: false,
|
|
||||||
});
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
heartbeat
|
|
||||||
.command("enable")
|
|
||||||
.description("Enable heartbeats")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli(
|
|
||||||
"set-heartbeats",
|
|
||||||
opts,
|
|
||||||
{ enabled: true },
|
|
||||||
{ expectFinal: false },
|
|
||||||
);
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
heartbeat
|
|
||||||
.command("disable")
|
|
||||||
.description("Disable heartbeats")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli(
|
|
||||||
"set-heartbeats",
|
|
||||||
opts,
|
|
||||||
{ enabled: false },
|
|
||||||
{ expectFinal: false },
|
|
||||||
);
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
system
|
|
||||||
.command("presence")
|
|
||||||
.description("List system presence entries")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli("system-presence", opts, undefined, {
|
|
||||||
expectFinal: false,
|
|
||||||
});
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -377,7 +377,6 @@ export async function agentCommand(
|
|||||||
runContext.messageChannel,
|
runContext.messageChannel,
|
||||||
opts.replyChannel ?? opts.channel,
|
opts.replyChannel ?? opts.channel,
|
||||||
);
|
);
|
||||||
const spawnedBy = opts.spawnedBy ?? sessionEntry?.spawnedBy;
|
|
||||||
const fallbackResult = await runWithModelFallback({
|
const fallbackResult = await runWithModelFallback({
|
||||||
cfg,
|
cfg,
|
||||||
provider,
|
provider,
|
||||||
@@ -413,10 +412,6 @@ export async function agentCommand(
|
|||||||
agentAccountId: runContext.accountId,
|
agentAccountId: runContext.accountId,
|
||||||
messageTo: opts.replyTo ?? opts.to,
|
messageTo: opts.replyTo ?? opts.to,
|
||||||
messageThreadId: opts.threadId,
|
messageThreadId: opts.threadId,
|
||||||
groupId: runContext.groupId,
|
|
||||||
groupChannel: runContext.groupChannel,
|
|
||||||
groupSpace: runContext.groupSpace,
|
|
||||||
spawnedBy,
|
|
||||||
currentChannelId: runContext.currentChannelId,
|
currentChannelId: runContext.currentChannelId,
|
||||||
currentThreadTs: runContext.currentThreadTs,
|
currentThreadTs: runContext.currentThreadTs,
|
||||||
replyToMode: runContext.replyToMode,
|
replyToMode: runContext.replyToMode,
|
||||||
|
|||||||
@@ -14,15 +14,6 @@ export function resolveAgentRunContext(opts: AgentCommandOpts): AgentRunContext
|
|||||||
const normalizedAccountId = normalizeAccountId(merged.accountId ?? opts.accountId);
|
const normalizedAccountId = normalizeAccountId(merged.accountId ?? opts.accountId);
|
||||||
if (normalizedAccountId) merged.accountId = normalizedAccountId;
|
if (normalizedAccountId) merged.accountId = normalizedAccountId;
|
||||||
|
|
||||||
const groupId = (merged.groupId ?? opts.groupId)?.toString().trim();
|
|
||||||
if (groupId) merged.groupId = groupId;
|
|
||||||
|
|
||||||
const groupChannel = (merged.groupChannel ?? opts.groupChannel)?.toString().trim();
|
|
||||||
if (groupChannel) merged.groupChannel = groupChannel;
|
|
||||||
|
|
||||||
const groupSpace = (merged.groupSpace ?? opts.groupSpace)?.toString().trim();
|
|
||||||
if (groupSpace) merged.groupSpace = groupSpace;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
merged.currentThreadTs == null &&
|
merged.currentThreadTs == null &&
|
||||||
opts.threadId != null &&
|
opts.threadId != null &&
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ export type AgentStreamParams = {
|
|||||||
export type AgentRunContext = {
|
export type AgentRunContext = {
|
||||||
messageChannel?: string;
|
messageChannel?: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
groupId?: string | null;
|
|
||||||
groupChannel?: string | null;
|
|
||||||
groupSpace?: string | null;
|
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
@@ -58,14 +55,6 @@ export type AgentCommandOpts = {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
/** Context for embedded run routing (channel/account/thread). */
|
/** Context for embedded run routing (channel/account/thread). */
|
||||||
runContext?: AgentRunContext;
|
runContext?: AgentRunContext;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
deliveryTargetMode?: ChannelOutboundTargetMode;
|
deliveryTargetMode?: ChannelOutboundTargetMode;
|
||||||
bestEffortDeliver?: boolean;
|
bestEffortDeliver?: boolean;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import type { ChannelId } from "../channels/plugins/types.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import { normalizeAccountId } from "../routing/session-key.js";
|
import { normalizeAccountId } from "../routing/session-key.js";
|
||||||
import type { ClawdbotConfig } from "./config.js";
|
import type { ClawdbotConfig } from "./config.js";
|
||||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
|
||||||
|
|
||||||
export type GroupPolicyChannel = ChannelId;
|
export type GroupPolicyChannel = ChannelId;
|
||||||
|
|
||||||
export type ChannelGroupConfig = {
|
export type ChannelGroupConfig = {
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
tools?: GroupToolPolicyConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelGroupPolicy = {
|
export type ChannelGroupPolicy = {
|
||||||
@@ -93,15 +91,3 @@ export function resolveChannelGroupRequireMention(params: {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveChannelGroupToolsPolicy(params: {
|
|
||||||
cfg: ClawdbotConfig;
|
|
||||||
channel: GroupPolicyChannel;
|
|
||||||
groupId?: string | null;
|
|
||||||
accountId?: string | null;
|
|
||||||
}): GroupToolPolicyConfig | undefined {
|
|
||||||
const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params);
|
|
||||||
if (groupConfig?.tools) return groupConfig.tools;
|
|
||||||
if (defaultConfig?.tools) return defaultConfig.tools;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
@@ -50,7 +50,6 @@ const GROUP_LABELS: Record<string, string> = {
|
|||||||
diagnostics: "Diagnostics",
|
diagnostics: "Diagnostics",
|
||||||
logging: "Logging",
|
logging: "Logging",
|
||||||
gateway: "Gateway",
|
gateway: "Gateway",
|
||||||
nodeHost: "Node Host",
|
|
||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
tools: "Tools",
|
tools: "Tools",
|
||||||
bindings: "Bindings",
|
bindings: "Bindings",
|
||||||
@@ -77,7 +76,6 @@ const GROUP_ORDER: Record<string, number> = {
|
|||||||
update: 25,
|
update: 25,
|
||||||
diagnostics: 27,
|
diagnostics: 27,
|
||||||
gateway: 30,
|
gateway: 30,
|
||||||
nodeHost: 35,
|
|
||||||
agents: 40,
|
agents: 40,
|
||||||
tools: 50,
|
tools: 50,
|
||||||
bindings: 55,
|
bindings: 55,
|
||||||
@@ -195,12 +193,8 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
|
"gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
|
||||||
"gateway.reload.mode": "Config Reload Mode",
|
"gateway.reload.mode": "Config Reload Mode",
|
||||||
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
||||||
"gateway.nodes.browser.mode": "Gateway Node Browser Mode",
|
|
||||||
"gateway.nodes.browser.node": "Gateway Node Browser Pin",
|
|
||||||
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
|
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
|
||||||
"gateway.nodes.denyCommands": "Gateway Node Denylist",
|
"gateway.nodes.denyCommands": "Gateway Node Denylist",
|
||||||
"nodeHost.browserProxy.enabled": "Node Browser Proxy Enabled",
|
|
||||||
"nodeHost.browserProxy.allowProfiles": "Node Browser Proxy Allowed Profiles",
|
|
||||||
"skills.load.watch": "Watch Skills",
|
"skills.load.watch": "Watch Skills",
|
||||||
"skills.load.watchDebounceMs": "Skills Watch Debounce (ms)",
|
"skills.load.watchDebounceMs": "Skills Watch Debounce (ms)",
|
||||||
"agents.defaults.workspace": "Workspace",
|
"agents.defaults.workspace": "Workspace",
|
||||||
@@ -372,16 +366,10 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
|
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
|
||||||
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',
|
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',
|
||||||
"gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.",
|
"gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.",
|
||||||
"gateway.nodes.browser.mode":
|
|
||||||
'Node browser routing ("auto" = pick single connected browser node, "manual" = require node param, "off" = disable).',
|
|
||||||
"gateway.nodes.browser.node": "Pin browser routing to a specific node id or name (optional).",
|
|
||||||
"gateway.nodes.allowCommands":
|
"gateway.nodes.allowCommands":
|
||||||
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
|
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
|
||||||
"gateway.nodes.denyCommands":
|
"gateway.nodes.denyCommands":
|
||||||
"Commands to block even if present in node claims or default allowlist.",
|
"Commands to block even if present in node claims or default allowlist.",
|
||||||
"nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.",
|
|
||||||
"nodeHost.browserProxy.allowProfiles":
|
|
||||||
"Optional allowlist of browser profile names exposed via the node proxy.",
|
|
||||||
"diagnostics.cacheTrace.enabled":
|
"diagnostics.cacheTrace.enabled":
|
||||||
"Log cache trace snapshots for embedded agent runs (default: false).",
|
"Log cache trace snapshots for embedded agent runs (default: false).",
|
||||||
"diagnostics.cacheTrace.filePath":
|
"diagnostics.cacheTrace.filePath":
|
||||||
|
|||||||
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user