mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 22:11:38 +08:00
Compare commits
2 Commits
feat/plugi
...
fix/sideba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ba61028f | ||
|
|
efe5c62ae1 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -5,31 +5,18 @@ Docs: https://docs.clawd.bot
|
||||
## 2026.1.23 (Unreleased)
|
||||
|
||||
### Changes
|
||||
- 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 LLM-free plugin slash commands and include them in `/commands`. (#1558) Thanks @Glucksberg.
|
||||
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
||||
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
||||
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
|
||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
||||
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
|
||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
||||
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
|
||||
|
||||
### Fixes
|
||||
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
|
||||
- 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.
|
||||
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
||||
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
|
||||
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
||||
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
||||
@@ -38,23 +25,12 @@ Docs: https://docs.clawd.bot
|
||||
- CLI: render auth probe results as a table in `clawdbot models status`.
|
||||
- CLI: suppress probe-only embedded logs unless `--verbose` is set.
|
||||
- CLI: move auth probe errors below the table to reduce wrapping.
|
||||
- CLI: prevent ANSI color bleed when table cells wrap.
|
||||
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
||||
- CLI: drop the em dash when the banner tagline wraps to a second line.
|
||||
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
||||
- Telegram: render markdown in media captions. (#1478)
|
||||
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
|
||||
- Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
|
||||
- Daemon: use platform PATH delimiters when building minimal service paths.
|
||||
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
||||
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
||||
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
||||
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
|
||||
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
||||
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
||||
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
||||
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
||||
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
|
||||
|
||||
## 2026.1.22
|
||||
|
||||
|
||||
46
README.md
46
README.md
@@ -480,27 +480,27 @@ Thanks to all clawtributors:
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
||||
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a>
|
||||
<a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a>
|
||||
<a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a>
|
||||
<a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a>
|
||||
<a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a>
|
||||
<a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a>
|
||||
<a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a>
|
||||
<a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a>
|
||||
<a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a>
|
||||
<a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a>
|
||||
<a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a>
|
||||
<a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a>
|
||||
<a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a>
|
||||
<a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a>
|
||||
<a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a>
|
||||
<a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a>
|
||||
<a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a>
|
||||
<a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a>
|
||||
<a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a>
|
||||
<a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
|
||||
<a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a>
|
||||
<a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a>
|
||||
<a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a>
|
||||
<a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||
<a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a>
|
||||
<a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a>
|
||||
<a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a>
|
||||
<a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a>
|
||||
<a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a>
|
||||
<a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a>
|
||||
<a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a>
|
||||
<a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a>
|
||||
<a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a>
|
||||
<a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a>
|
||||
<a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a>
|
||||
<a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a>
|
||||
<a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a>
|
||||
<a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a>
|
||||
<a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a>
|
||||
<a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a>
|
||||
<a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a>
|
||||
<a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a>
|
||||
<a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a>
|
||||
<a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a>
|
||||
<a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a>
|
||||
<a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a>
|
||||
<a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||
</p>
|
||||
|
||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
||||
public let to: String
|
||||
public let message: String
|
||||
public let mediaurl: String?
|
||||
public let mediaurls: [String]?
|
||||
public let gifplayback: Bool?
|
||||
public let channel: String?
|
||||
public let accountid: String?
|
||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
||||
to: String,
|
||||
message: String,
|
||||
mediaurl: String?,
|
||||
mediaurls: [String]?,
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
accountid: String?,
|
||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
||||
self.to = to
|
||||
self.message = message
|
||||
self.mediaurl = mediaurl
|
||||
self.mediaurls = mediaurls
|
||||
self.gifplayback = gifplayback
|
||||
self.channel = channel
|
||||
self.accountid = accountid
|
||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
||||
case to
|
||||
case message
|
||||
case mediaurl = "mediaUrl"
|
||||
case mediaurls = "mediaUrls"
|
||||
case gifplayback = "gifPlayback"
|
||||
case channel
|
||||
case accountid = "accountId"
|
||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let accountid: String?
|
||||
public let replyaccountid: String?
|
||||
public let threadid: String?
|
||||
public let groupid: String?
|
||||
public let groupchannel: String?
|
||||
public let groupspace: String?
|
||||
public let timeout: Int?
|
||||
public let lane: String?
|
||||
public let extrasystemprompt: String?
|
||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
accountid: String?,
|
||||
replyaccountid: String?,
|
||||
threadid: String?,
|
||||
groupid: String?,
|
||||
groupchannel: String?,
|
||||
groupspace: String?,
|
||||
timeout: Int?,
|
||||
lane: String?,
|
||||
extrasystemprompt: String?,
|
||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.accountid = accountid
|
||||
self.replyaccountid = replyaccountid
|
||||
self.threadid = threadid
|
||||
self.groupid = groupid
|
||||
self.groupchannel = groupchannel
|
||||
self.groupspace = groupspace
|
||||
self.timeout = timeout
|
||||
self.lane = lane
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
case accountid = "accountId"
|
||||
case replyaccountid = "replyAccountId"
|
||||
case threadid = "threadId"
|
||||
case groupid = "groupId"
|
||||
case groupchannel = "groupChannel"
|
||||
case groupspace = "groupSpace"
|
||||
case timeout
|
||||
case lane
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
|
||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
||||
public let to: String
|
||||
public let message: String
|
||||
public let mediaurl: String?
|
||||
public let mediaurls: [String]?
|
||||
public let gifplayback: Bool?
|
||||
public let channel: String?
|
||||
public let accountid: String?
|
||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
||||
to: String,
|
||||
message: String,
|
||||
mediaurl: String?,
|
||||
mediaurls: [String]?,
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
accountid: String?,
|
||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
||||
self.to = to
|
||||
self.message = message
|
||||
self.mediaurl = mediaurl
|
||||
self.mediaurls = mediaurls
|
||||
self.gifplayback = gifplayback
|
||||
self.channel = channel
|
||||
self.accountid = accountid
|
||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
||||
case to
|
||||
case message
|
||||
case mediaurl = "mediaUrl"
|
||||
case mediaurls = "mediaUrls"
|
||||
case gifplayback = "gifPlayback"
|
||||
case channel
|
||||
case accountid = "accountId"
|
||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let accountid: String?
|
||||
public let replyaccountid: String?
|
||||
public let threadid: String?
|
||||
public let groupid: String?
|
||||
public let groupchannel: String?
|
||||
public let groupspace: String?
|
||||
public let timeout: Int?
|
||||
public let lane: String?
|
||||
public let extrasystemprompt: String?
|
||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
accountid: String?,
|
||||
replyaccountid: String?,
|
||||
threadid: String?,
|
||||
groupid: String?,
|
||||
groupchannel: String?,
|
||||
groupspace: String?,
|
||||
timeout: Int?,
|
||||
lane: String?,
|
||||
extrasystemprompt: String?,
|
||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.accountid = accountid
|
||||
self.replyaccountid = replyaccountid
|
||||
self.threadid = threadid
|
||||
self.groupid = groupid
|
||||
self.groupchannel = groupchannel
|
||||
self.groupspace = groupspace
|
||||
self.timeout = timeout
|
||||
self.lane = lane
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
case accountid = "accountId"
|
||||
case replyaccountid = "replyAccountId"
|
||||
case threadid = "threadId"
|
||||
case groupid = "groupId"
|
||||
case groupchannel = "groupChannel"
|
||||
case groupspace = "groupSpace"
|
||||
case timeout
|
||||
case lane
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"gateway-daemon",
|
||||
"--bind",
|
||||
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
||||
"--port",
|
||||
|
||||
@@ -263,15 +263,15 @@ Run history:
|
||||
clawdbot cron runs --id <jobId> --limit 50
|
||||
```
|
||||
|
||||
Immediate system event without creating a job:
|
||||
Immediate wake without creating a job:
|
||||
```bash
|
||||
clawdbot system event --mode now --text "Next heartbeat: check battery."
|
||||
clawdbot wake --mode now --text "Next heartbeat: check battery."
|
||||
```
|
||||
|
||||
## Gateway API surface
|
||||
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
||||
- `cron.run` (force or due), `cron.runs`
|
||||
For immediate system events without a job, use [`clawdbot system event`](/cli/system).
|
||||
- `wake` (enqueue system event + optional heartbeat)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -271,4 +271,4 @@ clawdbot cron add \
|
||||
|
||||
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
|
||||
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
|
||||
- [System](/cli/system) - system events + heartbeat controls
|
||||
- [Wake](/cli/wake) - manual wake command
|
||||
@@ -17,37 +17,6 @@ not an API key.
|
||||
- Auth: AWS credentials (env vars, shared config, or instance role)
|
||||
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
|
||||
|
||||
## Automatic model discovery
|
||||
|
||||
If AWS credentials are detected, Clawdbot can automatically discover Bedrock
|
||||
models that support **streaming** and **text output**. Discovery uses
|
||||
`bedrock:ListFoundationModels` and is cached (default: 1 hour).
|
||||
|
||||
Config options live under `models.bedrockDiscovery`:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
bedrockDiscovery: {
|
||||
enabled: true,
|
||||
region: "us-east-1",
|
||||
providerFilter: ["anthropic", "amazon"],
|
||||
refreshInterval: 3600,
|
||||
defaultContextWindow: 32000,
|
||||
defaultMaxTokens: 4096
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `enabled` defaults to `true` when AWS credentials are present.
|
||||
- `region` defaults to `AWS_REGION` or `AWS_DEFAULT_REGION`, then `us-east-1`.
|
||||
- `providerFilter` matches Bedrock provider names (for example `anthropic`).
|
||||
- `refreshInterval` is seconds; set to `0` to disable caching.
|
||||
- `defaultContextWindow` (default: `32000`) and `defaultMaxTokens` (default: `4096`)
|
||||
are used for discovered models (override if you know your model limits).
|
||||
|
||||
## Setup (manual)
|
||||
|
||||
1) Ensure AWS credentials are available on the **gateway host**:
|
||||
@@ -98,7 +67,6 @@ export AWS_BEARER_TOKEN_BEDROCK="..."
|
||||
## Notes
|
||||
|
||||
- Bedrock requires **model access** enabled in your AWS account/region.
|
||||
- Automatic discovery needs the `bedrock:ListFoundationModels` permission.
|
||||
- If you use profiles, set `AWS_PROFILE` on the gateway host.
|
||||
- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`,
|
||||
then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the
|
||||
|
||||
@@ -44,7 +44,6 @@ clawdbot channels logout --channel whatsapp
|
||||
|
||||
- Run `clawdbot status --deep` for a broad probe.
|
||||
- Use `clawdbot doctor` for guided fixes.
|
||||
- `clawdbot channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude Code CLI.
|
||||
|
||||
## Capabilities probe
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`sessions`](/cli/sessions)
|
||||
- [`gateway`](/cli/gateway)
|
||||
- [`logs`](/cli/logs)
|
||||
- [`system`](/cli/system)
|
||||
- [`models`](/cli/models)
|
||||
- [`memory`](/cli/memory)
|
||||
- [`nodes`](/cli/nodes)
|
||||
@@ -39,6 +38,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`sandbox`](/cli/sandbox)
|
||||
- [`tui`](/cli/tui)
|
||||
- [`browser`](/cli/browser)
|
||||
- [`wake`](/cli/wake)
|
||||
- [`cron`](/cli/cron)
|
||||
- [`dns`](/cli/dns)
|
||||
- [`docs`](/cli/docs)
|
||||
@@ -145,10 +145,6 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
restart
|
||||
run
|
||||
logs
|
||||
system
|
||||
event
|
||||
heartbeat last|enable|disable
|
||||
presence
|
||||
models
|
||||
list
|
||||
status
|
||||
@@ -164,6 +160,7 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
list
|
||||
recreate
|
||||
explain
|
||||
wake
|
||||
cron
|
||||
status
|
||||
list
|
||||
@@ -766,9 +763,9 @@ Options:
|
||||
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
||||
- `clear`: `--provider <name>`, `--agent <id>`
|
||||
|
||||
## System
|
||||
## Cron + wake
|
||||
|
||||
### `system event`
|
||||
### `wake`
|
||||
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
||||
|
||||
Required:
|
||||
@@ -779,21 +776,7 @@ Options:
|
||||
- `--json`
|
||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||
|
||||
### `system heartbeat last|enable|disable`
|
||||
Heartbeat controls (Gateway RPC).
|
||||
|
||||
Options:
|
||||
- `--json`
|
||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||
|
||||
### `system presence`
|
||||
List system presence entries (Gateway RPC).
|
||||
|
||||
Options:
|
||||
- `--json`
|
||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||
|
||||
## Cron
|
||||
### `cron`
|
||||
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
||||
|
||||
Subcommands:
|
||||
|
||||
@@ -23,24 +23,6 @@ Common use cases:
|
||||
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
||||
node host, so you can keep command access scoped and explicit.
|
||||
|
||||
## Browser proxy (zero-config)
|
||||
|
||||
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
|
||||
disabled on the node. This lets the agent use browser automation on that node
|
||||
without extra configuration.
|
||||
|
||||
Disable it on the node if needed:
|
||||
|
||||
```json5
|
||||
{
|
||||
nodeHost: {
|
||||
browserProxy: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Run (foreground)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot system` (system events, heartbeat, presence)"
|
||||
read_when:
|
||||
- You want to enqueue a system event without creating a cron job
|
||||
- You need to enable or disable heartbeats
|
||||
- You want to inspect system presence entries
|
||||
---
|
||||
|
||||
# `clawdbot system`
|
||||
|
||||
System-level helpers for the Gateway: enqueue system events, control heartbeats,
|
||||
and view presence.
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
||||
clawdbot system heartbeat enable
|
||||
clawdbot system heartbeat last
|
||||
clawdbot system presence
|
||||
```
|
||||
|
||||
## `system event`
|
||||
|
||||
Enqueue a system event on the **main** session. The next heartbeat will inject
|
||||
it as a `System:` line in the prompt. Use `--mode now` to trigger the heartbeat
|
||||
immediately; `next-heartbeat` waits for the next scheduled tick.
|
||||
|
||||
Flags:
|
||||
- `--text <text>`: required system event text.
|
||||
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## `system heartbeat last|enable|disable`
|
||||
|
||||
Heartbeat controls:
|
||||
- `last`: show the last heartbeat event.
|
||||
- `enable`: turn heartbeats back on (use this if they were disabled).
|
||||
- `disable`: pause heartbeats.
|
||||
|
||||
Flags:
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## `system presence`
|
||||
|
||||
List the current system presence entries the Gateway knows about (nodes,
|
||||
instances, and similar status lines).
|
||||
|
||||
Flags:
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires a running Gateway reachable by your current config (local or remote).
|
||||
- System events are ephemeral and not persisted across restarts.
|
||||
35
docs/cli/wake.md
Normal file
35
docs/cli/wake.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot wake` (enqueue a system event and optionally trigger an immediate heartbeat)"
|
||||
read_when:
|
||||
- You want to “poke” a running Gateway to process a system event
|
||||
- You use `wake` with cron jobs or remote nodes
|
||||
---
|
||||
|
||||
# `clawdbot wake`
|
||||
|
||||
Enqueue a system event on the Gateway and optionally trigger an immediate heartbeat.
|
||||
|
||||
This is a lightweight “poke” for automation flows where you 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.
|
||||
|
||||
@@ -850,12 +850,12 @@
|
||||
"cli/memory",
|
||||
"cli/models",
|
||||
"cli/logs",
|
||||
"cli/system",
|
||||
"cli/nodes",
|
||||
"cli/approvals",
|
||||
"cli/gateway",
|
||||
"cli/tui",
|
||||
"cli/voicecall",
|
||||
"cli/wake",
|
||||
"cli/cron",
|
||||
"cli/dns",
|
||||
"cli/docs",
|
||||
@@ -1000,8 +1000,6 @@
|
||||
"group": "Tools & Skills",
|
||||
"pages": [
|
||||
"tools",
|
||||
"tools/lobster",
|
||||
"tools/llm-task",
|
||||
"plugin",
|
||||
"plugins/voice-call",
|
||||
"plugins/zalouser",
|
||||
|
||||
@@ -1970,7 +1970,6 @@ Example (provider/model-specific allowlist):
|
||||
```
|
||||
|
||||
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||||
Matching is case-insensitive and supports `*` wildcards (`"*"` means all tools).
|
||||
This is applied even when the Docker sandbox is **off**.
|
||||
|
||||
Example (disable browser/canvas everywhere):
|
||||
|
||||
@@ -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
|
||||
safe to include every 30 minutes.
|
||||
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
|
||||
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
||||
|
||||
Example `HEARTBEAT.md`:
|
||||
@@ -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:
|
||||
|
||||
```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
|
||||
|
||||
@@ -184,7 +184,7 @@ services:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"gateway-daemon",
|
||||
"--bind",
|
||||
"${CLAWDBOT_GATEWAY_BIND}",
|
||||
"--port",
|
||||
|
||||
@@ -62,7 +62,6 @@ Plugins can register:
|
||||
- Background services
|
||||
- Optional config validation
|
||||
- **Skills** (by listing `skills` directories in the plugin manifest)
|
||||
- **Auto-reply commands** (execute without invoking the AI agent)
|
||||
|
||||
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
||||
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
||||
@@ -495,66 +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
|
||||
- Telegram native commands only allow `a-z0-9_` (max 32 chars). Use underscores (not hyphens) if you want a plugin command to appear in Telegram’s native command list.
|
||||
- Reserved command names (like `help`, `status`, `reset`, etc.) cannot be overridden by plugins
|
||||
- Duplicate command registration across plugins will fail with a diagnostic error
|
||||
|
||||
### Register background services
|
||||
|
||||
```ts
|
||||
|
||||
@@ -5,5 +5,4 @@ read_when:
|
||||
---
|
||||
# HEARTBEAT.md
|
||||
|
||||
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
||||
# Add tasks below when you want the agent to check something periodically.
|
||||
Keep this file empty unless you want a tiny checklist. Keep it small.
|
||||
|
||||
@@ -7,16 +7,11 @@ read_when:
|
||||
|
||||
*Fill this in during your first conversation. Make it yours.*
|
||||
|
||||
- **Name:**
|
||||
*(pick something you like)*
|
||||
- **Creature:**
|
||||
*(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||
- **Vibe:**
|
||||
*(how do you come across? sharp? warm? chaotic? calm?)*
|
||||
- **Emoji:**
|
||||
*(your signature — pick one that feels right)*
|
||||
- **Avatar:**
|
||||
*(workspace-relative path, http(s) URL, or data URI)*
|
||||
- **Name:** *(pick something you like)*
|
||||
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
||||
- **Emoji:** *(your signature — pick one that feels right)*
|
||||
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -182,8 +182,6 @@ By default, Clawdbot runs a heartbeat every 30 minutes with the prompt:
|
||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||
|
||||
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||
- If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
||||
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
### 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
|
||||
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](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 are allowed only when policy + allowlist + (optional) user approval all agree.
|
||||
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
||||
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used.
|
||||
|
||||
If the companion app UI is **not available**, any request that requires a prompt is
|
||||
resolved by the **ask fallback** (default: deny).
|
||||
|
||||
@@ -22,10 +22,6 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Matching is case-insensitive.
|
||||
- `*` wildcards are supported (`"*"` means all tools).
|
||||
|
||||
## Tool profiles (base allowlist)
|
||||
|
||||
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
||||
@@ -160,7 +156,6 @@ alongside tools (for example, the voice-call plugin).
|
||||
|
||||
Optional plugin tools:
|
||||
- [Lobster](/tools/lobster): typed workflow runtime with resumable approvals (requires the Lobster CLI on the gateway host).
|
||||
- [LLM Task](/tools/llm-task): JSON-only LLM step for structured workflow output (optional schema validation).
|
||||
|
||||
## Tool inventory
|
||||
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
summary: "JSON-only LLM tasks for workflows (optional plugin tool)"
|
||||
read_when:
|
||||
- You want a JSON-only LLM step inside workflows
|
||||
- You need schema-validated LLM output for automation
|
||||
---
|
||||
|
||||
# LLM Task
|
||||
|
||||
`llm-task` is an **optional plugin tool** that runs a JSON-only LLM task and
|
||||
returns structured output (optionally validated against JSON Schema).
|
||||
|
||||
This is ideal for workflow engines like Lobster: you can add a single LLM step
|
||||
without writing custom Clawdbot code for each workflow.
|
||||
|
||||
## Enable the plugin
|
||||
|
||||
1) Enable the plugin:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": { "enabled": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2) Allowlist the tool (it is registered with `optional: true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"tools": { "allow": ["llm-task"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Config (optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"defaultProvider": "openai-codex",
|
||||
"defaultModel": "gpt-5.2",
|
||||
"defaultAuthProfileId": "main",
|
||||
"allowedModels": ["openai-codex/gpt-5.2"],
|
||||
"maxTokens": 800,
|
||||
"timeoutMs": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`allowedModels` is an allowlist of `provider/model` strings. If set, any request
|
||||
outside the list is rejected.
|
||||
|
||||
## Tool parameters
|
||||
|
||||
- `prompt` (string, required)
|
||||
- `input` (any, optional)
|
||||
- `schema` (object, optional JSON Schema)
|
||||
- `provider` (string, optional)
|
||||
- `model` (string, optional)
|
||||
- `authProfileId` (string, optional)
|
||||
- `temperature` (number, optional)
|
||||
- `maxTokens` (number, optional)
|
||||
- `timeoutMs` (number, optional)
|
||||
|
||||
## Output
|
||||
|
||||
Returns `details.json` containing the parsed JSON (and validates against
|
||||
`schema` when provided).
|
||||
|
||||
## Example: Lobster workflow step
|
||||
|
||||
```lobster
|
||||
clawd.invoke --tool llm-task --action json --args-json '{
|
||||
"prompt": "Given the input email, return intent and draft.",
|
||||
"input": {
|
||||
"subject": "Hello",
|
||||
"body": "Can you help?"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intent": { "type": "string" },
|
||||
"draft": { "type": "string" }
|
||||
},
|
||||
"required": ["intent", "draft"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Safety notes
|
||||
|
||||
- The tool is **JSON-only** and instructs the model to output only JSON (no
|
||||
code fences, no commentary).
|
||||
- No tools are exposed to the model for this run.
|
||||
- Treat output as untrusted unless you validate with `schema`.
|
||||
- Put approvals before any side-effecting step (send, post, exec).
|
||||
@@ -65,52 +65,6 @@ gog.gmail.search --query 'newer_than:1d' \
|
||||
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
|
||||
```
|
||||
|
||||
## JSON-only LLM steps (llm-task)
|
||||
|
||||
For workflows that need a **structured LLM step**, enable the optional
|
||||
`llm-task` plugin tool and call it from Lobster. This keeps the workflow
|
||||
deterministic while still letting you classify/summarize/draft with a model.
|
||||
|
||||
Enable the tool:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": { "enabled": true }
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"tools": { "allow": ["llm-task"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use it in a pipeline:
|
||||
|
||||
```lobster
|
||||
clawd.invoke --tool llm-task --action json --args-json '{
|
||||
"prompt": "Given the input email, return intent and draft.",
|
||||
"input": { "subject": "Hello", "body": "Can you help?" },
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intent": { "type": "string" },
|
||||
"draft": { "type": "string" }
|
||||
},
|
||||
"required": ["intent", "draft"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
See [LLM Task](/tools/llm-task) for details and configuration options.
|
||||
|
||||
## Workflow files (.lobster)
|
||||
|
||||
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
normalizeAccountId,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
@@ -63,7 +62,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
||||
resolveToolPolicy: resolveBlueBubblesGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -21,7 +21,6 @@ const bluebubblesActionSchema = z
|
||||
|
||||
const bluebubblesGroupConfigSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
});
|
||||
|
||||
const bluebubblesAccountSchema = z.object({
|
||||
|
||||
@@ -4,8 +4,6 @@ export type GroupPolicy = "open" | "disabled" | "allowlist";
|
||||
export type BlueBubblesGroupConfig = {
|
||||
/** If true, only respond in this group when mentioned. */
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this group. */
|
||||
tools?: { allow?: string[]; deny?: string[] };
|
||||
};
|
||||
|
||||
export type BlueBubblesAccountConfig = {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
resolveDiscordAccount,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordGroupRequireMention,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelPlugin,
|
||||
@@ -145,7 +144,6 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveDiscordGroupRequireMention,
|
||||
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
||||
},
|
||||
mentions: {
|
||||
stripPatterns: () => ["<@!?\\d+>"],
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
resolveIMessageGroupRequireMention,
|
||||
resolveIMessageGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
type ResolvedIMessageAccount,
|
||||
@@ -107,7 +106,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
||||
},
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# LLM Task (plugin)
|
||||
|
||||
Adds an **optional** agent tool `llm-task` for running **JSON-only** LLM tasks
|
||||
(drafting, summarizing, classifying) with optional JSON Schema validation.
|
||||
|
||||
Designed to be called from workflow engines (for example, Lobster via
|
||||
`clawd.invoke --each`) without adding new Clawdbot code per workflow.
|
||||
|
||||
## Enable
|
||||
|
||||
1) Enable the plugin:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": { "enabled": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2) Allowlist the tool (it is registered with `optional: true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"tools": { "allow": ["llm-task"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Config (optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"defaultProvider": "openai-codex",
|
||||
"defaultModel": "gpt-5.2",
|
||||
"defaultAuthProfileId": "main",
|
||||
"allowedModels": ["openai-codex/gpt-5.2"],
|
||||
"maxTokens": 800,
|
||||
"timeoutMs": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`allowedModels` is an allowlist of `provider/model` strings. If set, any request
|
||||
outside the list is rejected.
|
||||
|
||||
## Tool API
|
||||
|
||||
### Parameters
|
||||
|
||||
- `prompt` (string, required)
|
||||
- `input` (any, optional)
|
||||
- `schema` (object, optional JSON Schema)
|
||||
- `provider` (string, optional)
|
||||
- `model` (string, optional)
|
||||
- `authProfileId` (string, optional)
|
||||
- `temperature` (number, optional)
|
||||
- `maxTokens` (number, optional)
|
||||
- `timeoutMs` (number, optional)
|
||||
|
||||
### Output
|
||||
|
||||
Returns `details.json` containing the parsed JSON (and validates against
|
||||
`schema` when provided).
|
||||
|
||||
## Notes
|
||||
|
||||
- The tool is **JSON-only** and instructs the model to output only JSON
|
||||
(no code fences, no commentary).
|
||||
- No tools are exposed to the model for this run.
|
||||
- Side effects should be handled outside this tool (for example, approvals in
|
||||
Lobster) before calling tools that send messages/emails.
|
||||
|
||||
## Bundled extension note
|
||||
|
||||
This extension depends on Clawdbot internal modules (the embedded agent runner).
|
||||
It is intended to ship as a **bundled** Clawdbot extension (like `lobster`) and
|
||||
be enabled via `plugins.entries` + tool allowlists.
|
||||
|
||||
It is **not** currently designed to be copied into
|
||||
`~/.clawdbot/extensions` as a standalone plugin directory.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"id": "llm-task",
|
||||
"name": "LLM Task",
|
||||
"description": "Generic JSON-only LLM tool for structured tasks callable from workflows.",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"defaultProvider": { "type": "string" },
|
||||
"defaultModel": { "type": "string" },
|
||||
"defaultAuthProfileId": { "type": "string" },
|
||||
"allowedModels": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Allowlist of provider/model keys like openai-codex/gpt-5.2."
|
||||
},
|
||||
"maxTokens": { "type": "number" },
|
||||
"timeoutMs": { "type": "number" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
|
||||
|
||||
import { createLlmTaskTool } from "./src/llm-task-tool.js";
|
||||
|
||||
export default function register(api: ClawdbotPluginApi) {
|
||||
api.registerTool(createLlmTaskTool(api), { optional: true });
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "@clawdbot/llm-task",
|
||||
"version": "2026.1.23",
|
||||
"type": "module",
|
||||
"description": "Clawdbot JSON-only LLM task plugin",
|
||||
"clawdbot": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../../../src/agents/pi-embedded-runner.js", () => {
|
||||
return {
|
||||
runEmbeddedPiAgent: vi.fn(async () => ({
|
||||
meta: { startedAt: Date.now() },
|
||||
payloads: [{ text: "{}" }],
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
import { runEmbeddedPiAgent } from "../../../src/agents/pi-embedded-runner.js";
|
||||
import { createLlmTaskTool } from "./llm-task-tool.js";
|
||||
|
||||
function fakeApi(overrides: any = {}) {
|
||||
return {
|
||||
id: "llm-task",
|
||||
name: "llm-task",
|
||||
source: "test",
|
||||
config: { agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } } },
|
||||
pluginConfig: {},
|
||||
runtime: { version: "test" },
|
||||
logger: { debug() {}, info() {}, warn() {}, error() {} },
|
||||
registerTool() {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("llm-task tool (json-only)", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("returns parsed json", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const res = await tool.execute("id", { prompt: "return foo" });
|
||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("strips fenced json", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: "```json\n{\"ok\":true}\n```" }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const res = await tool.execute("id", { prompt: "return ok" });
|
||||
expect((res as any).details.json).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("validates schema", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: { foo: { type: "string" } },
|
||||
required: ["foo"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const res = await tool.execute("id", { prompt: "return foo", schema });
|
||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("throws on invalid json", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({ meta: {}, payloads: [{ text: "not-json" }] });
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
await expect(tool.execute("id", { prompt: "x" })).rejects.toThrow(/invalid json/i);
|
||||
});
|
||||
|
||||
it("throws on schema mismatch", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ foo: 1 }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const schema = { type: "object", properties: { foo: { type: "string" } }, required: ["foo"] };
|
||||
await expect(tool.execute("id", { prompt: "x", schema })).rejects.toThrow(/match schema/i);
|
||||
});
|
||||
|
||||
it("passes provider/model overrides to embedded runner", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
await tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" });
|
||||
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
||||
expect(call.provider).toBe("anthropic");
|
||||
expect(call.model).toBe("claude-4-sonnet");
|
||||
});
|
||||
|
||||
it("enforces allowedModels", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }) as any);
|
||||
await expect(tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" })).rejects.toThrow(
|
||||
/not allowed/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("disables tools for embedded run", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
await tool.execute("id", { prompt: "x" });
|
||||
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
||||
expect(call.disableTools).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,218 +0,0 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import Ajv from "ajv";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// NOTE: This extension is intended to be bundled with Clawdbot.
|
||||
// When running from source (tests/dev), Clawdbot internals live under src/.
|
||||
// When running from a built install, internals live under dist/ (no src/ tree).
|
||||
// So we resolve internal imports dynamically with src-first, dist-fallback.
|
||||
|
||||
import type { ClawdbotPluginApi } from "../../../src/plugins/types.js";
|
||||
|
||||
type RunEmbeddedPiAgentFn = (params: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
|
||||
// Source checkout (tests/dev)
|
||||
try {
|
||||
const mod = await import("../../../src/agents/pi-embedded-runner.js");
|
||||
if (typeof (mod as any).runEmbeddedPiAgent === "function") return (mod as any).runEmbeddedPiAgent;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Bundled install (built)
|
||||
const mod = await import("../../../agents/pi-embedded-runner.js");
|
||||
if (typeof (mod as any).runEmbeddedPiAgent !== "function") {
|
||||
throw new Error("Internal error: runEmbeddedPiAgent not available");
|
||||
}
|
||||
return (mod as any).runEmbeddedPiAgent;
|
||||
}
|
||||
|
||||
function stripCodeFences(s: string): string {
|
||||
const trimmed = s.trim();
|
||||
const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
||||
if (m) return (m[1] ?? "").trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function collectText(payloads: Array<{ text?: string; isError?: boolean }> | undefined): string {
|
||||
const texts = (payloads ?? [])
|
||||
.filter((p) => !p.isError && typeof p.text === "string")
|
||||
.map((p) => p.text ?? "");
|
||||
return texts.join("\n").trim();
|
||||
}
|
||||
|
||||
function toModelKey(provider?: string, model?: string): string | undefined {
|
||||
const p = provider?.trim();
|
||||
const m = model?.trim();
|
||||
if (!p || !m) return undefined;
|
||||
return `${p}/${m}`;
|
||||
}
|
||||
|
||||
type PluginCfg = {
|
||||
defaultProvider?: string;
|
||||
defaultModel?: string;
|
||||
defaultAuthProfileId?: string;
|
||||
allowedModels?: string[];
|
||||
maxTokens?: number;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export function createLlmTaskTool(api: ClawdbotPluginApi) {
|
||||
return {
|
||||
name: "llm-task",
|
||||
description:
|
||||
"Run a generic JSON-only LLM task and return schema-validated JSON. Designed for orchestration from Lobster workflows via clawd.invoke.",
|
||||
parameters: Type.Object({
|
||||
prompt: Type.String({ description: "Task instruction for the LLM." }),
|
||||
input: Type.Optional(Type.Unknown({ description: "Optional input payload for the task." })),
|
||||
schema: Type.Optional(Type.Unknown({ description: "Optional JSON Schema to validate the returned JSON." })),
|
||||
provider: Type.Optional(Type.String({ description: "Provider override (e.g. openai-codex, anthropic)." })),
|
||||
model: Type.Optional(Type.String({ description: "Model id override." })),
|
||||
authProfileId: Type.Optional(Type.String({ description: "Auth profile override." })),
|
||||
temperature: Type.Optional(Type.Number({ description: "Best-effort temperature override." })),
|
||||
maxTokens: Type.Optional(Type.Number({ description: "Best-effort maxTokens override." })),
|
||||
timeoutMs: Type.Optional(Type.Number({ description: "Timeout for the LLM run." })),
|
||||
}),
|
||||
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
const prompt = String(params.prompt ?? "");
|
||||
if (!prompt.trim()) throw new Error("prompt required");
|
||||
|
||||
const pluginCfg = (api.pluginConfig ?? {}) as PluginCfg;
|
||||
|
||||
const primary = api.config?.agents?.defaults?.model?.primary;
|
||||
const primaryProvider = typeof primary === "string" ? primary.split("/")[0] : undefined;
|
||||
const primaryModel = typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined;
|
||||
|
||||
const provider =
|
||||
(typeof params.provider === "string" && params.provider.trim()) ||
|
||||
(typeof pluginCfg.defaultProvider === "string" && pluginCfg.defaultProvider.trim()) ||
|
||||
primaryProvider ||
|
||||
undefined;
|
||||
|
||||
const model =
|
||||
(typeof params.model === "string" && params.model.trim()) ||
|
||||
(typeof pluginCfg.defaultModel === "string" && pluginCfg.defaultModel.trim()) ||
|
||||
primaryModel ||
|
||||
undefined;
|
||||
|
||||
const authProfileId =
|
||||
(typeof (params as any).authProfileId === "string" && (params as any).authProfileId.trim()) ||
|
||||
(typeof pluginCfg.defaultAuthProfileId === "string" && pluginCfg.defaultAuthProfileId.trim()) ||
|
||||
undefined;
|
||||
|
||||
const modelKey = toModelKey(provider, model);
|
||||
if (!provider || !model || !modelKey) {
|
||||
throw new Error(
|
||||
`provider/model could not be resolved (provider=${String(provider ?? "")}, model=${String(model ?? "")})`,
|
||||
);
|
||||
}
|
||||
|
||||
const allowed = Array.isArray(pluginCfg.allowedModels) ? pluginCfg.allowedModels : undefined;
|
||||
if (allowed && allowed.length > 0 && !allowed.includes(modelKey)) {
|
||||
throw new Error(
|
||||
`Model not allowed by llm-task plugin config: ${modelKey}. Allowed models: ${allowed.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const timeoutMs =
|
||||
(typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : undefined) ||
|
||||
(typeof pluginCfg.timeoutMs === "number" && pluginCfg.timeoutMs > 0 ? pluginCfg.timeoutMs : undefined) ||
|
||||
30_000;
|
||||
|
||||
const streamParams = {
|
||||
temperature: typeof params.temperature === "number" ? params.temperature : undefined,
|
||||
maxTokens:
|
||||
typeof params.maxTokens === "number"
|
||||
? params.maxTokens
|
||||
: typeof pluginCfg.maxTokens === "number"
|
||||
? pluginCfg.maxTokens
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const input = (params as any).input as unknown;
|
||||
let inputJson: string;
|
||||
try {
|
||||
inputJson = JSON.stringify(input ?? null, null, 2);
|
||||
} catch {
|
||||
throw new Error("input must be JSON-serializable");
|
||||
}
|
||||
|
||||
const system = [
|
||||
"You are a JSON-only function.",
|
||||
"Return ONLY a valid JSON value.",
|
||||
"Do not wrap in markdown fences.",
|
||||
"Do not include commentary.",
|
||||
"Do not call tools.",
|
||||
].join(" ");
|
||||
|
||||
const fullPrompt = `${system}\n\nTASK:\n${prompt}\n\nINPUT_JSON:\n${inputJson}\n`;
|
||||
|
||||
let tmpDir: string | null = null;
|
||||
try {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-llm-task-"));
|
||||
const sessionId = `llm-task-${Date.now()}`;
|
||||
const sessionFile = path.join(tmpDir, "session.json");
|
||||
|
||||
const runEmbeddedPiAgent = await loadRunEmbeddedPiAgent();
|
||||
|
||||
const result = await runEmbeddedPiAgent({
|
||||
sessionId,
|
||||
sessionFile,
|
||||
workspaceDir: api.config?.agents?.defaults?.workspace ?? process.cwd(),
|
||||
config: api.config,
|
||||
prompt: fullPrompt,
|
||||
timeoutMs,
|
||||
runId: `llm-task-${Date.now()}`,
|
||||
provider,
|
||||
model,
|
||||
authProfileId,
|
||||
authProfileIdSource: authProfileId ? "user" : "auto",
|
||||
streamParams,
|
||||
disableTools: true,
|
||||
});
|
||||
|
||||
const text = collectText((result as any).payloads);
|
||||
if (!text) throw new Error("LLM returned empty output");
|
||||
|
||||
const raw = stripCodeFences(text);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new Error("LLM returned invalid JSON");
|
||||
}
|
||||
|
||||
const schema = (params as any).schema as unknown;
|
||||
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
||||
const validate = ajv.compile(schema as any);
|
||||
const ok = validate(parsed);
|
||||
if (!ok) {
|
||||
const msg =
|
||||
validate.errors?.map((e) => `${e.instancePath || "<root>"} ${e.message || "invalid"}`).join("; ") ??
|
||||
"invalid";
|
||||
throw new Error(`LLM JSON did not match schema: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
||||
details: { json: parsed, provider, model },
|
||||
};
|
||||
} finally {
|
||||
if (tmpDir) {
|
||||
try {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
import { matrixMessageActions } from "./actions.js";
|
||||
import { MatrixConfigSchema } from "./config-schema.js";
|
||||
import { resolveMatrixGroupRequireMention, resolveMatrixGroupToolPolicy } from "./group-mentions.js";
|
||||
import { resolveMatrixGroupRequireMention } from "./group-mentions.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
import {
|
||||
listMatrixAccountIds,
|
||||
@@ -167,7 +167,6 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveMatrixGroupRequireMention,
|
||||
resolveToolPolicy: resolveMatrixGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -26,7 +26,6 @@ const matrixRoomSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
autoReply: z.boolean().optional(),
|
||||
users: z.array(allowFromEntry).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
||||
import type { ChannelGroupContext } from "clawdbot/plugin-sdk";
|
||||
|
||||
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
@@ -32,30 +32,3 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveMatrixGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const rawGroupId = params.groupId?.trim() ?? "";
|
||||
let roomId = rawGroupId;
|
||||
const lower = roomId.toLowerCase();
|
||||
if (lower.startsWith("matrix:")) {
|
||||
roomId = roomId.slice("matrix:".length).trim();
|
||||
}
|
||||
if (roomId.toLowerCase().startsWith("channel:")) {
|
||||
roomId = roomId.slice("channel:".length).trim();
|
||||
}
|
||||
if (roomId.toLowerCase().startsWith("room:")) {
|
||||
roomId = roomId.slice("room:".length).trim();
|
||||
}
|
||||
const groupChannel = params.groupChannel?.trim() ?? "";
|
||||
const aliases = groupChannel ? [groupChannel] : [];
|
||||
const cfg = params.cfg as CoreConfig;
|
||||
const resolved = resolveMatrixRoomConfig({
|
||||
rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
|
||||
roomId,
|
||||
aliases,
|
||||
name: groupChannel || undefined,
|
||||
}).config;
|
||||
return resolved?.tools;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ export type MatrixRoomConfig = {
|
||||
allow?: boolean;
|
||||
/** Require mentioning the bot to trigger replies. */
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this room. */
|
||||
tools?: { allow?: string[]; deny?: string[] };
|
||||
/** If true, reply without mention requirements. */
|
||||
autoReply?: boolean;
|
||||
/** Optional allowlist for room senders (user IDs or localparts). */
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
||||
import { msteamsOutbound } from "./outbound.js";
|
||||
import { probeMSTeams } from "./probe.js";
|
||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||
import {
|
||||
normalizeMSTeamsMessagingTarget,
|
||||
normalizeMSTeamsUserInput,
|
||||
@@ -78,9 +77,6 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
hasRepliedRef,
|
||||
}),
|
||||
},
|
||||
groups: {
|
||||
resolveToolPolicy: resolveMSTeamsGroupToolPolicy,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.msteams"] },
|
||||
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
||||
config: {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type {
|
||||
AllowlistMatch,
|
||||
ChannelGroupContext,
|
||||
GroupPolicy,
|
||||
GroupToolPolicyConfig,
|
||||
MSTeamsChannelConfig,
|
||||
MSTeamsConfig,
|
||||
MSTeamsReplyStyle,
|
||||
@@ -88,50 +86,6 @@ export function resolveMSTeamsRouteConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMSTeamsGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const cfg = params.cfg.channels?.msteams;
|
||||
if (!cfg) return undefined;
|
||||
const groupId = params.groupId?.trim();
|
||||
const groupChannel = params.groupChannel?.trim();
|
||||
const groupSpace = params.groupSpace?.trim();
|
||||
|
||||
const resolved = resolveMSTeamsRouteConfig({
|
||||
cfg,
|
||||
teamId: groupSpace,
|
||||
teamName: groupSpace,
|
||||
conversationId: groupId,
|
||||
channelName: groupChannel,
|
||||
});
|
||||
|
||||
if (resolved.channelConfig) {
|
||||
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools;
|
||||
}
|
||||
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
|
||||
|
||||
if (!groupId) return undefined;
|
||||
|
||||
const channelCandidates = buildChannelKeyCandidates(
|
||||
groupId,
|
||||
groupChannel,
|
||||
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
||||
);
|
||||
for (const teamConfig of Object.values(cfg.teams ?? {})) {
|
||||
const match = resolveChannelEntryMatchWithFallback({
|
||||
entries: teamConfig?.channels ?? {},
|
||||
keys: channelCandidates,
|
||||
wildcardKey: "*",
|
||||
normalizeKey: normalizeChannelSlug,
|
||||
});
|
||||
if (match.entry) {
|
||||
return match.entry.tools ?? teamConfig?.tools;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type MSTeamsReplyPolicy = {
|
||||
requireMention: boolean;
|
||||
replyStyle: MSTeamsReplyStyle;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
|
||||
import { getNextcloudTalkRuntime } from "./runtime.js";
|
||||
import { sendMessageNextcloudTalk } from "./send.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
||||
|
||||
const meta = {
|
||||
id: "nextcloud-talk",
|
||||
@@ -160,7 +159,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
|
||||
return true;
|
||||
},
|
||||
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
ToolPolicySchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
@@ -12,7 +11,6 @@ import { z } from "zod";
|
||||
export const NextcloudTalkRoomSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.string()).optional(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AllowlistMatch, ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
||||
import type { AllowlistMatch, GroupPolicy } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
buildChannelKeyCandidates,
|
||||
normalizeChannelSlug,
|
||||
@@ -86,21 +86,6 @@ export function resolveNextcloudTalkRoomMatch(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveNextcloudTalkGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const cfg = params.cfg as { channels?: { "nextcloud-talk"?: { rooms?: Record<string, NextcloudTalkRoomConfig> } } };
|
||||
const roomToken = params.groupId?.trim();
|
||||
if (!roomToken) return undefined;
|
||||
const roomName = params.groupChannel?.trim() || undefined;
|
||||
const match = resolveNextcloudTalkRoomMatch({
|
||||
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
|
||||
roomToken,
|
||||
roomName,
|
||||
});
|
||||
return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
|
||||
}
|
||||
|
||||
export function resolveNextcloudTalkRequireMention(params: {
|
||||
roomConfig?: NextcloudTalkRoomConfig;
|
||||
wildcardConfig?: NextcloudTalkRoomConfig;
|
||||
|
||||
@@ -7,8 +7,6 @@ import type {
|
||||
|
||||
export type NextcloudTalkRoomConfig = {
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this room. */
|
||||
tools?: { allow?: string[]; deny?: string[] };
|
||||
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
|
||||
skills?: string[];
|
||||
/** If false, disable the bot for this room. */
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
resolveSlackAccount,
|
||||
resolveSlackReplyToMode,
|
||||
resolveSlackGroupRequireMention,
|
||||
resolveSlackGroupToolPolicy,
|
||||
buildSlackThreadingToolContext,
|
||||
setAccountEnabledInConfigSection,
|
||||
slackOnboardingAdapter,
|
||||
@@ -162,7 +161,6 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveSlackGroupRequireMention,
|
||||
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
resolveDefaultTelegramAccountId,
|
||||
resolveTelegramAccount,
|
||||
resolveTelegramGroupRequireMention,
|
||||
resolveTelegramGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
telegramOnboardingAdapter,
|
||||
TelegramConfigSchema,
|
||||
@@ -155,7 +154,6 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
resolveDefaultWhatsAppAccountId,
|
||||
resolveWhatsAppAccount,
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
resolveWhatsAppGroupToolPolicy,
|
||||
resolveWhatsAppHeartbeatRecipients,
|
||||
whatsappOnboardingAdapter,
|
||||
WhatsAppConfigSchema,
|
||||
@@ -199,7 +198,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
||||
resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
|
||||
resolveGroupIntroHint: () =>
|
||||
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
||||
},
|
||||
|
||||
@@ -2,10 +2,8 @@ import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelDirectoryEntry,
|
||||
ChannelDock,
|
||||
ChannelGroupContext,
|
||||
ChannelPlugin,
|
||||
ClawdbotConfig,
|
||||
GroupToolPolicyConfig,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
@@ -81,26 +79,6 @@ function mapGroup(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveZalouserGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const account = resolveZalouserAccountSync({
|
||||
cfg: params.cfg as ClawdbotConfig,
|
||||
accountId: params.accountId ?? undefined,
|
||||
});
|
||||
const groups = account.config.groups ?? {};
|
||||
const groupId = params.groupId?.trim();
|
||||
const groupChannel = params.groupChannel?.trim();
|
||||
const candidates = [groupId, groupChannel, "*"].filter(
|
||||
(value): value is string => Boolean(value),
|
||||
);
|
||||
for (const key of candidates) {
|
||||
const entry = groups[key];
|
||||
if (entry?.tools) return entry.tools;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const zalouserDock: ChannelDock = {
|
||||
id: "zalouser",
|
||||
capabilities: {
|
||||
@@ -123,7 +101,6 @@ export const zalouserDock: ChannelDock = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: () => true,
|
||||
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: () => "off",
|
||||
@@ -211,7 +188,6 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: () => true,
|
||||
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: () => "off",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -6,7 +6,6 @@ const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
const groupConfigSchema = z.object({
|
||||
allow: z.boolean().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
});
|
||||
|
||||
const zalouserAccountSchema = z.object({
|
||||
|
||||
@@ -75,7 +75,7 @@ export type ZalouserAccountConfig = {
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
allowFrom?: Array<string | number>;
|
||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
||||
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||
messagePrefix?: string;
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ export type ZalouserConfig = {
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
allowFrom?: Array<string | number>;
|
||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
||||
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||
messagePrefix?: string;
|
||||
accounts?: Record<string, ZalouserAccountConfig>;
|
||||
};
|
||||
|
||||
@@ -147,7 +147,6 @@
|
||||
"packageManager": "pnpm@10.23.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.13.0",
|
||||
"@aws-sdk/client-bedrock": "^3.975.0",
|
||||
"@buape/carbon": "0.14.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
|
||||
589
pnpm-lock.yaml
generated
589
pnpm-lock.yaml
generated
@@ -16,9 +16,6 @@ importers:
|
||||
'@agentclientprotocol/sdk':
|
||||
specifier: 0.13.0
|
||||
version: 0.13.0(zod@4.3.5)
|
||||
'@aws-sdk/client-bedrock':
|
||||
specifier: ^3.975.0
|
||||
version: 3.975.0
|
||||
'@buape/carbon':
|
||||
specifier: 0.14.0
|
||||
version: 0.14.0(hono@4.11.4)
|
||||
@@ -304,8 +301,6 @@ importers:
|
||||
|
||||
extensions/imessage: {}
|
||||
|
||||
extensions/llm-task: {}
|
||||
|
||||
extensions/lobster: {}
|
||||
|
||||
extensions/matrix:
|
||||
@@ -313,6 +308,9 @@ importers:
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
clawdbot:
|
||||
specifier: workspace:*
|
||||
version: link:../..
|
||||
markdown-it:
|
||||
specifier: 14.1.0
|
||||
version: 14.1.0
|
||||
@@ -322,13 +320,6 @@ importers:
|
||||
music-metadata:
|
||||
specifier: ^11.10.6
|
||||
version: 11.10.6
|
||||
zod:
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5
|
||||
devDependencies:
|
||||
clawdbot:
|
||||
specifier: workspace:*
|
||||
version: link:../..
|
||||
|
||||
extensions/mattermost: {}
|
||||
|
||||
@@ -502,90 +493,46 @@ packages:
|
||||
resolution: {integrity: sha512-rzSuqgMkL488bR9TnZEALBa+SV1FfR3B7CkYvs6R5uZm2AqBMfq7xNZR/pgMiAH/YLlI9FWAh1aPmdnG7iXxnA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/client-bedrock@3.975.0':
|
||||
resolution: {integrity: sha512-rA30CX0zcTGKx0S8JSyASVKFYTdQmkDkpkE5o1Mv4j3RmLcp7J2/WeYGVLjWprkNjlAlfpxG3V9VqPsayQ3LzA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/client-sso@3.972.0':
|
||||
resolution: {integrity: sha512-5qw6qLiRE4SUiz0hWy878dSR13tSVhbTWhsvFT8mGHe37NRRiaobm5MA2sWD0deRAuO98djSiV+dhWXa1xIFNw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/client-sso@3.974.0':
|
||||
resolution: {integrity: sha512-ci+GiM0c4ULo4D79UMcY06LcOLcfvUfiyt8PzNY0vbt5O8BfCPYf4QomwVgkNcLLCYmroO4ge2Yy1EsLUlcD6g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/core@3.972.0':
|
||||
resolution: {integrity: sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/core@3.973.1':
|
||||
resolution: {integrity: sha512-Ocubx42QsMyVs9ANSmFpRm0S+hubWljpPLjOi9UFrtcnVJjrVJTzQ51sN0e5g4e8i8QZ7uY73zosLmgYL7kZTQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-env@3.972.0':
|
||||
resolution: {integrity: sha512-kKHoNv+maHlPQOAhYamhap0PObd16SAb3jwaY0KYgNTiSbeXlbGUZPLioo9oA3wU10zItJzx83ClU7d7h40luA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-env@3.972.1':
|
||||
resolution: {integrity: sha512-/etNHqnx96phy/SjI0HRC588o4vKH5F0xfkZ13yAATV7aNrb+5gYGNE6ePWafP+FuZ3HkULSSlJFj0AxgrAqYw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-http@3.972.0':
|
||||
resolution: {integrity: sha512-xzEi81L7I5jGUbpmqEHCe7zZr54hCABdj4H+3LzktHYuovV/oqnvoDdvZpGFR0e/KAw1+PL38NbGrpG30j6qlA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-http@3.972.2':
|
||||
resolution: {integrity: sha512-mXgdaUfe5oM+tWKyeZ7Vh/iQ94FrkMky1uuzwTOmFADiRcSk5uHy/e3boEFedXiT/PRGzgBmqvJVK4F6lUISCg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-ini@3.972.0':
|
||||
resolution: {integrity: sha512-ruhAMceUIq2aknFd3jhWxmO0P0Efab5efjyIXOkI9i80g+zDY5VekeSxfqRKStEEJSKSCHDLQuOu0BnAn4Rzew==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-ini@3.972.1':
|
||||
resolution: {integrity: sha512-OdbJA3v+XlNDsrYzNPRUwr8l7gw1r/nR8l4r96MDzSBDU8WEo8T6C06SvwaXR8SpzsjO3sq5KMP86wXWg7Rj4g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-login@3.972.0':
|
||||
resolution: {integrity: sha512-SsrsFJsEYAJHO4N/r2P0aK6o8si6f1lprR+Ej8J731XJqTckSGs/HFHcbxOyW/iKt+LNUvZa59/VlJmjhF4bEQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-login@3.972.1':
|
||||
resolution: {integrity: sha512-CccqDGL6ZrF3/EFWZefvKW7QwwRdxlHUO8NVBKNVcNq6womrPDvqB6xc9icACtE0XB0a7PLoSTkAg8bQVkTO2w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.972.0':
|
||||
resolution: {integrity: sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.972.1':
|
||||
resolution: {integrity: sha512-DwXPk9GfuU/xG9tmCyXFVkCr6X3W8ZCoL5Ptb0pbltEx1/LCcg7T+PBqDlPiiinNCD6ilIoMJDWsnJ8ikzZA7Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-process@3.972.0':
|
||||
resolution: {integrity: sha512-nmzYhamLDJ8K+v3zWck79IaKMc350xZnWsf/GeaXO6E3MewSzd3lYkTiMi7lEp3/UwDm9NHfPguoPm+mhlSWQQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-process@3.972.1':
|
||||
resolution: {integrity: sha512-bi47Zigu3692SJwdBvo8y1dEwE6B61stCwCFnuRWJVTfiM84B+VTSCV661CSWJmIZzmcy7J5J3kWyxL02iHj0w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-sso@3.972.0':
|
||||
resolution: {integrity: sha512-6mYyfk1SrMZ15cH9T53yAF4YSnvq4yU1Xlgm3nqV1gZVQzmF5kr4t/F3BU3ygbvzi4uSwWxG3I3TYYS5eMlAyg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-sso@3.972.1':
|
||||
resolution: {integrity: sha512-dLZVNhM7wSgVUFsgVYgI5hb5Z/9PUkT46pk/SHrSmUqfx6YDvoV4YcPtaiRqviPpEGGiRtdQMEadyOKIRqulUQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-web-identity@3.972.0':
|
||||
resolution: {integrity: sha512-vsJXBGL8H54kz4T6do3p5elATj5d1izVGUXMluRJntm9/I0be/zUYtdd4oDTM2kSUmd4Zhyw3fMQ9lw7CVhd4A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/credential-provider-web-identity@3.972.1':
|
||||
resolution: {integrity: sha512-YMDeYgi0u687Ay0dAq/pFPKuijrlKTgsaB/UATbxCs/FzZfMiG4If5ksywHmmW7MiYUF8VVv+uou3TczvLrN4w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/eventstream-handler-node@3.972.0':
|
||||
resolution: {integrity: sha512-B1AEv+TQOVxg2t60GMfrcagJvQjpx1p6UASUoFMLevV9K3WNI5qYTjtutMiifKY0HwK6g86zXgN/dpeaSi3q5Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@@ -598,34 +545,18 @@ packages:
|
||||
resolution: {integrity: sha512-3eztFI6F9/eHtkIaWKN3nT+PM+eQ6p1MALDuNshFk323ixuCZzOOVT8oUqtZa30Z6dycNXJwhlIq7NhUVFfimw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-host-header@3.972.1':
|
||||
resolution: {integrity: sha512-/R82lXLPmZ9JaUGSUdKtBp2k/5xQxvBT3zZWyKiBOhyulFotlfvdlrO8TnqstBimsl4lYEYySDL+W6ldFh6ALg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-logger@3.972.0':
|
||||
resolution: {integrity: sha512-ZvdyVRwzK+ra31v1pQrgbqR/KsLD+wwJjHgko6JfoKUBIcEfAwJzQKO6HspHxdHWTVUz6MgvwskheR/TTYZl2g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-logger@3.972.1':
|
||||
resolution: {integrity: sha512-JGgFl6cHg9G2FHu4lyFIzmFN8KESBiRr84gLC3Aeni0Gt1nKm+KxWLBuha/RPcXxJygGXCcMM4AykkIwxor8RA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-recursion-detection@3.972.0':
|
||||
resolution: {integrity: sha512-F2SmUeO+S6l1h6dydNet3BQIk173uAkcfU1HDkw/bUdRLAnh15D3HP9vCZ7oCPBNcdEICbXYDmx0BR9rRUHGlQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-recursion-detection@3.972.1':
|
||||
resolution: {integrity: sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-user-agent@3.972.0':
|
||||
resolution: {integrity: sha512-kFHQm2OCBJCzGWRafgdWHGFjitUXY/OxXngymcX4l8CiyiNDZB27HDDBg2yLj3OUJc4z4fexLMmP8r9vgag19g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-user-agent@3.972.2':
|
||||
resolution: {integrity: sha512-d+Exq074wy0X6wvShg/kmZVtkah+28vMuqCtuY3cydg8LUZOJBtbAolCpEJizSyb8mJJZF9BjWaTANXL4OYnkg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/middleware-websocket@3.972.0':
|
||||
resolution: {integrity: sha512-3pvbb/HtE7A8U38jk24RQ9T92d40NNSzjDEVEkBYZYhxExVcJ/Lk5Z+NM283FEtoi1T++oYrLuYDr1CIQxnaXQ==}
|
||||
engines: {node: '>= 14.0.0'}
|
||||
@@ -634,42 +565,18 @@ packages:
|
||||
resolution: {integrity: sha512-QGlbnuGzSQJVG6bR9Qw6G0Blh6abFR4VxNa61ttMbzy9jt28xmk2iGtrYLrQPlCCPhY6enHqjTWm3n3LOb0wAw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/nested-clients@3.974.0':
|
||||
resolution: {integrity: sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/nested-clients@3.975.0':
|
||||
resolution: {integrity: sha512-OkeFHPlQj2c/Y5bQGkX14pxhDWUGUFt3LRHhjcDKsSCw6lrxKcxN3WFZN0qbJwKNydP+knL5nxvfgKiCLpTLRA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/region-config-resolver@3.972.0':
|
||||
resolution: {integrity: sha512-JyOf+R/6vJW8OEVFCAyzEOn2reri/Q+L0z9zx4JQSKWvTmJ1qeFO25sOm8VIfB8URKhfGRTQF30pfYaH2zxt/A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/region-config-resolver@3.972.1':
|
||||
resolution: {integrity: sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/token-providers@3.972.0':
|
||||
resolution: {integrity: sha512-kWlXG+y5nZhgXGEtb72Je+EvqepBPs8E3vZse//1PYLWs2speFqbGE/ywCXmzEJgHgVqSB/u/lqBvs5WlYmSqQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/token-providers@3.974.0':
|
||||
resolution: {integrity: sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/token-providers@3.975.0':
|
||||
resolution: {integrity: sha512-AWQt64hkVbDQ+CmM09wnvSk2mVyH4iRROkmYkr3/lmUtFNbE2L/fnw26sckZnUcFCsHPqbkQrcsZAnTcBLbH4w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/types@3.972.0':
|
||||
resolution: {integrity: sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/types@3.973.0':
|
||||
resolution: {integrity: sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/util-endpoints@3.972.0':
|
||||
resolution: {integrity: sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@@ -685,9 +592,6 @@ packages:
|
||||
'@aws-sdk/util-user-agent-browser@3.972.0':
|
||||
resolution: {integrity: sha512-eOLdkQyoRbDgioTS3Orr7iVsVEutJyMZxvyZ6WAF95IrF0kfWx5Rd/KXnfbnG/VKa2CvjZiitWfouLzfVEyvJA==}
|
||||
|
||||
'@aws-sdk/util-user-agent-browser@3.972.1':
|
||||
resolution: {integrity: sha512-IgF55NFmJX8d9Wql9M0nEpk2eYbuD8G4781FN4/fFgwTXBn86DvlZJuRWDCMcMqZymnBVX7HW9r+3r9ylqfW0w==}
|
||||
|
||||
'@aws-sdk/util-user-agent-node@3.972.0':
|
||||
resolution: {integrity: sha512-GOy+AiSrE9kGiojiwlZvVVSXwylu4+fmP0MJfvras/MwP09RB/YtQuOVR1E0fKQc6OMwaTNBjgAbOEhxuWFbAw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@@ -697,23 +601,10 @@ packages:
|
||||
aws-crt:
|
||||
optional: true
|
||||
|
||||
'@aws-sdk/util-user-agent-node@3.972.1':
|
||||
resolution: {integrity: sha512-oIs4JFcADzoZ0c915R83XvK2HltWupxNsXUIuZse2rgk7b97zTpkxaqXiH0h9ylh31qtgo/t8hp4tIqcsMrEbQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
aws-crt: '>=1.0.0'
|
||||
peerDependenciesMeta:
|
||||
aws-crt:
|
||||
optional: true
|
||||
|
||||
'@aws-sdk/xml-builder@3.972.0':
|
||||
resolution: {integrity: sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/xml-builder@3.972.1':
|
||||
resolution: {integrity: sha512-6zZGlPOqn7Xb+25MAXGb1JhgvaC5HjZj6GzszuVrnEgbhvzBRFGKYemuHBV4bho+dtqeYKPgaZUv7/e80hIGNg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws/lambda-invoke-store@0.2.3':
|
||||
resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -2330,10 +2221,6 @@ packages:
|
||||
resolution: {integrity: sha512-bg2TfzgsERyETAxc/Ims/eJX8eAnIeTi4r4LHpMpfF/2NyO6RsWis0rjKcCPaGksljmOb23BZRiCeT/3NvwkXw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/core@3.21.1':
|
||||
resolution: {integrity: sha512-NUH8R4O6FkN8HKMojzbGg/5pNjsfTjlMmeFclyPfPaXXUrbr5TzhWgbf7t92wfrpCHRgpjyz7ffASIS3wX28aA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/credential-provider-imds@4.2.8':
|
||||
resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -2386,18 +2273,10 @@ packages:
|
||||
resolution: {integrity: sha512-kwWpNltpxrvPabnjEFvwSmA+66l6s2ReCvgVSzW/z92LU4T28fTdgZ18IdYRYOrisu2NMQ0jUndRScbO65A/zg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/middleware-endpoint@4.4.11':
|
||||
resolution: {integrity: sha512-/WqsrycweGGfb9sSzME4CrsuayjJF6BueBmkKlcbeU5q18OhxRrvvKlmfw3tpDsK5ilx2XUJvoukwxHB0nHs/Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/middleware-retry@4.4.26':
|
||||
resolution: {integrity: sha512-ozZMoTAr+B2aVYfLYfkssFvc8ZV3p/vLpVQ7/k277xxUOA9ykSPe5obL2j6yHfbdrM/SZV7qj0uk/hSqavHrLw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/middleware-retry@4.4.27':
|
||||
resolution: {integrity: sha512-xFUYCGRVsfgiN5EjsJJSzih9+yjStgMTCLANPlf0LVQkPDYCe0hz97qbdTZosFOiYlGBlHYityGRxrQ/hxhfVQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/middleware-serde@4.2.9':
|
||||
resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -2446,10 +2325,6 @@ packages:
|
||||
resolution: {integrity: sha512-6o804SCyHGMXAb5mFJ+iTy9kVKv7F91a9szN0J+9X6p8A0NrdpUxdaC57aye2ipQkP2C4IAqETEpGZ0Zj77Haw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/smithy-client@4.10.12':
|
||||
resolution: {integrity: sha512-VKO/HKoQ5OrSHW6AJUmEnUKeXI1/5LfCwO9cwyao7CmLvGnZeM1i36Lyful3LK1XU7HwTVieTqO1y2C/6t3qtA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/types@4.12.0':
|
||||
resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -2486,18 +2361,10 @@ packages:
|
||||
resolution: {integrity: sha512-8ugoNMtss2dJHsXnqsibGPqoaafvWJPACmYKxJ4E6QWaDrixsAemmiMMAVbvwYadjR0H9G2+AlzsInSzRi8PSw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/util-defaults-mode-browser@4.3.26':
|
||||
resolution: {integrity: sha512-vva0dzYUTgn7DdE0uaha10uEdAgmdLnNFowKFjpMm6p2R0XDk5FHPX3CBJLzWQkQXuEprsb0hGz9YwbicNWhjw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/util-defaults-mode-node@4.2.28':
|
||||
resolution: {integrity: sha512-mjUdcP8h3E0K/XvNMi9oBXRV3DMCzeRiYIieZ1LQ7jq5tu6GH/GTWym7a1xIIE0pKSoLcpGsaImuQhGPSIJzAA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/util-defaults-mode-node@4.2.29':
|
||||
resolution: {integrity: sha512-c6D7IUBsZt/aNnTBHMTf+OVh+h/JcxUUgfTcIJaWRe6zhOum1X+pNKSZtZ+7fbOn5I99XVFtmrnXKv8yHHErTQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/util-endpoints@3.2.8':
|
||||
resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -5464,7 +5331,7 @@ snapshots:
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-crypto/supports-web-crypto': 5.2.0
|
||||
'@aws-crypto/util': 5.2.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/types': 3.972.0
|
||||
'@aws-sdk/util-locate-window': 3.965.3
|
||||
'@smithy/util-utf8': 2.3.0
|
||||
tslib: 2.8.1
|
||||
@@ -5472,7 +5339,7 @@ snapshots:
|
||||
'@aws-crypto/sha256-js@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/types': 3.972.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-crypto/supports-web-crypto@5.2.0':
|
||||
@@ -5537,51 +5404,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/client-bedrock@3.975.0':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/credential-provider-node': 3.972.1
|
||||
'@aws-sdk/middleware-host-header': 3.972.1
|
||||
'@aws-sdk/middleware-logger': 3.972.1
|
||||
'@aws-sdk/middleware-recursion-detection': 3.972.1
|
||||
'@aws-sdk/middleware-user-agent': 3.972.2
|
||||
'@aws-sdk/region-config-resolver': 3.972.1
|
||||
'@aws-sdk/token-providers': 3.975.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/util-endpoints': 3.972.0
|
||||
'@aws-sdk/util-user-agent-browser': 3.972.1
|
||||
'@aws-sdk/util-user-agent-node': 3.972.1
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/hash-node': 4.2.8
|
||||
'@smithy/invalid-dependency': 4.2.8
|
||||
'@smithy/middleware-content-length': 4.2.8
|
||||
'@smithy/middleware-endpoint': 4.4.11
|
||||
'@smithy/middleware-retry': 4.4.27
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-body-length-node': 4.2.1
|
||||
'@smithy/util-defaults-mode-browser': 4.3.26
|
||||
'@smithy/util-defaults-mode-node': 4.2.29
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/client-sso@3.972.0':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
@@ -5625,49 +5447,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/client-sso@3.974.0':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/middleware-host-header': 3.972.1
|
||||
'@aws-sdk/middleware-logger': 3.972.1
|
||||
'@aws-sdk/middleware-recursion-detection': 3.972.1
|
||||
'@aws-sdk/middleware-user-agent': 3.972.2
|
||||
'@aws-sdk/region-config-resolver': 3.972.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/util-endpoints': 3.972.0
|
||||
'@aws-sdk/util-user-agent-browser': 3.972.1
|
||||
'@aws-sdk/util-user-agent-node': 3.972.1
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/hash-node': 4.2.8
|
||||
'@smithy/invalid-dependency': 4.2.8
|
||||
'@smithy/middleware-content-length': 4.2.8
|
||||
'@smithy/middleware-endpoint': 4.4.11
|
||||
'@smithy/middleware-retry': 4.4.27
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-body-length-node': 4.2.1
|
||||
'@smithy/util-defaults-mode-browser': 4.3.26
|
||||
'@smithy/util-defaults-mode-node': 4.2.29
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/core@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.972.0
|
||||
@@ -5684,22 +5463,6 @@ snapshots:
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/core@3.973.1':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/xml-builder': 3.972.1
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/signature-v4': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/credential-provider-env@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -5708,14 +5471,6 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/credential-provider-env@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/credential-provider-http@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -5729,19 +5484,6 @@ snapshots:
|
||||
'@smithy/util-stream': 4.5.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/credential-provider-http@3.972.2':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-stream': 4.5.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/credential-provider-ini@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -5761,25 +5503,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-ini@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/credential-provider-env': 3.972.1
|
||||
'@aws-sdk/credential-provider-http': 3.972.2
|
||||
'@aws-sdk/credential-provider-login': 3.972.1
|
||||
'@aws-sdk/credential-provider-process': 3.972.1
|
||||
'@aws-sdk/credential-provider-sso': 3.972.1
|
||||
'@aws-sdk/credential-provider-web-identity': 3.972.1
|
||||
'@aws-sdk/nested-clients': 3.974.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/credential-provider-imds': 4.2.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-login@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -5793,19 +5516,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-login@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/nested-clients': 3.974.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.972.0
|
||||
@@ -5823,23 +5533,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.972.1
|
||||
'@aws-sdk/credential-provider-http': 3.972.2
|
||||
'@aws-sdk/credential-provider-ini': 3.972.1
|
||||
'@aws-sdk/credential-provider-process': 3.972.1
|
||||
'@aws-sdk/credential-provider-sso': 3.972.1
|
||||
'@aws-sdk/credential-provider-web-identity': 3.972.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/credential-provider-imds': 4.2.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-process@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -5849,15 +5542,6 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/credential-provider-process@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/credential-provider-sso@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso': 3.972.0
|
||||
@@ -5871,19 +5555,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-sso@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso': 3.974.0
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/token-providers': 3.974.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-web-identity@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -5896,18 +5567,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-web-identity@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/nested-clients': 3.974.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/eventstream-handler-node@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.972.0
|
||||
@@ -5929,25 +5588,12 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-host-header@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-logger@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.972.0
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-logger@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-recursion-detection@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.972.0
|
||||
@@ -5956,14 +5602,6 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-recursion-detection@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws/lambda-invoke-store': 0.2.3
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-user-agent@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -5974,16 +5612,6 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-user-agent@3.972.2':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/util-endpoints': 3.972.0
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/middleware-websocket@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.972.0
|
||||
@@ -6040,92 +5668,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/nested-clients@3.974.0':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/middleware-host-header': 3.972.1
|
||||
'@aws-sdk/middleware-logger': 3.972.1
|
||||
'@aws-sdk/middleware-recursion-detection': 3.972.1
|
||||
'@aws-sdk/middleware-user-agent': 3.972.2
|
||||
'@aws-sdk/region-config-resolver': 3.972.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/util-endpoints': 3.972.0
|
||||
'@aws-sdk/util-user-agent-browser': 3.972.1
|
||||
'@aws-sdk/util-user-agent-node': 3.972.1
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/hash-node': 4.2.8
|
||||
'@smithy/invalid-dependency': 4.2.8
|
||||
'@smithy/middleware-content-length': 4.2.8
|
||||
'@smithy/middleware-endpoint': 4.4.11
|
||||
'@smithy/middleware-retry': 4.4.27
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-body-length-node': 4.2.1
|
||||
'@smithy/util-defaults-mode-browser': 4.3.26
|
||||
'@smithy/util-defaults-mode-node': 4.2.29
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/nested-clients@3.975.0':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/middleware-host-header': 3.972.1
|
||||
'@aws-sdk/middleware-logger': 3.972.1
|
||||
'@aws-sdk/middleware-recursion-detection': 3.972.1
|
||||
'@aws-sdk/middleware-user-agent': 3.972.2
|
||||
'@aws-sdk/region-config-resolver': 3.972.1
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@aws-sdk/util-endpoints': 3.972.0
|
||||
'@aws-sdk/util-user-agent-browser': 3.972.1
|
||||
'@aws-sdk/util-user-agent-node': 3.972.1
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/hash-node': 4.2.8
|
||||
'@smithy/invalid-dependency': 4.2.8
|
||||
'@smithy/middleware-content-length': 4.2.8
|
||||
'@smithy/middleware-endpoint': 4.4.11
|
||||
'@smithy/middleware-retry': 4.4.27
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-body-length-node': 4.2.1
|
||||
'@smithy/util-defaults-mode-browser': 4.3.26
|
||||
'@smithy/util-defaults-mode-node': 4.2.29
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/region-config-resolver@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.972.0
|
||||
@@ -6134,14 +5676,6 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/region-config-resolver@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/token-providers@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.972.0
|
||||
@@ -6154,40 +5688,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/token-providers@3.974.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/nested-clients': 3.974.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/token-providers@3.975.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.973.1
|
||||
'@aws-sdk/nested-clients': 3.975.0
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/types@3.972.0':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/types@3.973.0':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/util-endpoints@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.972.0
|
||||
@@ -6214,13 +5719,6 @@ snapshots:
|
||||
bowser: 2.13.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/util-user-agent-browser@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/types': 4.12.0
|
||||
bowser: 2.13.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/util-user-agent-node@3.972.0':
|
||||
dependencies:
|
||||
'@aws-sdk/middleware-user-agent': 3.972.0
|
||||
@@ -6229,26 +5727,12 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/util-user-agent-node@3.972.1':
|
||||
dependencies:
|
||||
'@aws-sdk/middleware-user-agent': 3.972.2
|
||||
'@aws-sdk/types': 3.973.0
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/xml-builder@3.972.0':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
fast-xml-parser: 5.2.5
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/xml-builder@3.972.1':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
fast-xml-parser: 5.2.5
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws/lambda-invoke-store@0.2.3': {}
|
||||
|
||||
'@azure/abort-controller@2.1.2':
|
||||
@@ -7818,19 +7302,6 @@ snapshots:
|
||||
'@smithy/uuid': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/core@3.21.1':
|
||||
dependencies:
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-stream': 4.5.10
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
'@smithy/uuid': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/credential-provider-imds@4.2.8':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
@@ -7914,17 +7385,6 @@ snapshots:
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/middleware-endpoint@4.4.11':
|
||||
dependencies:
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/middleware-retry@4.4.26':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
@@ -7937,18 +7397,6 @@ snapshots:
|
||||
'@smithy/uuid': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/middleware-retry@4.4.27':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/service-error-classification': 4.2.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/uuid': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/middleware-serde@4.2.9':
|
||||
dependencies:
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
@@ -8026,16 +7474,6 @@ snapshots:
|
||||
'@smithy/util-stream': 4.5.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/smithy-client@4.10.12':
|
||||
dependencies:
|
||||
'@smithy/core': 3.21.1
|
||||
'@smithy/middleware-endpoint': 4.4.11
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-stream': 4.5.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/types@4.12.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -8081,13 +7519,6 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/util-defaults-mode-browser@4.3.26':
|
||||
dependencies:
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/util-defaults-mode-node@4.2.28':
|
||||
dependencies:
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
@@ -8098,16 +7529,6 @@ snapshots:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/util-defaults-mode-node@4.2.29':
|
||||
dependencies:
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/credential-provider-imds': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/smithy-client': 4.10.12
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/util-endpoints@3.2.8':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
|
||||
@@ -129,25 +129,4 @@ describe("exec approvals", () => {
|
||||
expect(calls).toContain("node.invoke");
|
||||
expect(calls).not.toContain("exec.approval.request");
|
||||
});
|
||||
|
||||
it("honors ask=off for elevated gateway exec without prompting", async () => {
|
||||
const { callGatewayTool } = await import("./tools/gateway.js");
|
||||
const calls: string[] = [];
|
||||
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
|
||||
calls.push(method);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
const { createExecTool } = await import("./bash-tools.exec.js");
|
||||
const tool = createExecTool({
|
||||
ask: "off",
|
||||
security: "full",
|
||||
approvalRunningNoticeMs: 0,
|
||||
elevated: { enabled: true, allowed: true, defaultLevel: "ask" },
|
||||
});
|
||||
|
||||
const result = await tool.execute("call3", { command: "echo ok", elevated: true });
|
||||
expect(result.details.status).toBe("completed");
|
||||
expect(calls).not.toContain("exec.approval.request");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -838,7 +838,10 @@ export function createExecTool(
|
||||
applyPathPrepend(env, defaultPathPrepend);
|
||||
|
||||
if (host === "node") {
|
||||
const approvals = resolveExecApprovals(agentId, { security, ask });
|
||||
const approvals = resolveExecApprovals(
|
||||
agentId,
|
||||
host === "node" ? { security: "allowlist" } : undefined,
|
||||
);
|
||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||
const askFallback = approvals.agent.askFallback;
|
||||
@@ -1109,7 +1112,7 @@ export function createExecTool(
|
||||
}
|
||||
|
||||
if (host === "gateway" && !bypassApprovals) {
|
||||
const approvals = resolveExecApprovals(agentId, { security, ask });
|
||||
const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
|
||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||
const askFallback = approvals.agent.askFallback;
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import type { BedrockClient } from "@aws-sdk/client-bedrock";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendMock = vi.fn();
|
||||
const clientFactory = () => ({ send: sendMock }) as unknown as BedrockClient;
|
||||
|
||||
describe("bedrock discovery", () => {
|
||||
beforeEach(() => {
|
||||
sendMock.mockReset();
|
||||
});
|
||||
|
||||
it("filters to active streaming text models and maps modalities", async () => {
|
||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
||||
await import("./bedrock-discovery.js");
|
||||
resetBedrockDiscoveryCacheForTest();
|
||||
|
||||
sendMock.mockResolvedValueOnce({
|
||||
modelSummaries: [
|
||||
{
|
||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
modelName: "Claude 3.7 Sonnet",
|
||||
providerName: "anthropic",
|
||||
inputModalities: ["TEXT", "IMAGE"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
{
|
||||
modelId: "anthropic.claude-3-haiku-20240307-v1:0",
|
||||
modelName: "Claude 3 Haiku",
|
||||
providerName: "anthropic",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: false,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
{
|
||||
modelId: "meta.llama3-8b-instruct-v1:0",
|
||||
modelName: "Llama 3 8B",
|
||||
providerName: "meta",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "INACTIVE" },
|
||||
},
|
||||
{
|
||||
modelId: "amazon.titan-embed-text-v1",
|
||||
modelName: "Titan Embed",
|
||||
providerName: "amazon",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["EMBEDDING"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
|
||||
expect(models).toHaveLength(1);
|
||||
expect(models[0]).toMatchObject({
|
||||
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
name: "Claude 3.7 Sonnet",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 32000,
|
||||
maxTokens: 4096,
|
||||
});
|
||||
});
|
||||
|
||||
it("applies provider filter", async () => {
|
||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
||||
await import("./bedrock-discovery.js");
|
||||
resetBedrockDiscoveryCacheForTest();
|
||||
|
||||
sendMock.mockResolvedValueOnce({
|
||||
modelSummaries: [
|
||||
{
|
||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
modelName: "Claude 3.7 Sonnet",
|
||||
providerName: "anthropic",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const models = await discoverBedrockModels({
|
||||
region: "us-east-1",
|
||||
config: { providerFilter: ["amazon"] },
|
||||
clientFactory,
|
||||
});
|
||||
expect(models).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("uses configured defaults for context and max tokens", async () => {
|
||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
||||
await import("./bedrock-discovery.js");
|
||||
resetBedrockDiscoveryCacheForTest();
|
||||
|
||||
sendMock.mockResolvedValueOnce({
|
||||
modelSummaries: [
|
||||
{
|
||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
modelName: "Claude 3.7 Sonnet",
|
||||
providerName: "anthropic",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const models = await discoverBedrockModels({
|
||||
region: "us-east-1",
|
||||
config: { defaultContextWindow: 64000, defaultMaxTokens: 8192 },
|
||||
clientFactory,
|
||||
});
|
||||
expect(models[0]).toMatchObject({ contextWindow: 64000, maxTokens: 8192 });
|
||||
});
|
||||
|
||||
it("caches results when refreshInterval is enabled", async () => {
|
||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
||||
await import("./bedrock-discovery.js");
|
||||
resetBedrockDiscoveryCacheForTest();
|
||||
|
||||
sendMock.mockResolvedValueOnce({
|
||||
modelSummaries: [
|
||||
{
|
||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
modelName: "Claude 3.7 Sonnet",
|
||||
providerName: "anthropic",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await discoverBedrockModels({ region: "us-east-1", clientFactory });
|
||||
await discoverBedrockModels({ region: "us-east-1", clientFactory });
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips cache when refreshInterval is 0", async () => {
|
||||
const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } =
|
||||
await import("./bedrock-discovery.js");
|
||||
resetBedrockDiscoveryCacheForTest();
|
||||
|
||||
sendMock
|
||||
.mockResolvedValueOnce({
|
||||
modelSummaries: [
|
||||
{
|
||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
modelName: "Claude 3.7 Sonnet",
|
||||
providerName: "anthropic",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
modelSummaries: [
|
||||
{
|
||||
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
modelName: "Claude 3.7 Sonnet",
|
||||
providerName: "anthropic",
|
||||
inputModalities: ["TEXT"],
|
||||
outputModalities: ["TEXT"],
|
||||
responseStreamingSupported: true,
|
||||
modelLifecycle: { status: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await discoverBedrockModels({
|
||||
region: "us-east-1",
|
||||
config: { refreshInterval: 0 },
|
||||
clientFactory,
|
||||
});
|
||||
await discoverBedrockModels({
|
||||
region: "us-east-1",
|
||||
config: { refreshInterval: 0 },
|
||||
clientFactory,
|
||||
});
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
import {
|
||||
BedrockClient,
|
||||
ListFoundationModelsCommand,
|
||||
type ListFoundationModelsCommandOutput,
|
||||
} from "@aws-sdk/client-bedrock";
|
||||
|
||||
import type { BedrockDiscoveryConfig, ModelDefinitionConfig } from "../config/types.js";
|
||||
|
||||
const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600;
|
||||
const DEFAULT_CONTEXT_WINDOW = 32000;
|
||||
const DEFAULT_MAX_TOKENS = 4096;
|
||||
const DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
type BedrockModelSummary = NonNullable<ListFoundationModelsCommandOutput["modelSummaries"]>[number];
|
||||
|
||||
type BedrockDiscoveryCacheEntry = {
|
||||
expiresAt: number;
|
||||
value?: ModelDefinitionConfig[];
|
||||
inFlight?: Promise<ModelDefinitionConfig[]>;
|
||||
};
|
||||
|
||||
const discoveryCache = new Map<string, BedrockDiscoveryCacheEntry>();
|
||||
let hasLoggedBedrockError = false;
|
||||
|
||||
function normalizeProviderFilter(filter?: string[]): string[] {
|
||||
if (!filter || filter.length === 0) return [];
|
||||
const normalized = new Set(
|
||||
filter.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0),
|
||||
);
|
||||
return Array.from(normalized).sort();
|
||||
}
|
||||
|
||||
function buildCacheKey(params: {
|
||||
region: string;
|
||||
providerFilter: string[];
|
||||
refreshIntervalSeconds: number;
|
||||
defaultContextWindow: number;
|
||||
defaultMaxTokens: number;
|
||||
}): string {
|
||||
return JSON.stringify(params);
|
||||
}
|
||||
|
||||
function includesTextModalities(modalities?: Array<string>): boolean {
|
||||
return (modalities ?? []).some((entry) => entry.toLowerCase() === "text");
|
||||
}
|
||||
|
||||
function isActive(summary: BedrockModelSummary): boolean {
|
||||
const status = summary.modelLifecycle?.status;
|
||||
return typeof status === "string" ? status.toUpperCase() === "ACTIVE" : false;
|
||||
}
|
||||
|
||||
function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image"> {
|
||||
const inputs = summary.inputModalities ?? [];
|
||||
const mapped = new Set<"text" | "image">();
|
||||
for (const modality of inputs) {
|
||||
const lower = modality.toLowerCase();
|
||||
if (lower === "text") mapped.add("text");
|
||||
if (lower === "image") mapped.add("image");
|
||||
}
|
||||
if (mapped.size === 0) mapped.add("text");
|
||||
return Array.from(mapped);
|
||||
}
|
||||
|
||||
function inferReasoningSupport(summary: BedrockModelSummary): boolean {
|
||||
const haystack = `${summary.modelId ?? ""} ${summary.modelName ?? ""}`.toLowerCase();
|
||||
return haystack.includes("reasoning") || haystack.includes("thinking");
|
||||
}
|
||||
|
||||
function resolveDefaultContextWindow(config?: BedrockDiscoveryConfig): number {
|
||||
const value = Math.floor(config?.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW);
|
||||
return value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
|
||||
}
|
||||
|
||||
function resolveDefaultMaxTokens(config?: BedrockDiscoveryConfig): number {
|
||||
const value = Math.floor(config?.defaultMaxTokens ?? DEFAULT_MAX_TOKENS);
|
||||
return value > 0 ? value : DEFAULT_MAX_TOKENS;
|
||||
}
|
||||
|
||||
function matchesProviderFilter(summary: BedrockModelSummary, filter: string[]): boolean {
|
||||
if (filter.length === 0) return true;
|
||||
const providerName =
|
||||
summary.providerName ??
|
||||
(typeof summary.modelId === "string" ? summary.modelId.split(".")[0] : undefined);
|
||||
const normalized = providerName?.trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
return filter.includes(normalized);
|
||||
}
|
||||
|
||||
function shouldIncludeSummary(summary: BedrockModelSummary, filter: string[]): boolean {
|
||||
if (!summary.modelId?.trim()) return false;
|
||||
if (!matchesProviderFilter(summary, filter)) return false;
|
||||
if (summary.responseStreamingSupported !== true) return false;
|
||||
if (!includesTextModalities(summary.outputModalities)) return false;
|
||||
if (!isActive(summary)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function toModelDefinition(
|
||||
summary: BedrockModelSummary,
|
||||
defaults: { contextWindow: number; maxTokens: number },
|
||||
): ModelDefinitionConfig {
|
||||
const id = summary.modelId?.trim() ?? "";
|
||||
return {
|
||||
id,
|
||||
name: summary.modelName?.trim() || id,
|
||||
reasoning: inferReasoningSupport(summary),
|
||||
input: mapInputModalities(summary),
|
||||
cost: DEFAULT_COST,
|
||||
contextWindow: defaults.contextWindow,
|
||||
maxTokens: defaults.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetBedrockDiscoveryCacheForTest(): void {
|
||||
discoveryCache.clear();
|
||||
hasLoggedBedrockError = false;
|
||||
}
|
||||
|
||||
export async function discoverBedrockModels(params: {
|
||||
region: string;
|
||||
config?: BedrockDiscoveryConfig;
|
||||
now?: () => number;
|
||||
clientFactory?: (region: string) => BedrockClient;
|
||||
}): Promise<ModelDefinitionConfig[]> {
|
||||
const refreshIntervalSeconds = Math.max(
|
||||
0,
|
||||
Math.floor(params.config?.refreshInterval ?? DEFAULT_REFRESH_INTERVAL_SECONDS),
|
||||
);
|
||||
const providerFilter = normalizeProviderFilter(params.config?.providerFilter);
|
||||
const defaultContextWindow = resolveDefaultContextWindow(params.config);
|
||||
const defaultMaxTokens = resolveDefaultMaxTokens(params.config);
|
||||
const cacheKey = buildCacheKey({
|
||||
region: params.region,
|
||||
providerFilter,
|
||||
refreshIntervalSeconds,
|
||||
defaultContextWindow,
|
||||
defaultMaxTokens,
|
||||
});
|
||||
const now = params.now?.() ?? Date.now();
|
||||
|
||||
if (refreshIntervalSeconds > 0) {
|
||||
const cached = discoveryCache.get(cacheKey);
|
||||
if (cached?.value && cached.expiresAt > now) {
|
||||
return cached.value;
|
||||
}
|
||||
if (cached?.inFlight) {
|
||||
return cached.inFlight;
|
||||
}
|
||||
}
|
||||
|
||||
const clientFactory = params.clientFactory ?? ((region: string) => new BedrockClient({ region }));
|
||||
const client = clientFactory(params.region);
|
||||
|
||||
const discoveryPromise = (async () => {
|
||||
const response = await client.send(new ListFoundationModelsCommand({}));
|
||||
const discovered: ModelDefinitionConfig[] = [];
|
||||
for (const summary of response.modelSummaries ?? []) {
|
||||
if (!shouldIncludeSummary(summary, providerFilter)) continue;
|
||||
discovered.push(
|
||||
toModelDefinition(summary, {
|
||||
contextWindow: defaultContextWindow,
|
||||
maxTokens: defaultMaxTokens,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return discovered.sort((a, b) => a.name.localeCompare(b.name));
|
||||
})();
|
||||
|
||||
if (refreshIntervalSeconds > 0) {
|
||||
discoveryCache.set(cacheKey, {
|
||||
expiresAt: now + refreshIntervalSeconds * 1000,
|
||||
inFlight: discoveryPromise,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const value = await discoveryPromise;
|
||||
if (refreshIntervalSeconds > 0) {
|
||||
discoveryCache.set(cacheKey, {
|
||||
expiresAt: now + refreshIntervalSeconds * 1000,
|
||||
value,
|
||||
});
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (refreshIntervalSeconds > 0) {
|
||||
discoveryCache.delete(cacheKey);
|
||||
}
|
||||
if (!hasLoggedBedrockError) {
|
||||
hasLoggedBedrockError = true;
|
||||
console.warn(`[bedrock-discovery] Failed to list models: ${String(error)}`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,6 @@ export function createClawdbotTools(options?: {
|
||||
agentTo?: string;
|
||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||
agentThreadId?: string | number;
|
||||
/** Group id for channel-level tool policy inheritance. */
|
||||
agentGroupId?: string | null;
|
||||
/** Group channel label for channel-level tool policy inheritance. */
|
||||
agentGroupChannel?: string | null;
|
||||
/** Group space label for channel-level tool policy inheritance. */
|
||||
agentGroupSpace?: string | null;
|
||||
agentDir?: string;
|
||||
sandboxRoot?: string;
|
||||
workspaceDir?: string;
|
||||
@@ -120,9 +114,6 @@ export function createClawdbotTools(options?: {
|
||||
agentAccountId: options?.agentAccountId,
|
||||
agentTo: options?.agentTo,
|
||||
agentThreadId: options?.agentThreadId,
|
||||
agentGroupId: options?.agentGroupId,
|
||||
agentGroupChannel: options?.agentGroupChannel,
|
||||
agentGroupSpace: options?.agentGroupSpace,
|
||||
sandboxed: options?.sandboxed,
|
||||
}),
|
||||
createSessionStatusTool({
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
const IDENTITY_PLACEHOLDER_VALUES = new Set([
|
||||
"pick something you like",
|
||||
"ai? robot? familiar? ghost in the machine? something weirder?",
|
||||
"how do you come across? sharp? warm? chaotic? calm?",
|
||||
"your signature - pick one that feels right",
|
||||
"workspace-relative path, http(s) url, or data uri",
|
||||
]);
|
||||
|
||||
function normalizeIdentityValue(value: string): string {
|
||||
let normalized = value.trim();
|
||||
normalized = normalized.replace(/^[*_]+|[*_]+$/g, "").trim();
|
||||
if (normalized.startsWith("(") && normalized.endsWith(")")) {
|
||||
normalized = normalized.slice(1, -1).trim();
|
||||
}
|
||||
normalized = normalized.replace(/[\u2013\u2014]/g, "-");
|
||||
normalized = normalized.replace(/\s+/g, " ").toLowerCase();
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isIdentityPlaceholder(value: string): boolean {
|
||||
const normalized = normalizeIdentityValue(value);
|
||||
return IDENTITY_PLACEHOLDER_VALUES.has(normalized);
|
||||
}
|
||||
|
||||
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
||||
const identity: AgentIdentityFile = {};
|
||||
const lines = content.split(/\r?\n/);
|
||||
@@ -49,7 +25,6 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
||||
.replace(/^[*_]+|[*_]+$/g, "")
|
||||
.trim();
|
||||
if (!value) continue;
|
||||
if (isIdentityPlaceholder(value)) continue;
|
||||
if (label === "name") identity.name = value;
|
||||
if (label === "emoji") identity.emoji = value;
|
||||
if (label === "creature") identity.creature = value;
|
||||
|
||||
@@ -75,12 +75,12 @@ function resolveEnvSourceLabel(params: {
|
||||
return `${prefix}${params.label}`;
|
||||
}
|
||||
|
||||
export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
||||
if (env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
|
||||
if (env[AWS_ACCESS_KEY_ENV]?.trim() && env[AWS_SECRET_KEY_ENV]?.trim()) {
|
||||
export function resolveAwsSdkEnvVarName(): string | undefined {
|
||||
if (process.env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
|
||||
if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) {
|
||||
return AWS_ACCESS_KEY_ENV;
|
||||
}
|
||||
if (env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
|
||||
if (process.env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from "../providers/github-copilot-token.js";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
||||
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
||||
import { discoverBedrockModels } from "./bedrock-discovery.js";
|
||||
import {
|
||||
buildSyntheticModelDefinition,
|
||||
SYNTHETIC_BASE_URL,
|
||||
@@ -376,27 +375,3 @@ export async function resolveImplicitCopilotProvider(params: {
|
||||
models: [],
|
||||
} satisfies ProviderConfig;
|
||||
}
|
||||
|
||||
export async function resolveImplicitBedrockProvider(params: {
|
||||
agentDir: string;
|
||||
config?: ClawdbotConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<ProviderConfig | null> {
|
||||
const env = params.env ?? process.env;
|
||||
const discoveryConfig = params.config?.models?.bedrockDiscovery;
|
||||
const enabled = discoveryConfig?.enabled;
|
||||
const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined;
|
||||
if (enabled === false) return null;
|
||||
if (enabled !== true && !hasAwsCreds) return null;
|
||||
|
||||
const region = discoveryConfig?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
|
||||
const models = await discoverBedrockModels({ region, config: discoveryConfig });
|
||||
if (models.length === 0) return null;
|
||||
|
||||
return {
|
||||
baseUrl: `https://bedrock-runtime.${region}.amazonaws.com`,
|
||||
api: "bedrock-converse-stream",
|
||||
auth: "aws-sdk",
|
||||
models,
|
||||
} satisfies ProviderConfig;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||
import {
|
||||
normalizeProviders,
|
||||
type ProviderConfig,
|
||||
resolveImplicitBedrockProvider,
|
||||
resolveImplicitCopilotProvider,
|
||||
resolveImplicitProviders,
|
||||
} from "./models-config.providers.js";
|
||||
@@ -85,13 +84,6 @@ export async function ensureClawdbotModelsJson(
|
||||
implicit: implicitProviders,
|
||||
explicit: explicitProviders,
|
||||
});
|
||||
const implicitBedrock = await resolveImplicitBedrockProvider({ agentDir, config: cfg });
|
||||
if (implicitBedrock) {
|
||||
const existing = providers["amazon-bedrock"];
|
||||
providers["amazon-bedrock"] = existing
|
||||
? mergeProviderModels(implicitBedrock, existing)
|
||||
: implicitBedrock;
|
||||
}
|
||||
const implicitCopilot = await resolveImplicitCopilotProvider({ agentDir });
|
||||
if (implicitCopilot && !providers["github-copilot"]) {
|
||||
providers["github-copilot"] = implicitCopilot;
|
||||
|
||||
@@ -63,12 +63,12 @@ const makeAttempt = (overrides: Partial<EmbeddedRunAttemptResult>): EmbeddedRunA
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotConfig =>
|
||||
const makeConfig = (): ClawdbotConfig =>
|
||||
({
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
fallbacks: opts?.fallbacks ?? [],
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -76,7 +76,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
|
||||
providers: {
|
||||
openai: {
|
||||
api: "openai-responses",
|
||||
apiKey: opts?.apiKey ?? "sk-test",
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://example.com",
|
||||
models: [
|
||||
{
|
||||
@@ -94,13 +94,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
|
||||
},
|
||||
}) satisfies ClawdbotConfig;
|
||||
|
||||
const writeAuthStore = async (
|
||||
agentDir: string,
|
||||
opts?: {
|
||||
includeAnthropic?: boolean;
|
||||
usageStats?: Record<string, { lastUsed?: number; cooldownUntil?: number }>;
|
||||
},
|
||||
) => {
|
||||
const writeAuthStore = async (agentDir: string, opts?: { includeAnthropic?: boolean }) => {
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const payload = {
|
||||
version: 1,
|
||||
@@ -111,12 +105,10 @@ const writeAuthStore = async (
|
||||
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
|
||||
: {}),
|
||||
},
|
||||
usageStats:
|
||||
opts?.usageStats ??
|
||||
({
|
||||
"openai:p1": { lastUsed: 1 },
|
||||
"openai:p2": { lastUsed: 2 },
|
||||
} as Record<string, { lastUsed?: number }>),
|
||||
usageStats: {
|
||||
"openai:p1": { lastUsed: 1 },
|
||||
"openai:p2": { lastUsed: 2 },
|
||||
},
|
||||
};
|
||||
await fs.writeFile(authPath, JSON.stringify(payload));
|
||||
};
|
||||
@@ -392,92 +384,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("fails over when all profiles are in cooldown and fallbacks are configured", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
try {
|
||||
await writeAuthStore(agentDir, {
|
||||
usageStats: {
|
||||
"openai:p1": { lastUsed: 1, cooldownUntil: now + 60 * 60 * 1000 },
|
||||
"openai:p2": { lastUsed: 2, cooldownUntil: now + 60 * 60 * 1000 },
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
runEmbeddedPiAgent({
|
||||
sessionId: "session:test",
|
||||
sessionKey: "agent:test:cooldown-failover",
|
||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||
workspaceDir,
|
||||
agentDir,
|
||||
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
|
||||
prompt: "hello",
|
||||
provider: "openai",
|
||||
model: "mock-1",
|
||||
authProfileIdSource: "auto",
|
||||
timeoutMs: 5_000,
|
||||
runId: "run:cooldown-failover",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
name: "FailoverError",
|
||||
reason: "rate_limit",
|
||||
provider: "openai",
|
||||
model: "mock-1",
|
||||
});
|
||||
|
||||
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("fails over when auth is unavailable and fallbacks are configured", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
||||
const previousOpenAiKey = process.env.OPENAI_API_KEY;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
try {
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
await fs.writeFile(authPath, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }));
|
||||
|
||||
await expect(
|
||||
runEmbeddedPiAgent({
|
||||
sessionId: "session:test",
|
||||
sessionKey: "agent:test:auth-unavailable",
|
||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||
workspaceDir,
|
||||
agentDir,
|
||||
config: makeConfig({ fallbacks: ["openai/mock-2"], apiKey: "" }),
|
||||
prompt: "hello",
|
||||
provider: "openai",
|
||||
model: "mock-1",
|
||||
authProfileIdSource: "auto",
|
||||
timeoutMs: 5_000,
|
||||
runId: "run:auth-unavailable",
|
||||
}),
|
||||
).rejects.toMatchObject({ name: "FailoverError", reason: "auth" });
|
||||
|
||||
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
if (previousOpenAiKey === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = previousOpenAiKey;
|
||||
}
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("skips profiles in cooldown when rotating after failure", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
||||
@@ -165,7 +165,6 @@ const readSessionMessages = async (sessionFile: string) => {
|
||||
};
|
||||
|
||||
describe("runEmbeddedPiAgent", () => {
|
||||
const itIfNotWin32 = process.platform === "win32" ? it.skip : it;
|
||||
it("writes models.json into the provided agentDir", async () => {
|
||||
const sessionFile = nextSessionFile();
|
||||
|
||||
@@ -211,39 +210,35 @@ describe("runEmbeddedPiAgent", () => {
|
||||
await expect(fs.stat(path.join(agentDir, "models.json"))).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
itIfNotWin32(
|
||||
"persists the first user message before assistant output",
|
||||
{ timeout: 60_000 },
|
||||
async () => {
|
||||
const sessionFile = nextSessionFile();
|
||||
const cfg = makeOpenAiConfig(["mock-1"]);
|
||||
await ensureModels(cfg);
|
||||
it("persists the first user message before assistant output", { timeout: 60_000 }, async () => {
|
||||
const sessionFile = nextSessionFile();
|
||||
const cfg = makeOpenAiConfig(["mock-1"]);
|
||||
await ensureModels(cfg);
|
||||
|
||||
await runEmbeddedPiAgent({
|
||||
sessionId: "session:test",
|
||||
sessionKey: testSessionKey,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
prompt: "hello",
|
||||
provider: "openai",
|
||||
model: "mock-1",
|
||||
timeoutMs: 5_000,
|
||||
agentDir,
|
||||
enqueue: immediateEnqueue,
|
||||
});
|
||||
await runEmbeddedPiAgent({
|
||||
sessionId: "session:test",
|
||||
sessionKey: testSessionKey,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
prompt: "hello",
|
||||
provider: "openai",
|
||||
model: "mock-1",
|
||||
timeoutMs: 5_000,
|
||||
agentDir,
|
||||
enqueue: immediateEnqueue,
|
||||
});
|
||||
|
||||
const messages = await readSessionMessages(sessionFile);
|
||||
const firstUserIndex = messages.findIndex(
|
||||
(message) => message?.role === "user" && textFromContent(message.content) === "hello",
|
||||
);
|
||||
const firstAssistantIndex = messages.findIndex((message) => message?.role === "assistant");
|
||||
expect(firstUserIndex).toBeGreaterThanOrEqual(0);
|
||||
if (firstAssistantIndex !== -1) {
|
||||
expect(firstUserIndex).toBeLessThan(firstAssistantIndex);
|
||||
}
|
||||
},
|
||||
);
|
||||
const messages = await readSessionMessages(sessionFile);
|
||||
const firstUserIndex = messages.findIndex(
|
||||
(message) => message?.role === "user" && textFromContent(message.content) === "hello",
|
||||
);
|
||||
const firstAssistantIndex = messages.findIndex((message) => message?.role === "assistant");
|
||||
expect(firstUserIndex).toBeGreaterThanOrEqual(0);
|
||||
if (firstAssistantIndex !== -1) {
|
||||
expect(firstUserIndex).toBeLessThan(firstAssistantIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it("persists the user message when prompt fails before assistant output", async () => {
|
||||
const sessionFile = nextSessionFile();
|
||||
|
||||
@@ -73,14 +73,6 @@ export async function compactEmbeddedPiSession(params: {
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
/** Group id for channel-level tool policy resolution. */
|
||||
groupId?: string | null;
|
||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||
groupChannel?: string | null;
|
||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
agentDir?: string;
|
||||
@@ -215,10 +207,6 @@ export async function compactEmbeddedPiSession(params: {
|
||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||
agentAccountId: params.agentAccountId,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
config: params.config,
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
isRateLimitAssistantError,
|
||||
isTimeoutErrorMessage,
|
||||
pickFallbackThinkingLevel,
|
||||
type FailoverReason,
|
||||
} from "../pi-embedded-helpers.js";
|
||||
import { normalizeUsage, type UsageLike } from "../usage.js";
|
||||
|
||||
@@ -72,8 +71,6 @@ export async function runEmbeddedPiAgent(
|
||||
const globalLane = resolveGlobalLane(params.lane);
|
||||
const enqueueGlobal =
|
||||
params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
|
||||
const enqueueSession =
|
||||
params.enqueue ?? ((task, opts) => enqueueCommandInLane(sessionLane, task, opts));
|
||||
const channelHint = params.messageChannel ?? params.messageProvider;
|
||||
const resolvedToolResultFormat =
|
||||
params.toolResultFormat ??
|
||||
@@ -84,7 +81,7 @@ export async function runEmbeddedPiAgent(
|
||||
: "markdown");
|
||||
const isProbeSession = params.sessionId?.startsWith("probe-") ?? false;
|
||||
|
||||
return enqueueSession(() =>
|
||||
return enqueueCommandInLane(sessionLane, () =>
|
||||
enqueueGlobal(async () => {
|
||||
const started = Date.now();
|
||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||
@@ -93,8 +90,6 @@ export async function runEmbeddedPiAgent(
|
||||
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
||||
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
||||
const fallbackConfigured =
|
||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||
await ensureClawdbotModelsJson(params.config, agentDir);
|
||||
|
||||
const { model, error, authStorage, modelRegistry } = resolveModel(
|
||||
@@ -168,42 +163,6 @@ export async function runEmbeddedPiAgent(
|
||||
let apiKeyInfo: ApiKeyInfo | null = null;
|
||||
let lastProfileId: string | undefined;
|
||||
|
||||
const resolveAuthProfileFailoverReason = (params: {
|
||||
allInCooldown: boolean;
|
||||
message: string;
|
||||
}): FailoverReason => {
|
||||
if (params.allInCooldown) return "rate_limit";
|
||||
const classified = classifyFailoverReason(params.message);
|
||||
return classified ?? "auth";
|
||||
};
|
||||
|
||||
const throwAuthProfileFailover = (params: {
|
||||
allInCooldown: boolean;
|
||||
message?: string;
|
||||
error?: unknown;
|
||||
}): never => {
|
||||
const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
|
||||
const message =
|
||||
params.message?.trim() ||
|
||||
(params.error ? describeUnknownError(params.error).trim() : "") ||
|
||||
fallbackMessage;
|
||||
const reason = resolveAuthProfileFailoverReason({
|
||||
allInCooldown: params.allInCooldown,
|
||||
message,
|
||||
});
|
||||
if (fallbackConfigured) {
|
||||
throw new FailoverError(message, {
|
||||
reason,
|
||||
provider,
|
||||
model: modelId,
|
||||
status: resolveFailoverStatus(reason),
|
||||
cause: params.error,
|
||||
});
|
||||
}
|
||||
if (params.error instanceof Error) throw params.error;
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
const resolveApiKeyForCandidate = async (candidate?: string) => {
|
||||
return getApiKeyForModel({
|
||||
model,
|
||||
@@ -277,17 +236,14 @@ export async function runEmbeddedPiAgent(
|
||||
break;
|
||||
}
|
||||
if (profileIndex >= profileCandidates.length) {
|
||||
throwAuthProfileFailover({ allInCooldown: true });
|
||||
throw new Error(
|
||||
`No available auth profile for ${provider} (all in cooldown or unavailable).`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof FailoverError) throw err;
|
||||
if (profileCandidates[profileIndex] === lockedProfileId) {
|
||||
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
||||
}
|
||||
if (profileCandidates[profileIndex] === lockedProfileId) throw err;
|
||||
const advanced = await advanceAuthProfile();
|
||||
if (!advanced) {
|
||||
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
||||
}
|
||||
if (!advanced) throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -306,10 +262,6 @@ export async function runEmbeddedPiAgent(
|
||||
agentAccountId: params.agentAccountId,
|
||||
messageTo: params.messageTo,
|
||||
messageThreadId: params.messageThreadId,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
replyToMode: params.replyToMode,
|
||||
@@ -321,7 +273,6 @@ export async function runEmbeddedPiAgent(
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
prompt,
|
||||
images: params.images,
|
||||
disableTools: params.disableTools,
|
||||
provider,
|
||||
modelId,
|
||||
model,
|
||||
@@ -435,7 +386,9 @@ export async function runEmbeddedPiAgent(
|
||||
}
|
||||
// FIX: Throw FailoverError for prompt errors when fallbacks configured
|
||||
// This enables model fallback for quota/rate limit errors during prompt submission
|
||||
if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
|
||||
const promptFallbackConfigured =
|
||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||
if (promptFallbackConfigured && isFailoverErrorMessage(errorText)) {
|
||||
throw new FailoverError(errorText, {
|
||||
reason: promptFailoverReason ?? "unknown",
|
||||
provider,
|
||||
@@ -459,6 +412,8 @@ export async function runEmbeddedPiAgent(
|
||||
continue;
|
||||
}
|
||||
|
||||
const fallbackConfigured =
|
||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||
const authFailure = isAuthAssistantError(lastAssistant);
|
||||
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
||||
const failoverFailure = isFailoverAssistantError(lastAssistant);
|
||||
|
||||
@@ -196,36 +196,30 @@ export async function runEmbeddedAttempt(
|
||||
|
||||
// Check if the model supports native image input
|
||||
const modelHasVision = params.model.input?.includes("image") ?? false;
|
||||
const toolsRaw = params.disableTools
|
||||
? []
|
||||
: createClawdbotCodingTools({
|
||||
exec: {
|
||||
...params.execOverrides,
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||
agentAccountId: params.agentAccountId,
|
||||
messageTo: params.messageTo,
|
||||
messageThreadId: params.messageThreadId,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
config: params.config,
|
||||
abortSignal: runAbortController.signal,
|
||||
modelProvider: params.model.provider,
|
||||
modelId: params.modelId,
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
replyToMode: params.replyToMode,
|
||||
hasRepliedRef: params.hasRepliedRef,
|
||||
modelHasVision,
|
||||
});
|
||||
const toolsRaw = createClawdbotCodingTools({
|
||||
exec: {
|
||||
...params.execOverrides,
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||
agentAccountId: params.agentAccountId,
|
||||
messageTo: params.messageTo,
|
||||
messageThreadId: params.messageThreadId,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
config: params.config,
|
||||
abortSignal: runAbortController.signal,
|
||||
modelProvider: params.model.provider,
|
||||
modelId: params.modelId,
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
replyToMode: params.replyToMode,
|
||||
hasRepliedRef: params.hasRepliedRef,
|
||||
modelHasVision,
|
||||
});
|
||||
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
|
||||
logToolSchemasForGoogle({ tools, provider: params.provider });
|
||||
|
||||
|
||||
@@ -27,14 +27,6 @@ export type RunEmbeddedPiAgentParams = {
|
||||
messageTo?: string;
|
||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||
messageThreadId?: string | number;
|
||||
/** Group id for channel-level tool policy resolution. */
|
||||
groupId?: string | null;
|
||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||
groupChannel?: string | null;
|
||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
/** Current channel ID for auto-threading (Slack). */
|
||||
currentChannelId?: string;
|
||||
/** Current thread timestamp for auto-threading (Slack). */
|
||||
@@ -52,8 +44,6 @@ export type RunEmbeddedPiAgentParams = {
|
||||
images?: ImageContent[];
|
||||
/** Optional client-provided tools (OpenResponses hosted tools). */
|
||||
clientTools?: ClientToolDefinition[];
|
||||
/** Disable built-in tools for this run (LLM-only mode). */
|
||||
disableTools?: boolean;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
authProfileId?: string;
|
||||
|
||||
@@ -23,14 +23,6 @@ export type EmbeddedRunAttemptParams = {
|
||||
agentAccountId?: string;
|
||||
messageTo?: string;
|
||||
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;
|
||||
currentThreadTs?: string;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
@@ -44,8 +36,6 @@ export type EmbeddedRunAttemptParams = {
|
||||
images?: ImageContent[];
|
||||
/** Optional client-provided tools (OpenResponses hosted tools). */
|
||||
clientTools?: ClientToolDefinition[];
|
||||
/** Disable built-in tools for this run (LLM-only mode). */
|
||||
disableTools?: boolean;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
model: Model<Api>;
|
||||
|
||||
@@ -43,7 +43,7 @@ export function abortEmbeddedPiRun(sessionId: string): boolean {
|
||||
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
|
||||
return false;
|
||||
}
|
||||
diag.debug(`aborting run: sessionId=${sessionId}`);
|
||||
diag.info(`aborting run: sessionId=${sessionId}`);
|
||||
handle.abort();
|
||||
return true;
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export function setActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueueH
|
||||
reason: wasActive ? "run_replaced" : "run_started",
|
||||
});
|
||||
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);
|
||||
logSessionStateChange({ sessionId, state: "idle", reason: "run_completed" });
|
||||
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);
|
||||
} else {
|
||||
|
||||
@@ -231,95 +231,6 @@ describe("Agent-specific tool filtering", () => {
|
||||
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", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterToolsByPolicy, isToolAllowedByPolicyName } from "./pi-tools.policy.js";
|
||||
|
||||
function createStubTool(name: string): AgentTool<unknown, unknown> {
|
||||
return {
|
||||
name,
|
||||
label: name,
|
||||
description: "",
|
||||
parameters: {},
|
||||
execute: async () => ({}) as AgentToolResult<unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
describe("pi-tools.policy", () => {
|
||||
it("treats * in allow as allow-all", () => {
|
||||
const tools = [createStubTool("read"), createStubTool("exec")];
|
||||
const filtered = filterToolsByPolicy(tools, { allow: ["*"] });
|
||||
expect(filtered.map((tool) => tool.name)).toEqual(["read", "exec"]);
|
||||
});
|
||||
|
||||
it("treats * in deny as deny-all", () => {
|
||||
const tools = [createStubTool("read"), createStubTool("exec")];
|
||||
const filtered = filterToolsByPolicy(tools, { deny: ["*"] });
|
||||
expect(filtered).toEqual([]);
|
||||
});
|
||||
|
||||
it("supports wildcard allow/deny patterns", () => {
|
||||
expect(isToolAllowedByPolicyName("web_fetch", { allow: ["web_*"] })).toBe(true);
|
||||
expect(isToolAllowedByPolicyName("web_search", { deny: ["web_*"] })).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps apply_patch when exec is allowlisted", () => {
|
||||
expect(isToolAllowedByPolicyName("apply_patch", { allow: ["exec"] })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,58 +1,8 @@
|
||||
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 type { AnyAgentTool } from "./pi-tools.types.js";
|
||||
import type { SandboxToolPolicy } from "./sandbox.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 =
|
||||
| { kind: "all" }
|
||||
| { kind: "exact"; value: string }
|
||||
| { kind: "regex"; value: RegExp };
|
||||
|
||||
function compilePattern(pattern: string): CompiledPattern {
|
||||
const normalized = normalizeToolName(pattern);
|
||||
if (!normalized) return { kind: "exact", value: "" };
|
||||
if (normalized === "*") return { kind: "all" };
|
||||
if (!normalized.includes("*")) return { kind: "exact", value: normalized };
|
||||
const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return {
|
||||
kind: "regex",
|
||||
value: new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`),
|
||||
};
|
||||
}
|
||||
|
||||
function compilePatterns(patterns?: string[]): CompiledPattern[] {
|
||||
if (!Array.isArray(patterns)) return [];
|
||||
return expandToolGroups(patterns)
|
||||
.map(compilePattern)
|
||||
.filter((pattern) => pattern.kind !== "exact" || pattern.value);
|
||||
}
|
||||
|
||||
function matchesAny(name: string, patterns: CompiledPattern[]): boolean {
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.kind === "all") return true;
|
||||
if (pattern.kind === "exact" && name === pattern.value) return true;
|
||||
if (pattern.kind === "regex" && pattern.value.test(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function makeToolPolicyMatcher(policy: SandboxToolPolicy) {
|
||||
const deny = compilePatterns(policy.deny);
|
||||
const allow = compilePatterns(policy.allow);
|
||||
return (name: string) => {
|
||||
const normalized = normalizeToolName(name);
|
||||
if (matchesAny(normalized, deny)) return false;
|
||||
if (allow.length === 0) return true;
|
||||
if (matchesAny(normalized, allow)) return true;
|
||||
if (normalized === "apply_patch" && matchesAny("exec", allow)) return true;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||
// Session management - main agent orchestrates
|
||||
@@ -85,13 +35,22 @@ export function resolveSubagentToolPolicy(cfg?: ClawdbotConfig): SandboxToolPoli
|
||||
|
||||
export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean {
|
||||
if (!policy) return true;
|
||||
return makeToolPolicyMatcher(policy)(name);
|
||||
const deny = new Set(expandToolGroups(policy.deny));
|
||||
const allowRaw = expandToolGroups(policy.allow);
|
||||
const allow = allowRaw.length > 0 ? new Set(allowRaw) : null;
|
||||
const normalized = normalizeToolName(name);
|
||||
if (deny.has(normalized)) return false;
|
||||
if (allow) {
|
||||
if (allow.has(normalized)) return true;
|
||||
if (normalized === "apply_patch" && allow.has("exec")) return true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) {
|
||||
if (!policy) return tools;
|
||||
const matcher = makeToolPolicyMatcher(policy);
|
||||
return tools.filter((tool) => matcher(tool.name));
|
||||
return tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy));
|
||||
}
|
||||
|
||||
type ToolPolicyConfig = {
|
||||
@@ -112,26 +71,6 @@ function normalizeProviderKey(value: string): string {
|
||||
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: {
|
||||
byProvider?: Record<string, ToolPolicyConfig>;
|
||||
modelProvider?: string;
|
||||
@@ -198,47 +137,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(
|
||||
name: string,
|
||||
policies: Array<SandboxToolPolicy | undefined>,
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
filterToolsByPolicy,
|
||||
isToolAllowedByPolicies,
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveSubagentToolPolicy,
|
||||
} from "./pi-tools.policy.js";
|
||||
import {
|
||||
@@ -129,14 +128,6 @@ export function createClawdbotCodingTools(options?: {
|
||||
currentChannelId?: string;
|
||||
/** Current thread timestamp for auto-threading (Slack). */
|
||||
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. */
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||
@@ -160,16 +151,6 @@ export function createClawdbotCodingTools(options?: {
|
||||
modelProvider: options?.modelProvider,
|
||||
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 providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
@@ -184,7 +165,6 @@ export function createClawdbotCodingTools(options?: {
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
sandbox?.tools,
|
||||
subagentPolicy,
|
||||
]);
|
||||
@@ -293,9 +273,6 @@ export function createClawdbotCodingTools(options?: {
|
||||
agentAccountId: options?.agentAccountId,
|
||||
agentTo: options?.messageTo,
|
||||
agentThreadId: options?.messageThreadId,
|
||||
agentGroupId: options?.groupId ?? null,
|
||||
agentGroupChannel: options?.groupChannel ?? null,
|
||||
agentGroupSpace: options?.groupSpace ?? null,
|
||||
agentDir: options?.agentDir,
|
||||
sandboxRoot,
|
||||
workspaceDir: options?.workspaceDir,
|
||||
@@ -308,7 +285,6 @@ export function createClawdbotCodingTools(options?: {
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
sandbox?.tools,
|
||||
subagentPolicy,
|
||||
]),
|
||||
@@ -347,10 +323,6 @@ export function createClawdbotCodingTools(options?: {
|
||||
stripPluginOnlyAllowlist(agentProviderPolicy, pluginGroups),
|
||||
pluginGroups,
|
||||
);
|
||||
const groupPolicyExpanded = expandPolicyWithPluginGroups(
|
||||
stripPluginOnlyAllowlist(groupPolicy, pluginGroups),
|
||||
pluginGroups,
|
||||
);
|
||||
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
||||
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
||||
|
||||
@@ -372,12 +344,9 @@ export function createClawdbotCodingTools(options?: {
|
||||
const agentProviderFiltered = agentProviderExpanded
|
||||
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
||||
: agentFiltered;
|
||||
const groupFiltered = groupPolicyExpanded
|
||||
? filterToolsByPolicy(agentProviderFiltered, groupPolicyExpanded)
|
||||
: agentProviderFiltered;
|
||||
const sandboxed = sandboxPolicyExpanded
|
||||
? filterToolsByPolicy(groupFiltered, sandboxPolicyExpanded)
|
||||
: groupFiltered;
|
||||
? filterToolsByPolicy(agentProviderFiltered, sandboxPolicyExpanded)
|
||||
: agentProviderFiltered;
|
||||
const subagentFiltered = subagentPolicyExpanded
|
||||
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
||||
: sandboxed;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SandboxToolPolicy } from "./types.js";
|
||||
import { isToolAllowed } from "./tool-policy.js";
|
||||
|
||||
describe("sandbox tool policy", () => {
|
||||
it("allows all tools with * allow", () => {
|
||||
const policy: SandboxToolPolicy = { allow: ["*"], deny: [] };
|
||||
expect(isToolAllowed(policy, "browser")).toBe(true);
|
||||
});
|
||||
|
||||
it("denies all tools with * deny", () => {
|
||||
const policy: SandboxToolPolicy = { allow: [], deny: ["*"] };
|
||||
expect(isToolAllowed(policy, "read")).toBe(false);
|
||||
});
|
||||
|
||||
it("supports wildcard patterns", () => {
|
||||
const policy: SandboxToolPolicy = { allow: ["web_*"] };
|
||||
expect(isToolAllowed(policy, "web_fetch")).toBe(true);
|
||||
expect(isToolAllowed(policy, "read")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8,46 +8,12 @@ import type {
|
||||
SandboxToolPolicySource,
|
||||
} from "./types.js";
|
||||
|
||||
type CompiledPattern =
|
||||
| { kind: "all" }
|
||||
| { kind: "exact"; value: string }
|
||||
| { kind: "regex"; value: RegExp };
|
||||
|
||||
function compilePattern(pattern: string): CompiledPattern {
|
||||
const normalized = pattern.trim().toLowerCase();
|
||||
if (!normalized) return { kind: "exact", value: "" };
|
||||
if (normalized === "*") return { kind: "all" };
|
||||
if (!normalized.includes("*")) return { kind: "exact", value: normalized };
|
||||
const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return {
|
||||
kind: "regex",
|
||||
value: new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`),
|
||||
};
|
||||
}
|
||||
|
||||
function compilePatterns(patterns?: string[]): CompiledPattern[] {
|
||||
if (!Array.isArray(patterns)) return [];
|
||||
return expandToolGroups(patterns)
|
||||
.map(compilePattern)
|
||||
.filter((pattern) => pattern.kind !== "exact" || pattern.value);
|
||||
}
|
||||
|
||||
function matchesAny(name: string, patterns: CompiledPattern[]): boolean {
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.kind === "all") return true;
|
||||
if (pattern.kind === "exact" && name === pattern.value) return true;
|
||||
if (pattern.kind === "regex" && pattern.value.test(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isToolAllowed(policy: SandboxToolPolicy, name: string) {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
const deny = compilePatterns(policy.deny);
|
||||
if (matchesAny(normalized, deny)) return false;
|
||||
const allow = compilePatterns(policy.allow);
|
||||
const deny = new Set(expandToolGroups(policy.deny));
|
||||
if (deny.has(name.toLowerCase())) return false;
|
||||
const allow = expandToolGroups(policy.allow);
|
||||
if (allow.length === 0) return true;
|
||||
return matchesAny(normalized, allow);
|
||||
return allow.includes(name.toLowerCase());
|
||||
}
|
||||
|
||||
export function resolveSandboxToolPolicyForAgent(
|
||||
|
||||
@@ -35,7 +35,7 @@ const BROWSER_TOOL_ACTIONS = [
|
||||
"act",
|
||||
] 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_MODES = ["efficient"] as const;
|
||||
@@ -84,7 +84,6 @@ const BrowserActSchema = Type.Object({
|
||||
export const BrowserToolSchema = Type.Object({
|
||||
action: stringEnum(BROWSER_TOOL_ACTIONS),
|
||||
target: optionalStringEnum(BROWSER_TARGETS),
|
||||
node: Type.Optional(Type.String()),
|
||||
profile: Type.Optional(Type.String()),
|
||||
controlUrl: Type.Optional(Type.String()),
|
||||
targetUrl: Type.Optional(Type.String()),
|
||||
|
||||
@@ -49,25 +49,6 @@ const browserConfigMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
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(() => ({
|
||||
loadConfig: vi.fn(() => ({ browser: {} })),
|
||||
}));
|
||||
@@ -91,7 +72,6 @@ describe("browser tool snapshot maxChars", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
configMocks.loadConfig.mockReturnValue({ browser: {} });
|
||||
nodesUtilsMocks.listNodes.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -18,173 +18,11 @@ import {
|
||||
browserPdfSave,
|
||||
browserScreenshotAction,
|
||||
} from "../../browser/client-actions.js";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { resolveBrowserConfig } from "../../browser/config.js";
|
||||
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.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 { 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: {
|
||||
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).",
|
||||
'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).',
|
||||
'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.",
|
||||
"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.',
|
||||
"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).",
|
||||
hostHint,
|
||||
allowlistHint,
|
||||
@@ -305,184 +142,49 @@ export function createBrowserTool(opts?: {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const controlUrl = readStringParam(params, "controlUrl");
|
||||
const profile = readStringParam(params, "profile");
|
||||
const requestedNode = readStringParam(params, "node");
|
||||
let target = readStringParam(params, "target") as
|
||||
| "sandbox"
|
||||
| "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.
|
||||
let target = readStringParam(params, "target") as "sandbox" | "host" | "custom" | undefined;
|
||||
if (profile === "chrome" && !target && !controlUrl?.trim()) {
|
||||
// Chrome extension relay takeover is a host Chrome feature; default to host even in sandboxed sessions.
|
||||
target = "host";
|
||||
}
|
||||
|
||||
const nodeTarget = await resolveBrowserNodeTarget({
|
||||
requestedNode: requestedNode ?? undefined,
|
||||
const baseUrl = resolveBrowserBaseUrl({
|
||||
target,
|
||||
controlUrl,
|
||||
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) {
|
||||
case "status":
|
||||
if (proxyRequest) {
|
||||
return jsonResult(
|
||||
await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/",
|
||||
profile,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||
case "start":
|
||||
if (proxyRequest) {
|
||||
await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/start",
|
||||
profile,
|
||||
});
|
||||
return jsonResult(
|
||||
await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/",
|
||||
profile,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await browserStart(baseUrl, { profile });
|
||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||
case "stop":
|
||||
if (proxyRequest) {
|
||||
await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/stop",
|
||||
profile,
|
||||
});
|
||||
return jsonResult(
|
||||
await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/",
|
||||
profile,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await browserStop(baseUrl, { profile });
|
||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||
case "profiles":
|
||||
if (proxyRequest) {
|
||||
const result = await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/profiles",
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
return jsonResult({ profiles: await browserProfiles(baseUrl) });
|
||||
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 }) });
|
||||
case "open": {
|
||||
const targetUrl = readStringParam(params, "targetUrl", {
|
||||
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 }));
|
||||
}
|
||||
case "focus": {
|
||||
const targetId = readStringParam(params, "targetId", {
|
||||
required: true,
|
||||
});
|
||||
if (proxyRequest) {
|
||||
const result = await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/tabs/focus",
|
||||
profile,
|
||||
body: { targetId },
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
await browserFocusTab(baseUrl, targetId, { profile });
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "close": {
|
||||
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 });
|
||||
else await browserAct(baseUrl, { kind: "close" }, { profile });
|
||||
return jsonResult({ ok: true });
|
||||
@@ -530,41 +232,21 @@ export function createBrowserTool(opts?: {
|
||||
: undefined;
|
||||
const selector = typeof params.selector === "string" ? params.selector.trim() : undefined;
|
||||
const frame = typeof params.frame === "string" ? params.frame.trim() : undefined;
|
||||
const snapshot = proxyRequest
|
||||
? ((await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/snapshot",
|
||||
profile,
|
||||
query: {
|
||||
format,
|
||||
targetId,
|
||||
limit,
|
||||
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
||||
refs,
|
||||
interactive,
|
||||
compact,
|
||||
depth,
|
||||
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,
|
||||
});
|
||||
const snapshot = 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 (labels && snapshot.imagePath) {
|
||||
return await imageResultFromFile({
|
||||
@@ -587,27 +269,14 @@ export function createBrowserTool(opts?: {
|
||||
const ref = readStringParam(params, "ref");
|
||||
const element = readStringParam(params, "element");
|
||||
const type = params.type === "jpeg" ? "jpeg" : "png";
|
||||
const result = proxyRequest
|
||||
? ((await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/screenshot",
|
||||
profile,
|
||||
body: {
|
||||
targetId,
|
||||
fullPage,
|
||||
ref,
|
||||
element,
|
||||
type,
|
||||
},
|
||||
})) as Awaited<ReturnType<typeof browserScreenshotAction>>)
|
||||
: await browserScreenshotAction(baseUrl, {
|
||||
targetId,
|
||||
fullPage,
|
||||
ref,
|
||||
element,
|
||||
type,
|
||||
profile,
|
||||
});
|
||||
const result = await browserScreenshotAction(baseUrl, {
|
||||
targetId,
|
||||
fullPage,
|
||||
ref,
|
||||
element,
|
||||
type,
|
||||
profile,
|
||||
});
|
||||
return await imageResultFromFile({
|
||||
label: "browser:screenshot",
|
||||
path: result.path,
|
||||
@@ -619,18 +288,6 @@ export function createBrowserTool(opts?: {
|
||||
required: true,
|
||||
});
|
||||
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(
|
||||
await browserNavigate(baseUrl, {
|
||||
url: targetUrl,
|
||||
@@ -642,30 +299,11 @@ export function createBrowserTool(opts?: {
|
||||
case "console": {
|
||||
const level = typeof params.level === "string" ? params.level.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 }));
|
||||
}
|
||||
case "pdf": {
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
const result = proxyRequest
|
||||
? ((await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/pdf",
|
||||
profile,
|
||||
body: { targetId },
|
||||
})) as Awaited<ReturnType<typeof browserPdfSave>>)
|
||||
: await browserPdfSave(baseUrl, { targetId, profile });
|
||||
const result = await browserPdfSave(baseUrl, { targetId, profile });
|
||||
return {
|
||||
content: [{ type: "text", text: `FILE:${result.path}` }],
|
||||
details: result,
|
||||
@@ -682,22 +320,6 @@ export function createBrowserTool(opts?: {
|
||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||
? params.timeoutMs
|
||||
: 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(
|
||||
await browserArmFileChooser(baseUrl, {
|
||||
paths,
|
||||
@@ -718,20 +340,6 @@ export function createBrowserTool(opts?: {
|
||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||
? params.timeoutMs
|
||||
: undefined;
|
||||
if (proxyRequest) {
|
||||
const result = await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/hooks/dialog",
|
||||
profile,
|
||||
body: {
|
||||
accept,
|
||||
promptText,
|
||||
targetId,
|
||||
timeoutMs,
|
||||
},
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
return jsonResult(
|
||||
await browserArmDialog(baseUrl, {
|
||||
accept,
|
||||
@@ -748,29 +356,14 @@ export function createBrowserTool(opts?: {
|
||||
throw new Error("request required");
|
||||
}
|
||||
try {
|
||||
const result = proxyRequest
|
||||
? await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/act",
|
||||
profile,
|
||||
body: request,
|
||||
})
|
||||
: await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
|
||||
profile,
|
||||
});
|
||||
const result = await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
|
||||
profile,
|
||||
});
|
||||
return jsonResult(result);
|
||||
} catch (err) {
|
||||
const msg = String(err);
|
||||
if (msg.includes("404:") && msg.includes("tab not found") && profile === "chrome") {
|
||||
const tabs = proxyRequest
|
||||
? ((
|
||||
(await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/tabs",
|
||||
profile,
|
||||
})) as { tabs?: unknown[] }
|
||||
).tabs ?? [])
|
||||
: await browserTabs(baseUrl, { profile }).catch(() => []);
|
||||
const tabs = await browserTabs(baseUrl, { profile }).catch(() => []);
|
||||
if (!tabs.length) {
|
||||
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.",
|
||||
|
||||
@@ -63,9 +63,6 @@ export function createSessionsSpawnTool(opts?: {
|
||||
agentAccountId?: string;
|
||||
agentTo?: string;
|
||||
agentThreadId?: string | number;
|
||||
agentGroupId?: string | null;
|
||||
agentGroupChannel?: string | null;
|
||||
agentGroupSpace?: string | null;
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
@@ -156,7 +153,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
}
|
||||
}
|
||||
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
||||
const spawnedByKey = requesterInternalKey;
|
||||
const shouldPatchSpawnedBy = opts?.sandboxed === true;
|
||||
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
||||
const resolvedModel =
|
||||
normalizeModelSelection(modelOverride) ??
|
||||
@@ -222,10 +219,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
thinking: thinkingOverride,
|
||||
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
||||
label: label || undefined,
|
||||
spawnedBy: spawnedByKey,
|
||||
groupId: opts?.agentGroupId ?? undefined,
|
||||
groupChannel: opts?.agentGroupChannel ?? undefined,
|
||||
groupSpace: opts?.agentGroupSpace ?? undefined,
|
||||
spawnedBy: shouldPatchSpawnedBy ? requesterInternalKey : undefined,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
})) as { runId?: string };
|
||||
|
||||
@@ -81,14 +81,6 @@ export async function extractReadableContent(params: {
|
||||
url: string;
|
||||
extractMode: ExtractMode;
|
||||
}): Promise<{ text: string; title?: string } | null> {
|
||||
const fallback = (): { text: string; title?: string } => {
|
||||
const rendered = htmlToMarkdown(params.html);
|
||||
if (params.extractMode === "text") {
|
||||
const text = markdownToText(rendered.text) || normalizeWhitespace(stripTags(params.html));
|
||||
return { text, title: rendered.title };
|
||||
}
|
||||
return rendered;
|
||||
};
|
||||
try {
|
||||
const [{ Readability }, { parseHTML }] = await Promise.all([
|
||||
import("@mozilla/readability"),
|
||||
@@ -102,15 +94,15 @@ export async function extractReadableContent(params: {
|
||||
}
|
||||
const reader = new Readability(document, { charThreshold: 0 });
|
||||
const parsed = reader.parse();
|
||||
if (!parsed?.content) return fallback();
|
||||
if (!parsed?.content) return null;
|
||||
const title = parsed.title || undefined;
|
||||
if (params.extractMode === "text") {
|
||||
const text = normalizeWhitespace(parsed.textContent ?? "");
|
||||
return text ? { text, title } : fallback();
|
||||
return { text, title };
|
||||
}
|
||||
const rendered = htmlToMarkdown(parsed.content);
|
||||
return { text: rendered.text, title: title ?? rendered.title };
|
||||
} catch {
|
||||
return fallback();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
listChatCommandsForConfig,
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
normalizeNativeCommandSpecsForSurface,
|
||||
normalizeCommandBody,
|
||||
parseCommandArgs,
|
||||
resolveCommandArgMenu,
|
||||
@@ -16,18 +15,15 @@ import {
|
||||
shouldHandleTextCommands,
|
||||
} from "./commands-registry.js";
|
||||
import type { ChatCommandDefinition } from "./commands-registry.types.js";
|
||||
import { clearPluginCommands, registerPluginCommand } from "../plugins/commands.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
clearPluginCommands();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
clearPluginCommands();
|
||||
});
|
||||
|
||||
describe("commands registry", () => {
|
||||
@@ -46,20 +42,6 @@ describe("commands registry", () => {
|
||||
expect(specs.find((spec) => spec.name === "compact")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("normalizes telegram native command specs", () => {
|
||||
const specs = [
|
||||
{ name: "OK", description: "Ok", acceptsArgs: false },
|
||||
{ name: "bad-name", description: "Bad", acceptsArgs: false },
|
||||
{ name: "fine_name", description: "Fine", acceptsArgs: false },
|
||||
{ name: "ok", description: "Dup", acceptsArgs: false },
|
||||
];
|
||||
const normalized = normalizeNativeCommandSpecsForSurface({
|
||||
surface: "telegram",
|
||||
specs,
|
||||
});
|
||||
expect(normalized.map((spec) => spec.name)).toEqual(["ok", "fine_name"]);
|
||||
});
|
||||
|
||||
it("filters commands based on config flags", () => {
|
||||
const disabled = listChatCommandsForConfig({
|
||||
commands: { config: false, debug: false },
|
||||
@@ -103,19 +85,6 @@ describe("commands registry", () => {
|
||||
expect(native.find((spec) => spec.name === "demo_skill")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("includes plugin commands in native specs", () => {
|
||||
registerPluginCommand("plugin-core", {
|
||||
name: "plugstatus",
|
||||
description: "Plugin status",
|
||||
handler: () => ({ text: "ok" }),
|
||||
});
|
||||
const native = listNativeCommandSpecsForConfig(
|
||||
{ commands: { config: false, debug: false, native: true } },
|
||||
{ skillCommands: [] },
|
||||
);
|
||||
expect(native.find((spec) => spec.name === "plugstatus")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("detects known text commands", () => {
|
||||
const detection = getCommandDetection();
|
||||
expect(detection.exact.has("/commands")).toBe(true);
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||
import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js";
|
||||
import { getPluginCommandSpecs } from "../plugins/commands.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import {
|
||||
normalizeTelegramCommandName,
|
||||
TELEGRAM_COMMAND_NAME_PATTERN,
|
||||
} from "../config/telegram-custom-commands.js";
|
||||
import type {
|
||||
ChatCommandDefinition,
|
||||
CommandArgChoiceContext,
|
||||
@@ -113,7 +108,7 @@ export function listChatCommandsForConfig(
|
||||
export function listNativeCommandSpecs(params?: {
|
||||
skillCommands?: SkillCommandSpec[];
|
||||
}): NativeCommandSpec[] {
|
||||
const base = listChatCommands({ skillCommands: params?.skillCommands })
|
||||
return listChatCommands({ skillCommands: params?.skillCommands })
|
||||
.filter((command) => command.scope !== "text" && command.nativeName)
|
||||
.map((command) => ({
|
||||
name: command.nativeName ?? command.key,
|
||||
@@ -121,18 +116,13 @@ export function listNativeCommandSpecs(params?: {
|
||||
acceptsArgs: Boolean(command.acceptsArgs),
|
||||
args: command.args,
|
||||
}));
|
||||
const pluginSpecs = getPluginCommandSpecs();
|
||||
if (pluginSpecs.length === 0) return base;
|
||||
const seen = new Set(base.map((spec) => spec.name.toLowerCase()));
|
||||
const extras = pluginSpecs.filter((spec) => !seen.has(spec.name.toLowerCase()));
|
||||
return extras.length > 0 ? [...base, ...extras] : base;
|
||||
}
|
||||
|
||||
export function listNativeCommandSpecsForConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
params?: { skillCommands?: SkillCommandSpec[] },
|
||||
): NativeCommandSpec[] {
|
||||
const base = listChatCommandsForConfig(cfg, params)
|
||||
return listChatCommandsForConfig(cfg, params)
|
||||
.filter((command) => command.scope !== "text" && command.nativeName)
|
||||
.map((command) => ({
|
||||
name: command.nativeName ?? command.key,
|
||||
@@ -140,42 +130,6 @@ export function listNativeCommandSpecsForConfig(
|
||||
acceptsArgs: Boolean(command.acceptsArgs),
|
||||
args: command.args,
|
||||
}));
|
||||
const pluginSpecs = getPluginCommandSpecs();
|
||||
if (pluginSpecs.length === 0) return base;
|
||||
const seen = new Set(base.map((spec) => spec.name.toLowerCase()));
|
||||
const extras = pluginSpecs.filter((spec) => !seen.has(spec.name.toLowerCase()));
|
||||
return extras.length > 0 ? [...base, ...extras] : base;
|
||||
}
|
||||
|
||||
function normalizeNativeCommandNameForSurface(name: string, surface: string): string | null {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return null;
|
||||
if (surface === "telegram") {
|
||||
const normalized = normalizeTelegramCommandName(trimmed);
|
||||
if (!normalized) return null;
|
||||
if (!TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) return null;
|
||||
return normalized;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function normalizeNativeCommandSpecsForSurface(params: {
|
||||
surface: string;
|
||||
specs: NativeCommandSpec[];
|
||||
}): NativeCommandSpec[] {
|
||||
const surface = params.surface.toLowerCase();
|
||||
if (!surface) return params.specs;
|
||||
const normalized: NativeCommandSpec[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const spec of params.specs) {
|
||||
const normalizedName = normalizeNativeCommandNameForSurface(spec.name, surface);
|
||||
if (!normalizedName) continue;
|
||||
const key = normalizedName.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
normalized.push(normalizedName === spec.name ? spec : { ...spec, name: normalizedName });
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function findCommandByNativeName(name: string): ChatCommandDefinition | undefined {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
isHeartbeatContentEffectivelyEmpty,
|
||||
stripHeartbeatToken,
|
||||
} from "./heartbeat.js";
|
||||
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "./heartbeat.js";
|
||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||
|
||||
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_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 {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
return trimmed || HEARTBEAT_PROMPT;
|
||||
|
||||
@@ -97,7 +97,7 @@ afterEach(() => {
|
||||
describe("trigger handling", () => {
|
||||
it("includes the error cause when the embedded agent throws", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("sandbox is not defined."));
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("sandbox is not defined"));
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
@@ -111,7 +111,7 @@ describe("trigger handling", () => {
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe(
|
||||
"⚠️ Agent failed before reply: sandbox is not defined.\nLogs: clawdbot logs --follow",
|
||||
"⚠️ Agent failed before reply: sandbox is not defined. Check gateway logs for details.",
|
||||
);
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "../../agents/pi-embedded-helpers.js";
|
||||
import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
resolveGroupSessionKey,
|
||||
resolveSessionTranscriptPath,
|
||||
type SessionEntry,
|
||||
updateSessionStore,
|
||||
@@ -215,10 +214,6 @@ export async function runAgentTurnWithFallback(params: {
|
||||
agentAccountId: params.sessionCtx.AccountId,
|
||||
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
|
||||
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
|
||||
...buildThreadingToolContext({
|
||||
sessionCtx: params.sessionCtx,
|
||||
@@ -512,17 +507,14 @@ export async function runAgentTurnWithFallback(params: {
|
||||
}
|
||||
|
||||
defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
|
||||
const trimmedMessage = message.replace(/\.\s*$/, "");
|
||||
const fallbackText = isContextOverflow
|
||||
? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
|
||||
: isRoleOrderingError
|
||||
? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."
|
||||
: `⚠️ Agent failed before reply: ${trimmedMessage}.\nLogs: clawdbot logs --follow`;
|
||||
|
||||
return {
|
||||
kind: "final",
|
||||
payload: {
|
||||
text: fallbackText,
|
||||
text: isContextOverflow
|
||||
? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
|
||||
: isRoleOrderingError
|
||||
? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."
|
||||
: `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,10 +67,6 @@ export const handleCompactCommand: CommandHandler = async (params) => {
|
||||
sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
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),
|
||||
workspaceDir: params.workspaceDir,
|
||||
config: params.cfg,
|
||||
|
||||
@@ -81,10 +81,6 @@ async function resolveContextReport(
|
||||
workspaceDir,
|
||||
sessionKey: params.sessionKey,
|
||||
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,
|
||||
modelId: params.model,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
handleStopCommand,
|
||||
handleUsageCommand,
|
||||
} from "./commands-session.js";
|
||||
import { handlePluginCommand } from "./commands-plugin.js";
|
||||
import type {
|
||||
CommandHandler,
|
||||
CommandHandlerResult,
|
||||
@@ -32,8 +31,6 @@ import type {
|
||||
} from "./commands-types.js";
|
||||
|
||||
const HANDLERS: CommandHandler[] = [
|
||||
// Plugin commands are processed first, before built-in commands
|
||||
handlePluginCommand,
|
||||
handleBashCommand,
|
||||
handleActivationCommand,
|
||||
handleSendPolicyCommand,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { clearPluginCommands, registerPluginCommand } from "../../plugins/commands.js";
|
||||
import type { HandleCommandsParams } from "./commands-types.js";
|
||||
import { handlePluginCommand } from "./commands-plugin.js";
|
||||
|
||||
describe("handlePluginCommand", () => {
|
||||
beforeEach(() => {
|
||||
clearPluginCommands();
|
||||
});
|
||||
|
||||
it("skips plugin commands when text commands are disabled", async () => {
|
||||
registerPluginCommand("plugin-core", {
|
||||
name: "ping",
|
||||
description: "Ping",
|
||||
handler: () => ({ text: "pong" }),
|
||||
});
|
||||
|
||||
const params = {
|
||||
command: {
|
||||
commandBodyNormalized: "/ping",
|
||||
senderId: "user-1",
|
||||
channel: "test",
|
||||
isAuthorizedSender: true,
|
||||
},
|
||||
cfg: {} as ClawdbotConfig,
|
||||
} as HandleCommandsParams;
|
||||
|
||||
const result = await handlePluginCommand(params, false);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("executes plugin commands when text commands are enabled", async () => {
|
||||
registerPluginCommand("plugin-core", {
|
||||
name: "ping",
|
||||
description: "Ping",
|
||||
handler: () => ({ text: "pong" }),
|
||||
});
|
||||
|
||||
const params = {
|
||||
command: {
|
||||
commandBodyNormalized: "/ping",
|
||||
senderId: "user-1",
|
||||
channel: "test",
|
||||
isAuthorizedSender: true,
|
||||
},
|
||||
cfg: {} as ClawdbotConfig,
|
||||
} as HandleCommandsParams;
|
||||
|
||||
const result = await handlePluginCommand(params, true);
|
||||
expect(result?.reply?.text).toBe("pong");
|
||||
});
|
||||
});
|
||||
@@ -1,42 +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> => {
|
||||
if (!allowTextCommands) return 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,
|
||||
messageTo: queued.originatingTo,
|
||||
messageThreadId: queued.originatingThreadId,
|
||||
groupId: queued.run.groupId,
|
||||
groupChannel: queued.run.groupChannel,
|
||||
groupSpace: queued.run.groupSpace,
|
||||
sessionFile: queued.run.sessionFile,
|
||||
workspaceDir: queued.run.workspaceDir,
|
||||
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 { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
resolveGroupSessionKey,
|
||||
resolveSessionFilePath,
|
||||
type SessionEntry,
|
||||
updateSessionStore,
|
||||
@@ -367,9 +366,6 @@ export async function runPreparedReply(
|
||||
sessionKey,
|
||||
messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
agentAccountId: sessionCtx.AccountId,
|
||||
groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined,
|
||||
groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(),
|
||||
groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
|
||||
@@ -48,9 +48,6 @@ export type FollowupRun = {
|
||||
sessionKey?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
groupId?: string;
|
||||
groupChannel?: string;
|
||||
groupSpace?: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config: ClawdbotConfig;
|
||||
|
||||
@@ -4,11 +4,9 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { clearPluginCommands, registerPluginCommand } from "../plugins/commands.js";
|
||||
import { buildCommandsMessage, buildHelpMessage, buildStatusMessage } from "./status.js";
|
||||
|
||||
afterEach(() => {
|
||||
clearPluginCommands();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -425,19 +423,6 @@ describe("buildCommandsMessage", () => {
|
||||
);
|
||||
expect(text).toContain("/demo_skill - Demo skill");
|
||||
});
|
||||
|
||||
it("includes plugin commands when registered", () => {
|
||||
registerPluginCommand("plugin-core", {
|
||||
name: "plugstatus",
|
||||
description: "Plugin status",
|
||||
handler: () => ({ text: "ok" }),
|
||||
});
|
||||
const text = buildCommandsMessage({
|
||||
commands: { config: false, debug: false },
|
||||
} as ClawdbotConfig);
|
||||
expect(text).toContain("🔌 Plugin commands");
|
||||
expect(text).toContain("/plugstatus - Plugin status");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildHelpMessage", () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user