Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4b5b74854 | ||
|
|
9c9e2ee6be | ||
|
|
8ea8801d06 | ||
|
|
c97bf23a4a | ||
|
|
3fff943ba1 | ||
|
|
90685ef814 | ||
|
|
a8f2ac5411 | ||
|
|
dea96a2c3d | ||
|
|
90ae2f541c | ||
|
|
d9a467fe3b | ||
|
|
aef88cd9f1 | ||
|
|
104d977d12 | ||
|
|
4b24753be7 | ||
|
|
df09e583aa | ||
|
|
46e6546bb9 | ||
|
|
5428c97685 | ||
|
|
202d7af855 | ||
|
|
72020b37c3 | ||
|
|
b051621bd4 | ||
|
|
ff52aec38e | ||
|
|
15620b1092 | ||
|
|
ad7fc4964a | ||
|
|
8f4426052c | ||
|
|
6a60d47c53 | ||
|
|
b1482957f5 | ||
|
|
4d2e9e8113 | ||
|
|
72d62a54c6 | ||
|
|
ae48066d28 | ||
|
|
f56f799990 | ||
|
|
7e498ab94a | ||
|
|
6bd6ae41b1 | ||
|
|
f648aae440 | ||
|
|
b56587f26e | ||
|
|
4ee808dbcb | ||
|
|
66eec295b8 | ||
|
|
675019cb6f | ||
|
|
795b592286 | ||
|
|
9d98e55ed5 | ||
|
|
c07949a99c | ||
|
|
e51bf46abe | ||
|
|
eba0625a70 | ||
|
|
886752217d | ||
|
|
5662a9cdfc | ||
|
|
fd23b9b209 | ||
|
|
975f5a5284 | ||
|
|
63176ccb8a | ||
|
|
6c3a9fc092 | ||
|
|
d9f173a03d | ||
|
|
c3cb26f7ca | ||
|
|
dd06028827 | ||
|
|
71203829d8 | ||
|
|
dfa80e1e5d | ||
|
|
951a4ea065 | ||
|
|
4fa1517e6d | ||
|
|
de2d986008 | ||
|
|
d57cb2e1a8 | ||
|
|
b697374ce5 | ||
|
|
b9106ba5f9 | ||
|
|
3ba9821254 | ||
|
|
17f2a990a8 | ||
|
|
71f7bd1cfd | ||
|
|
c4c01089ab | ||
|
|
b6591c3f69 | ||
|
|
e6fdbae79b | ||
|
|
a4e57d3ac4 | ||
|
|
1d862cf5c2 | ||
|
|
0840029982 | ||
|
|
309fcc5321 | ||
|
|
00ae21bed2 | ||
|
|
00fd57b8f5 | ||
|
|
aabe0bed30 | ||
|
|
350131b4d7 | ||
|
|
95d45c0aa7 | ||
|
|
cb06e133ca | ||
|
|
4e77483051 | ||
|
|
81535d512a | ||
|
|
8effb557d5 | ||
|
|
c66b1fd18b | ||
|
|
c04f8ba1ea | ||
|
|
c1b7f6b6ba | ||
|
|
e4708b3b99 | ||
|
|
f938f6617b | ||
|
|
e882f7d207 | ||
|
|
e38fd8603f | ||
|
|
89283aa788 | ||
|
|
f7dc27f2d0 | ||
|
|
ed560e466f | ||
|
|
b5f1dc9d95 | ||
|
|
f58ad7625f | ||
|
|
49c6d8019f | ||
|
|
86db180a17 | ||
|
|
31e59cd583 | ||
|
|
d2bfcd70e7 |
No files matched your search
@@ -77,6 +77,7 @@
|
|||||||
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
|
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
|
||||||
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
|
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
|
||||||
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
|
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
|
||||||
|
- Goal: merge PRs. Prefer **rebase** when commits are clean; **squash** when history is messy.
|
||||||
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
|
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
|
||||||
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
|
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
|
||||||
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
|
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
|
||||||
|
|||||||
@@ -5,28 +5,65 @@ Docs: https://docs.clawd.bot
|
|||||||
## 2026.1.23 (Unreleased)
|
## 2026.1.23 (Unreleased)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
- Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits.
|
||||||
|
- Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman.
|
||||||
|
- Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node).
|
||||||
|
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
|
||||||
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
||||||
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
||||||
|
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
|
||||||
|
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
||||||
|
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
||||||
|
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
|
||||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||||
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
||||||
|
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
|
||||||
|
- TTS: move Telegram TTS into core with auto-replies, commands, and gateway methods. (#1559) Thanks @Glucksberg.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
|
||||||
|
- Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
|
||||||
|
- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
|
||||||
|
- Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops).
|
||||||
|
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
|
||||||
|
- Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4.
|
||||||
|
- Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
|
||||||
|
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
||||||
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
||||||
|
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
||||||
|
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
|
||||||
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
||||||
|
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
||||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||||
|
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
||||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
||||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
||||||
- CLI: suppress diagnostic session/run noise during auth probes.
|
- CLI: suppress diagnostic session/run noise during auth probes.
|
||||||
- CLI: hide auth probe timeout warnings from embedded runs.
|
- CLI: hide auth probe timeout warnings from embedded runs.
|
||||||
- CLI: render auth probe results as a table in `clawdbot models status`.
|
- CLI: render auth probe results as a table in `clawdbot models status`.
|
||||||
|
- CLI: suppress probe-only embedded logs unless `--verbose` is set.
|
||||||
|
- CLI: move auth probe errors below the table to reduce wrapping.
|
||||||
|
- CLI: prevent ANSI color bleed when table cells wrap.
|
||||||
|
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
||||||
|
- CLI: drop the em dash when the banner tagline wraps to a second line.
|
||||||
|
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
||||||
|
- Telegram: render markdown in media captions. (#1478)
|
||||||
|
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
|
||||||
|
- Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
|
||||||
|
- Daemon: use platform PATH delimiters when building minimal service paths.
|
||||||
|
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
||||||
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
||||||
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
||||||
|
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
|
||||||
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
||||||
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
||||||
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
||||||
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
||||||
|
- MS Teams (plugin): remove `.default` suffix from Bot Framework probe scope to avoid double-appending. (#1574) Thanks @Evizero.
|
||||||
|
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
|
||||||
|
- Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)
|
||||||
|
|
||||||
## 2026.1.22
|
## 2026.1.22
|
||||||
|
|
||||||
|
|||||||
@@ -479,24 +479,25 @@ Thanks to all clawtributors:
|
|||||||
<p align="left">
|
<p align="left">
|
||||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
||||||
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||||
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/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/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/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a>
|
||||||
<a href="https://github.com/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/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a>
|
||||||
<a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a>
|
||||||
<a href="https://github.com/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/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/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/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/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/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/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/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/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/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a>
|
||||||
<a href="https://github.com/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/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/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/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
|
||||||
<a href="https://github.com/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/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/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/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a>
|
||||||
|
<a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a>
|
||||||
<a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
|
<a href="https://github.com/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/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/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>
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
public let to: String
|
public let to: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let mediaurl: String?
|
public let mediaurl: String?
|
||||||
|
public let mediaurls: [String]?
|
||||||
public let gifplayback: Bool?
|
public let gifplayback: Bool?
|
||||||
public let channel: String?
|
public let channel: String?
|
||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
@@ -395,6 +396,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
to: String,
|
to: String,
|
||||||
message: String,
|
message: String,
|
||||||
mediaurl: String?,
|
mediaurl: String?,
|
||||||
|
mediaurls: [String]?,
|
||||||
gifplayback: Bool?,
|
gifplayback: Bool?,
|
||||||
channel: String?,
|
channel: String?,
|
||||||
accountid: String?,
|
accountid: String?,
|
||||||
@@ -404,6 +406,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
self.to = to
|
self.to = to
|
||||||
self.message = message
|
self.message = message
|
||||||
self.mediaurl = mediaurl
|
self.mediaurl = mediaurl
|
||||||
|
self.mediaurls = mediaurls
|
||||||
self.gifplayback = gifplayback
|
self.gifplayback = gifplayback
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
@@ -414,6 +417,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
case to
|
case to
|
||||||
case message
|
case message
|
||||||
case mediaurl = "mediaUrl"
|
case mediaurl = "mediaUrl"
|
||||||
|
case mediaurls = "mediaUrls"
|
||||||
case gifplayback = "gifPlayback"
|
case gifplayback = "gifPlayback"
|
||||||
case channel
|
case channel
|
||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
@@ -478,6 +482,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
public let replyaccountid: String?
|
public let replyaccountid: String?
|
||||||
public let threadid: String?
|
public let threadid: String?
|
||||||
|
public let groupid: String?
|
||||||
|
public let groupchannel: String?
|
||||||
|
public let groupspace: String?
|
||||||
public let timeout: Int?
|
public let timeout: Int?
|
||||||
public let lane: String?
|
public let lane: String?
|
||||||
public let extrasystemprompt: String?
|
public let extrasystemprompt: String?
|
||||||
@@ -500,6 +507,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
accountid: String?,
|
accountid: String?,
|
||||||
replyaccountid: String?,
|
replyaccountid: String?,
|
||||||
threadid: String?,
|
threadid: String?,
|
||||||
|
groupid: String?,
|
||||||
|
groupchannel: String?,
|
||||||
|
groupspace: String?,
|
||||||
timeout: Int?,
|
timeout: Int?,
|
||||||
lane: String?,
|
lane: String?,
|
||||||
extrasystemprompt: String?,
|
extrasystemprompt: String?,
|
||||||
@@ -521,6 +531,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
self.replyaccountid = replyaccountid
|
self.replyaccountid = replyaccountid
|
||||||
self.threadid = threadid
|
self.threadid = threadid
|
||||||
|
self.groupid = groupid
|
||||||
|
self.groupchannel = groupchannel
|
||||||
|
self.groupspace = groupspace
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.lane = lane
|
self.lane = lane
|
||||||
self.extrasystemprompt = extrasystemprompt
|
self.extrasystemprompt = extrasystemprompt
|
||||||
@@ -543,6 +556,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
case replyaccountid = "replyAccountId"
|
case replyaccountid = "replyAccountId"
|
||||||
case threadid = "threadId"
|
case threadid = "threadId"
|
||||||
|
case groupid = "groupId"
|
||||||
|
case groupchannel = "groupChannel"
|
||||||
|
case groupspace = "groupSpace"
|
||||||
case timeout
|
case timeout
|
||||||
case lane
|
case lane
|
||||||
case extrasystemprompt = "extraSystemPrompt"
|
case extrasystemprompt = "extraSystemPrompt"
|
||||||
@@ -1973,25 +1989,25 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
|||||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||||
public let id: String?
|
public let id: String?
|
||||||
public let command: String
|
public let command: String
|
||||||
public let cwd: String?
|
public let cwd: AnyCodable?
|
||||||
public let host: String?
|
public let host: AnyCodable?
|
||||||
public let security: String?
|
public let security: AnyCodable?
|
||||||
public let ask: String?
|
public let ask: AnyCodable?
|
||||||
public let agentid: String?
|
public let agentid: AnyCodable?
|
||||||
public let resolvedpath: String?
|
public let resolvedpath: AnyCodable?
|
||||||
public let sessionkey: String?
|
public let sessionkey: AnyCodable?
|
||||||
public let timeoutms: Int?
|
public let timeoutms: Int?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: String?,
|
id: String?,
|
||||||
command: String,
|
command: String,
|
||||||
cwd: String?,
|
cwd: AnyCodable?,
|
||||||
host: String?,
|
host: AnyCodable?,
|
||||||
security: String?,
|
security: AnyCodable?,
|
||||||
ask: String?,
|
ask: AnyCodable?,
|
||||||
agentid: String?,
|
agentid: AnyCodable?,
|
||||||
resolvedpath: String?,
|
resolvedpath: AnyCodable?,
|
||||||
sessionkey: String?,
|
sessionkey: AnyCodable?,
|
||||||
timeoutms: Int?
|
timeoutms: Int?
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
public let to: String
|
public let to: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let mediaurl: String?
|
public let mediaurl: String?
|
||||||
|
public let mediaurls: [String]?
|
||||||
public let gifplayback: Bool?
|
public let gifplayback: Bool?
|
||||||
public let channel: String?
|
public let channel: String?
|
||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
@@ -395,6 +396,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
to: String,
|
to: String,
|
||||||
message: String,
|
message: String,
|
||||||
mediaurl: String?,
|
mediaurl: String?,
|
||||||
|
mediaurls: [String]?,
|
||||||
gifplayback: Bool?,
|
gifplayback: Bool?,
|
||||||
channel: String?,
|
channel: String?,
|
||||||
accountid: String?,
|
accountid: String?,
|
||||||
@@ -404,6 +406,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
self.to = to
|
self.to = to
|
||||||
self.message = message
|
self.message = message
|
||||||
self.mediaurl = mediaurl
|
self.mediaurl = mediaurl
|
||||||
|
self.mediaurls = mediaurls
|
||||||
self.gifplayback = gifplayback
|
self.gifplayback = gifplayback
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
@@ -414,6 +417,7 @@ public struct SendParams: Codable, Sendable {
|
|||||||
case to
|
case to
|
||||||
case message
|
case message
|
||||||
case mediaurl = "mediaUrl"
|
case mediaurl = "mediaUrl"
|
||||||
|
case mediaurls = "mediaUrls"
|
||||||
case gifplayback = "gifPlayback"
|
case gifplayback = "gifPlayback"
|
||||||
case channel
|
case channel
|
||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
@@ -478,6 +482,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
public let replyaccountid: String?
|
public let replyaccountid: String?
|
||||||
public let threadid: String?
|
public let threadid: String?
|
||||||
|
public let groupid: String?
|
||||||
|
public let groupchannel: String?
|
||||||
|
public let groupspace: String?
|
||||||
public let timeout: Int?
|
public let timeout: Int?
|
||||||
public let lane: String?
|
public let lane: String?
|
||||||
public let extrasystemprompt: String?
|
public let extrasystemprompt: String?
|
||||||
@@ -500,6 +507,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
accountid: String?,
|
accountid: String?,
|
||||||
replyaccountid: String?,
|
replyaccountid: String?,
|
||||||
threadid: String?,
|
threadid: String?,
|
||||||
|
groupid: String?,
|
||||||
|
groupchannel: String?,
|
||||||
|
groupspace: String?,
|
||||||
timeout: Int?,
|
timeout: Int?,
|
||||||
lane: String?,
|
lane: String?,
|
||||||
extrasystemprompt: String?,
|
extrasystemprompt: String?,
|
||||||
@@ -521,6 +531,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
self.replyaccountid = replyaccountid
|
self.replyaccountid = replyaccountid
|
||||||
self.threadid = threadid
|
self.threadid = threadid
|
||||||
|
self.groupid = groupid
|
||||||
|
self.groupchannel = groupchannel
|
||||||
|
self.groupspace = groupspace
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.lane = lane
|
self.lane = lane
|
||||||
self.extrasystemprompt = extrasystemprompt
|
self.extrasystemprompt = extrasystemprompt
|
||||||
@@ -543,6 +556,9 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
case replyaccountid = "replyAccountId"
|
case replyaccountid = "replyAccountId"
|
||||||
case threadid = "threadId"
|
case threadid = "threadId"
|
||||||
|
case groupid = "groupId"
|
||||||
|
case groupchannel = "groupChannel"
|
||||||
|
case groupspace = "groupSpace"
|
||||||
case timeout
|
case timeout
|
||||||
case lane
|
case lane
|
||||||
case extrasystemprompt = "extraSystemPrompt"
|
case extrasystemprompt = "extraSystemPrompt"
|
||||||
@@ -1973,25 +1989,25 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
|||||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||||
public let id: String?
|
public let id: String?
|
||||||
public let command: String
|
public let command: String
|
||||||
public let cwd: String?
|
public let cwd: AnyCodable?
|
||||||
public let host: String?
|
public let host: AnyCodable?
|
||||||
public let security: String?
|
public let security: AnyCodable?
|
||||||
public let ask: String?
|
public let ask: AnyCodable?
|
||||||
public let agentid: String?
|
public let agentid: AnyCodable?
|
||||||
public let resolvedpath: String?
|
public let resolvedpath: AnyCodable?
|
||||||
public let sessionkey: String?
|
public let sessionkey: AnyCodable?
|
||||||
public let timeoutms: Int?
|
public let timeoutms: Int?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: String?,
|
id: String?,
|
||||||
command: String,
|
command: String,
|
||||||
cwd: String?,
|
cwd: AnyCodable?,
|
||||||
host: String?,
|
host: AnyCodable?,
|
||||||
security: String?,
|
security: AnyCodable?,
|
||||||
ask: String?,
|
ask: AnyCodable?,
|
||||||
agentid: String?,
|
agentid: AnyCodable?,
|
||||||
resolvedpath: String?,
|
resolvedpath: AnyCodable?,
|
||||||
sessionkey: String?,
|
sessionkey: AnyCodable?,
|
||||||
timeoutms: Int?
|
timeoutms: Int?
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ services:
|
|||||||
[
|
[
|
||||||
"node",
|
"node",
|
||||||
"dist/index.js",
|
"dist/index.js",
|
||||||
"gateway-daemon",
|
"gateway",
|
||||||
"--bind",
|
"--bind",
|
||||||
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ summary: "Cron jobs + wakeups for the Gateway scheduler"
|
|||||||
read_when:
|
read_when:
|
||||||
- Scheduling background jobs or wakeups
|
- Scheduling background jobs or wakeups
|
||||||
- Wiring automation that should run with or alongside heartbeats
|
- Wiring automation that should run with or alongside heartbeats
|
||||||
|
- Deciding between heartbeat and cron for scheduled tasks
|
||||||
---
|
---
|
||||||
# Cron jobs (Gateway scheduler)
|
# Cron jobs (Gateway scheduler)
|
||||||
|
|
||||||
|
> **Cron vs Heartbeat?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each.
|
||||||
|
|
||||||
Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at
|
Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at
|
||||||
the right time, and can optionally deliver output back to a chat.
|
the right time, and can optionally deliver output back to a chat.
|
||||||
|
|
||||||
@@ -260,15 +263,15 @@ Run history:
|
|||||||
clawdbot cron runs --id <jobId> --limit 50
|
clawdbot cron runs --id <jobId> --limit 50
|
||||||
```
|
```
|
||||||
|
|
||||||
Immediate wake without creating a job:
|
Immediate system event without creating a job:
|
||||||
```bash
|
```bash
|
||||||
clawdbot wake --mode now --text "Next heartbeat: check battery."
|
clawdbot system event --mode now --text "Next heartbeat: check battery."
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gateway API surface
|
## Gateway API surface
|
||||||
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
||||||
- `cron.run` (force or due), `cron.runs`
|
- `cron.run` (force or due), `cron.runs`
|
||||||
- `wake` (enqueue system event + optional heartbeat)
|
For immediate system events without a job, use [`clawdbot system event`](/cli/system).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
---
|
||||||
|
summary: "Guidance for choosing between heartbeat and cron jobs for automation"
|
||||||
|
read_when:
|
||||||
|
- Deciding how to schedule recurring tasks
|
||||||
|
- Setting up background monitoring or notifications
|
||||||
|
- Optimizing token usage for periodic checks
|
||||||
|
---
|
||||||
|
# Cron vs Heartbeat: When to Use Each
|
||||||
|
|
||||||
|
Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case.
|
||||||
|
|
||||||
|
## Quick Decision Guide
|
||||||
|
|
||||||
|
| Use Case | Recommended | Why |
|
||||||
|
|----------|-------------|-----|
|
||||||
|
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
|
||||||
|
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
|
||||||
|
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
|
||||||
|
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
|
||||||
|
| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing |
|
||||||
|
| Background project health check | Heartbeat | Piggybacks on existing cycle |
|
||||||
|
|
||||||
|
## Heartbeat: Periodic Awareness
|
||||||
|
|
||||||
|
Heartbeats run in the **main session** at a regular interval (default: 30 min). They're designed for the agent to check on things and surface anything important.
|
||||||
|
|
||||||
|
### When to use heartbeat
|
||||||
|
|
||||||
|
- **Multiple periodic checks**: Instead of 5 separate cron jobs checking inbox, calendar, weather, notifications, and project status, a single heartbeat can batch all of these.
|
||||||
|
- **Context-aware decisions**: The agent has full main-session context, so it can make smart decisions about what's urgent vs. what can wait.
|
||||||
|
- **Conversational continuity**: Heartbeat runs share the same session, so the agent remembers recent conversations and can follow up naturally.
|
||||||
|
- **Low-overhead monitoring**: One heartbeat replaces many small polling tasks.
|
||||||
|
|
||||||
|
### Heartbeat advantages
|
||||||
|
|
||||||
|
- **Batches multiple checks**: One agent turn can review inbox, calendar, and notifications together.
|
||||||
|
- **Reduces API calls**: A single heartbeat is cheaper than 5 isolated cron jobs.
|
||||||
|
- **Context-aware**: The agent knows what you've been working on and can prioritize accordingly.
|
||||||
|
- **Smart suppression**: If nothing needs attention, the agent replies `HEARTBEAT_OK` and no message is delivered.
|
||||||
|
- **Natural timing**: Drifts slightly based on queue load, which is fine for most monitoring.
|
||||||
|
|
||||||
|
### Heartbeat example: HEARTBEAT.md checklist
|
||||||
|
|
||||||
|
```md
|
||||||
|
# Heartbeat checklist
|
||||||
|
|
||||||
|
- Check email for urgent messages
|
||||||
|
- Review calendar for events in next 2 hours
|
||||||
|
- If a background task finished, summarize results
|
||||||
|
- If idle for 8+ hours, send a brief check-in
|
||||||
|
```
|
||||||
|
|
||||||
|
The agent reads this on each heartbeat and handles all items in one turn.
|
||||||
|
|
||||||
|
### Configuring heartbeat
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
heartbeat: {
|
||||||
|
every: "30m", // interval
|
||||||
|
target: "last", // where to deliver alerts
|
||||||
|
activeHours: { start: "08:00", end: "22:00" } // optional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Heartbeat](/gateway/heartbeat) for full configuration.
|
||||||
|
|
||||||
|
## Cron: Precise Scheduling
|
||||||
|
|
||||||
|
Cron jobs run at **exact times** and can run in isolated sessions without affecting main context.
|
||||||
|
|
||||||
|
### When to use cron
|
||||||
|
|
||||||
|
- **Exact timing required**: "Send this at 9:00 AM every Monday" (not "sometime around 9").
|
||||||
|
- **Standalone tasks**: Tasks that don't need conversational context.
|
||||||
|
- **Different model/thinking**: Heavy analysis that warrants a more powerful model.
|
||||||
|
- **One-shot reminders**: "Remind me in 20 minutes" with `--at`.
|
||||||
|
- **Noisy/frequent tasks**: Tasks that would clutter main session history.
|
||||||
|
- **External triggers**: Tasks that should run independently of whether the agent is otherwise active.
|
||||||
|
|
||||||
|
### Cron advantages
|
||||||
|
|
||||||
|
- **Exact timing**: 5-field cron expressions with timezone support.
|
||||||
|
- **Session isolation**: Runs in `cron:<jobId>` without polluting main history.
|
||||||
|
- **Model overrides**: Use a cheaper or more powerful model per job.
|
||||||
|
- **Delivery control**: Can deliver directly to a channel; still posts a summary to main by default (configurable).
|
||||||
|
- **No agent context needed**: Runs even if main session is idle or compacted.
|
||||||
|
- **One-shot support**: `--at` for precise future timestamps.
|
||||||
|
|
||||||
|
### Cron example: Daily morning briefing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot cron add \
|
||||||
|
--name "Morning briefing" \
|
||||||
|
--cron "0 7 * * *" \
|
||||||
|
--tz "America/New_York" \
|
||||||
|
--session isolated \
|
||||||
|
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
|
||||||
|
--model opus \
|
||||||
|
--deliver \
|
||||||
|
--channel whatsapp \
|
||||||
|
--to "+15551234567"
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs at exactly 7:00 AM New York time, uses Opus for quality, and delivers directly to WhatsApp.
|
||||||
|
|
||||||
|
### Cron example: One-shot reminder
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot cron add \
|
||||||
|
--name "Meeting reminder" \
|
||||||
|
--at "20m" \
|
||||||
|
--session main \
|
||||||
|
--system-event "Reminder: standup meeting starts in 10 minutes." \
|
||||||
|
--wake now \
|
||||||
|
--delete-after-run
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Cron jobs](/automation/cron-jobs) for full CLI reference.
|
||||||
|
|
||||||
|
## Decision Flowchart
|
||||||
|
|
||||||
|
```
|
||||||
|
Does the task need to run at an EXACT time?
|
||||||
|
YES -> Use cron
|
||||||
|
NO -> Continue...
|
||||||
|
|
||||||
|
Does the task need isolation from main session?
|
||||||
|
YES -> Use cron (isolated)
|
||||||
|
NO -> Continue...
|
||||||
|
|
||||||
|
Can this task be batched with other periodic checks?
|
||||||
|
YES -> Use heartbeat (add to HEARTBEAT.md)
|
||||||
|
NO -> Use cron
|
||||||
|
|
||||||
|
Is this a one-shot reminder?
|
||||||
|
YES -> Use cron with --at
|
||||||
|
NO -> Continue...
|
||||||
|
|
||||||
|
Does it need a different model or thinking level?
|
||||||
|
YES -> Use cron (isolated) with --model/--thinking
|
||||||
|
NO -> Use heartbeat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Combining Both
|
||||||
|
|
||||||
|
The most efficient setup uses **both**:
|
||||||
|
|
||||||
|
1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
|
||||||
|
2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders.
|
||||||
|
|
||||||
|
### Example: Efficient automation setup
|
||||||
|
|
||||||
|
**HEARTBEAT.md** (checked every 30 min):
|
||||||
|
```md
|
||||||
|
# Heartbeat checklist
|
||||||
|
- Scan inbox for urgent emails
|
||||||
|
- Check calendar for events in next 2h
|
||||||
|
- Review any pending tasks
|
||||||
|
- Light check-in if quiet for 8+ hours
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cron jobs** (precise timing):
|
||||||
|
```bash
|
||||||
|
# Daily morning briefing at 7am
|
||||||
|
clawdbot cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --deliver
|
||||||
|
|
||||||
|
# Weekly project review on Mondays at 9am
|
||||||
|
clawdbot cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
|
||||||
|
|
||||||
|
# One-shot reminder
|
||||||
|
clawdbot cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Lobster: Deterministic workflows with approvals
|
||||||
|
|
||||||
|
Lobster is the workflow runtime for **multi-step tool pipelines** that need deterministic execution and explicit approvals.
|
||||||
|
Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints.
|
||||||
|
|
||||||
|
### When Lobster fits
|
||||||
|
|
||||||
|
- **Multi-step automation**: You need a fixed pipeline of tool calls, not a one-off prompt.
|
||||||
|
- **Approval gates**: Side effects should pause until you approve, then resume.
|
||||||
|
- **Resumable runs**: Continue a paused workflow without re-running earlier steps.
|
||||||
|
|
||||||
|
### How it pairs with heartbeat and cron
|
||||||
|
|
||||||
|
- **Heartbeat/cron** decide *when* a run happens.
|
||||||
|
- **Lobster** defines *what steps* happen once the run starts.
|
||||||
|
|
||||||
|
For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster.
|
||||||
|
For ad-hoc workflows, call Lobster directly.
|
||||||
|
|
||||||
|
### Operational notes (from the code)
|
||||||
|
|
||||||
|
- Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**.
|
||||||
|
- If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag.
|
||||||
|
- The tool is an **optional plugin**; you must allowlist `lobster` in `tools.allow`.
|
||||||
|
- If you pass `lobsterPath`, it must be an **absolute path**.
|
||||||
|
|
||||||
|
See [Lobster](/tools/lobster) for full usage and examples.
|
||||||
|
|
||||||
|
## Main Session vs Isolated Session
|
||||||
|
|
||||||
|
Both heartbeat and cron can interact with the main session, but differently:
|
||||||
|
|
||||||
|
| | Heartbeat | Cron (main) | Cron (isolated) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Session | Main | Main (via system event) | `cron:<jobId>` |
|
||||||
|
| History | Shared | Shared | Fresh each run |
|
||||||
|
| Context | Full | Full | None (starts clean) |
|
||||||
|
| Model | Main session model | Main session model | Can override |
|
||||||
|
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Summary posted to main |
|
||||||
|
|
||||||
|
### When to use main session cron
|
||||||
|
|
||||||
|
Use `--session main` with `--system-event` when you want:
|
||||||
|
- The reminder/event to appear in main session context
|
||||||
|
- The agent to handle it during the next heartbeat with full context
|
||||||
|
- No separate isolated run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot cron add \
|
||||||
|
--name "Check project" \
|
||||||
|
--every "4h" \
|
||||||
|
--session main \
|
||||||
|
--system-event "Time for a project health check" \
|
||||||
|
--wake now
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to use isolated cron
|
||||||
|
|
||||||
|
Use `--session isolated` when you want:
|
||||||
|
- A clean slate without prior context
|
||||||
|
- Different model or thinking settings
|
||||||
|
- Output delivered directly to a channel (summary still posts to main by default)
|
||||||
|
- History that doesn't clutter main session
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot cron add \
|
||||||
|
--name "Deep analysis" \
|
||||||
|
--cron "0 6 * * 0" \
|
||||||
|
--session isolated \
|
||||||
|
--message "Weekly codebase analysis..." \
|
||||||
|
--model opus \
|
||||||
|
--thinking high \
|
||||||
|
--deliver
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cost Considerations
|
||||||
|
|
||||||
|
| Mechanism | Cost Profile |
|
||||||
|
|-----------|--------------|
|
||||||
|
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
|
||||||
|
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
|
||||||
|
| Cron (isolated) | Full agent turn per job; can use cheaper model |
|
||||||
|
|
||||||
|
**Tips**:
|
||||||
|
- Keep `HEARTBEAT.md` small to minimize token overhead.
|
||||||
|
- Batch similar checks into heartbeat instead of multiple cron jobs.
|
||||||
|
- Use `target: "none"` on heartbeat if you only want internal processing.
|
||||||
|
- Use isolated cron with a cheaper model for routine tasks.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
|
||||||
|
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
|
||||||
|
- [System](/cli/system) - system events + heartbeat controls
|
||||||
@@ -17,6 +17,37 @@ not an API key.
|
|||||||
- Auth: AWS credentials (env vars, shared config, or instance role)
|
- Auth: AWS credentials (env vars, shared config, or instance role)
|
||||||
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
|
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
|
||||||
|
|
||||||
|
## Automatic model discovery
|
||||||
|
|
||||||
|
If AWS credentials are detected, Clawdbot can automatically discover Bedrock
|
||||||
|
models that support **streaming** and **text output**. Discovery uses
|
||||||
|
`bedrock:ListFoundationModels` and is cached (default: 1 hour).
|
||||||
|
|
||||||
|
Config options live under `models.bedrockDiscovery`:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
models: {
|
||||||
|
bedrockDiscovery: {
|
||||||
|
enabled: true,
|
||||||
|
region: "us-east-1",
|
||||||
|
providerFilter: ["anthropic", "amazon"],
|
||||||
|
refreshInterval: 3600,
|
||||||
|
defaultContextWindow: 32000,
|
||||||
|
defaultMaxTokens: 4096
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `enabled` defaults to `true` when AWS credentials are present.
|
||||||
|
- `region` defaults to `AWS_REGION` or `AWS_DEFAULT_REGION`, then `us-east-1`.
|
||||||
|
- `providerFilter` matches Bedrock provider names (for example `anthropic`).
|
||||||
|
- `refreshInterval` is seconds; set to `0` to disable caching.
|
||||||
|
- `defaultContextWindow` (default: `32000`) and `defaultMaxTokens` (default: `4096`)
|
||||||
|
are used for discovered models (override if you know your model limits).
|
||||||
|
|
||||||
## Setup (manual)
|
## Setup (manual)
|
||||||
|
|
||||||
1) Ensure AWS credentials are available on the **gateway host**:
|
1) Ensure AWS credentials are available on the **gateway host**:
|
||||||
@@ -67,6 +98,7 @@ export AWS_BEARER_TOKEN_BEDROCK="..."
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Bedrock requires **model access** enabled in your AWS account/region.
|
- Bedrock requires **model access** enabled in your AWS account/region.
|
||||||
|
- Automatic discovery needs the `bedrock:ListFoundationModels` permission.
|
||||||
- If you use profiles, set `AWS_PROFILE` on the gateway host.
|
- If you use profiles, set `AWS_PROFILE` on the gateway host.
|
||||||
- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`,
|
- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`,
|
||||||
then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the
|
then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ clawdbot channels logout --channel whatsapp
|
|||||||
|
|
||||||
- Run `clawdbot status --deep` for a broad probe.
|
- Run `clawdbot status --deep` for a broad probe.
|
||||||
- Use `clawdbot doctor` for guided fixes.
|
- Use `clawdbot doctor` for guided fixes.
|
||||||
|
- `clawdbot channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude Code CLI.
|
||||||
|
|
||||||
## Capabilities probe
|
## Capabilities probe
|
||||||
|
|
||||||
|
|||||||
+22
-5
@@ -29,6 +29,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`sessions`](/cli/sessions)
|
- [`sessions`](/cli/sessions)
|
||||||
- [`gateway`](/cli/gateway)
|
- [`gateway`](/cli/gateway)
|
||||||
- [`logs`](/cli/logs)
|
- [`logs`](/cli/logs)
|
||||||
|
- [`system`](/cli/system)
|
||||||
- [`models`](/cli/models)
|
- [`models`](/cli/models)
|
||||||
- [`memory`](/cli/memory)
|
- [`memory`](/cli/memory)
|
||||||
- [`nodes`](/cli/nodes)
|
- [`nodes`](/cli/nodes)
|
||||||
@@ -38,7 +39,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`sandbox`](/cli/sandbox)
|
- [`sandbox`](/cli/sandbox)
|
||||||
- [`tui`](/cli/tui)
|
- [`tui`](/cli/tui)
|
||||||
- [`browser`](/cli/browser)
|
- [`browser`](/cli/browser)
|
||||||
- [`wake`](/cli/wake)
|
|
||||||
- [`cron`](/cli/cron)
|
- [`cron`](/cli/cron)
|
||||||
- [`dns`](/cli/dns)
|
- [`dns`](/cli/dns)
|
||||||
- [`docs`](/cli/docs)
|
- [`docs`](/cli/docs)
|
||||||
@@ -145,6 +145,10 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
restart
|
restart
|
||||||
run
|
run
|
||||||
logs
|
logs
|
||||||
|
system
|
||||||
|
event
|
||||||
|
heartbeat last|enable|disable
|
||||||
|
presence
|
||||||
models
|
models
|
||||||
list
|
list
|
||||||
status
|
status
|
||||||
@@ -160,7 +164,6 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
list
|
list
|
||||||
recreate
|
recreate
|
||||||
explain
|
explain
|
||||||
wake
|
|
||||||
cron
|
cron
|
||||||
status
|
status
|
||||||
list
|
list
|
||||||
@@ -763,9 +766,9 @@ Options:
|
|||||||
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
||||||
- `clear`: `--provider <name>`, `--agent <id>`
|
- `clear`: `--provider <name>`, `--agent <id>`
|
||||||
|
|
||||||
## Cron + wake
|
## System
|
||||||
|
|
||||||
### `wake`
|
### `system event`
|
||||||
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
||||||
|
|
||||||
Required:
|
Required:
|
||||||
@@ -776,7 +779,21 @@ Options:
|
|||||||
- `--json`
|
- `--json`
|
||||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||||
|
|
||||||
### `cron`
|
### `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
|
||||||
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
|
|||||||
@@ -23,6 +23,24 @@ Common use cases:
|
|||||||
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
||||||
node host, so you can keep command access scoped and explicit.
|
node host, so you can keep command access scoped and explicit.
|
||||||
|
|
||||||
|
## Browser proxy (zero-config)
|
||||||
|
|
||||||
|
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
|
||||||
|
disabled on the node. This lets the agent use browser automation on that node
|
||||||
|
without extra configuration.
|
||||||
|
|
||||||
|
Disable it on the node if needed:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
nodeHost: {
|
||||||
|
browserProxy: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Run (foreground)
|
## Run (foreground)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "CLI reference for `clawdbot wake` (enqueue a system event and optionally trigger an immediate heartbeat)"
|
|
||||||
read_when:
|
|
||||||
- You want to “poke” a running Gateway to process a system event
|
|
||||||
- You use `wake` with cron jobs or remote nodes
|
|
||||||
---
|
|
||||||
|
|
||||||
# `clawdbot wake`
|
|
||||||
|
|
||||||
Enqueue a system event on the Gateway and optionally trigger an immediate heartbeat.
|
|
||||||
|
|
||||||
This is a lightweight “poke” for automation flows where you don’t want to run a full command, but you do want the Gateway to react quickly.
|
|
||||||
|
|
||||||
Related:
|
|
||||||
- Cron jobs: [Cron](/cli/cron)
|
|
||||||
- Gateway heartbeat: [Heartbeat](/gateway/heartbeat)
|
|
||||||
|
|
||||||
## Common commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot wake --text "sync"
|
|
||||||
clawdbot wake --text "sync" --mode now
|
|
||||||
```
|
|
||||||
|
|
||||||
## Flags
|
|
||||||
|
|
||||||
- `--text <text>`: system event text.
|
|
||||||
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
|
||||||
- `--json`: machine-readable output.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Requires a running Gateway reachable by your current config (local or remote).
|
|
||||||
- If you’re using sandboxing, `wake` still targets the Gateway; sandboxing does not block the command itself.
|
|
||||||
|
|
||||||
@@ -66,12 +66,12 @@ To inspect how much each injected file contributes (raw vs injected, truncation,
|
|||||||
|
|
||||||
## Time handling
|
## Time handling
|
||||||
|
|
||||||
The system prompt includes a dedicated **Current Date & Time** section when user
|
The system prompt includes a dedicated **Current Date & Time** section when the
|
||||||
time or timezone is known. It is explicit about:
|
user timezone is known. To keep the prompt cache-stable, it now only includes
|
||||||
|
the **time zone** (no dynamic clock or time format).
|
||||||
|
|
||||||
- The user’s **local time** (already converted).
|
Use `session_status` when the agent needs the current time; the status card
|
||||||
- The **time zone** used for the conversion.
|
includes a timestamp line.
|
||||||
- The **time format** (12-hour / 24-hour).
|
|
||||||
|
|
||||||
Configure with:
|
Configure with:
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -7,8 +7,8 @@ read_when:
|
|||||||
|
|
||||||
# Date & Time
|
# Date & Time
|
||||||
|
|
||||||
Clawdbot defaults to **host-local time for transport timestamps** and **user-local time only in the system prompt**.
|
Clawdbot defaults to **host-local time for transport timestamps** and **user timezone only in the system prompt**.
|
||||||
Provider timestamps are preserved so tools keep their native semantics.
|
Provider timestamps are preserved so tools keep their native semantics (current time is available via `session_status`).
|
||||||
|
|
||||||
## Message envelopes (local by default)
|
## Message envelopes (local by default)
|
||||||
|
|
||||||
@@ -63,16 +63,16 @@ You can override this behavior:
|
|||||||
|
|
||||||
## System prompt: Current Date & Time
|
## System prompt: Current Date & Time
|
||||||
|
|
||||||
If the user timezone or local time is known, the system prompt includes a dedicated
|
If the user timezone is known, the system prompt includes a dedicated
|
||||||
**Current Date & Time** section:
|
**Current Date & Time** section with the **time zone only** (no clock/time format)
|
||||||
|
to keep prompt caching stable:
|
||||||
|
|
||||||
```
|
```
|
||||||
Thursday, January 15th, 2026 — 3:07 PM (America/Chicago)
|
Time zone: America/Chicago
|
||||||
Time format: 12-hour
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If only the timezone is known, we still include the section and instruct the model
|
When the agent needs the current time, use the `session_status` tool; the status
|
||||||
to assume UTC for unknown time references.
|
card includes a timestamp line.
|
||||||
|
|
||||||
## System event lines (local by default)
|
## System event lines (local by default)
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -349,6 +349,14 @@
|
|||||||
"source": "/cron-jobs",
|
"source": "/cron-jobs",
|
||||||
"destination": "/automation/cron-jobs"
|
"destination": "/automation/cron-jobs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/cron-vs-heartbeat",
|
||||||
|
"destination": "/automation/cron-vs-heartbeat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/cron-vs-heartbeat/",
|
||||||
|
"destination": "/automation/cron-vs-heartbeat"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/dashboard",
|
"source": "/dashboard",
|
||||||
"destination": "/web/dashboard"
|
"destination": "/web/dashboard"
|
||||||
@@ -842,12 +850,12 @@
|
|||||||
"cli/memory",
|
"cli/memory",
|
||||||
"cli/models",
|
"cli/models",
|
||||||
"cli/logs",
|
"cli/logs",
|
||||||
|
"cli/system",
|
||||||
"cli/nodes",
|
"cli/nodes",
|
||||||
"cli/approvals",
|
"cli/approvals",
|
||||||
"cli/gateway",
|
"cli/gateway",
|
||||||
"cli/tui",
|
"cli/tui",
|
||||||
"cli/voicecall",
|
"cli/voicecall",
|
||||||
"cli/wake",
|
|
||||||
"cli/cron",
|
"cli/cron",
|
||||||
"cli/dns",
|
"cli/dns",
|
||||||
"cli/docs",
|
"cli/docs",
|
||||||
@@ -984,6 +992,7 @@
|
|||||||
"automation/webhook",
|
"automation/webhook",
|
||||||
"automation/gmail-pubsub",
|
"automation/gmail-pubsub",
|
||||||
"automation/cron-jobs",
|
"automation/cron-jobs",
|
||||||
|
"automation/cron-vs-heartbeat",
|
||||||
"automation/poll"
|
"automation/poll"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -991,6 +1000,8 @@
|
|||||||
"group": "Tools & Skills",
|
"group": "Tools & Skills",
|
||||||
"pages": [
|
"pages": [
|
||||||
"tools",
|
"tools",
|
||||||
|
"tools/lobster",
|
||||||
|
"tools/llm-task",
|
||||||
"plugin",
|
"plugin",
|
||||||
"plugins/voice-call",
|
"plugins/voice-call",
|
||||||
"plugins/zalouser",
|
"plugins/zalouser",
|
||||||
@@ -1034,6 +1045,7 @@
|
|||||||
"platforms/android",
|
"platforms/android",
|
||||||
"platforms/windows",
|
"platforms/windows",
|
||||||
"platforms/linux",
|
"platforms/linux",
|
||||||
|
"platforms/fly",
|
||||||
"platforms/hetzner",
|
"platforms/hetzner",
|
||||||
"platforms/exe-dev"
|
"platforms/exe-dev"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1446,6 +1446,44 @@ active agent’s `identity.emoji` when set, otherwise `"👀"`. Set it to `""` t
|
|||||||
`removeAckAfterReply` removes the bot’s ack reaction after a reply is sent
|
`removeAckAfterReply` removes the bot’s ack reaction after a reply is sent
|
||||||
(Slack/Discord/Telegram only). Default: `false`.
|
(Slack/Discord/Telegram only). Default: `false`.
|
||||||
|
|
||||||
|
#### `messages.tts`
|
||||||
|
|
||||||
|
Enable text-to-speech for outbound replies. When on, Clawdbot generates audio
|
||||||
|
using ElevenLabs or OpenAI and attaches it to responses. Telegram uses Opus
|
||||||
|
voice notes; other channels send MP3 audio.
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
messages: {
|
||||||
|
tts: {
|
||||||
|
enabled: true,
|
||||||
|
mode: "final", // final | all (include tool/block replies)
|
||||||
|
provider: "elevenlabs",
|
||||||
|
maxTextLength: 4000,
|
||||||
|
timeoutMs: 30000,
|
||||||
|
prefsPath: "~/.clawdbot/settings/tts.json",
|
||||||
|
elevenlabs: {
|
||||||
|
apiKey: "elevenlabs_api_key",
|
||||||
|
voiceId: "voice_id",
|
||||||
|
modelId: "eleven_multilingual_v2"
|
||||||
|
},
|
||||||
|
openai: {
|
||||||
|
apiKey: "openai_api_key",
|
||||||
|
model: "gpt-4o-mini-tts",
|
||||||
|
voice: "alloy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `messages.tts.enabled` can be overridden by local user prefs (see `/tts_on`, `/tts_off`).
|
||||||
|
- `prefsPath` stores local overrides (enabled/provider/limit/summarize).
|
||||||
|
- `maxTextLength` is a hard cap for TTS input; summaries are truncated to fit.
|
||||||
|
- `/tts_limit` and `/tts_summary` control per-user summarization settings.
|
||||||
|
- `apiKey` values fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
|
||||||
|
|
||||||
### `talk`
|
### `talk`
|
||||||
|
|
||||||
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.
|
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.
|
||||||
@@ -1970,6 +2008,7 @@ Example (provider/model-specific allowlist):
|
|||||||
```
|
```
|
||||||
|
|
||||||
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||||||
|
Matching is case-insensitive and supports `*` wildcards (`"*"` means all tools).
|
||||||
This is applied even when the Docker sandbox is **off**.
|
This is applied even when the Docker sandbox is **off**.
|
||||||
|
|
||||||
Example (disable browser/canvas everywhere):
|
Example (disable browser/canvas everywhere):
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
summary: "Heartbeat polling messages and notification rules"
|
summary: "Heartbeat polling messages and notification rules"
|
||||||
read_when:
|
read_when:
|
||||||
- Adjusting heartbeat cadence or messaging
|
- Adjusting heartbeat cadence or messaging
|
||||||
|
- Deciding between heartbeat and cron for scheduled tasks
|
||||||
---
|
---
|
||||||
# Heartbeat (Gateway)
|
# Heartbeat (Gateway)
|
||||||
|
|
||||||
|
> **Heartbeat vs Cron?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each.
|
||||||
|
|
||||||
Heartbeat runs **periodic agent turns** in the main session so the model can
|
Heartbeat runs **periodic agent turns** in the main session so the model can
|
||||||
surface anything that needs attention without spamming you.
|
surface anything that needs attention without spamming you.
|
||||||
|
|
||||||
@@ -159,6 +162,10 @@ If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
|
|||||||
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
||||||
safe to include every 30 minutes.
|
safe to include every 30 minutes.
|
||||||
|
|
||||||
|
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||||
|
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||||
|
If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||||
|
|
||||||
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
||||||
|
|
||||||
Example `HEARTBEAT.md`:
|
Example `HEARTBEAT.md`:
|
||||||
@@ -192,7 +199,7 @@ Safety note: don’t put secrets (API keys, phone numbers, private tokens) into
|
|||||||
You can enqueue a system event and trigger an immediate heartbeat with:
|
You can enqueue a system event and trigger an immediate heartbeat with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot wake --text "Check for urgent follow-ups" --mode now
|
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
||||||
```
|
```
|
||||||
|
|
||||||
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
---
|
||||||
|
title: Fly.io
|
||||||
|
description: Deploy Clawdbot on Fly.io
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fly.io Deployment
|
||||||
|
|
||||||
|
**Goal:** Clawdbot Gateway running on a [Fly.io](https://fly.io) machine with persistent storage, automatic HTTPS, and Discord/channel access.
|
||||||
|
|
||||||
|
## What you need
|
||||||
|
|
||||||
|
- [flyctl CLI](https://fly.io/docs/hands-on/install-flyctl/) installed
|
||||||
|
- Fly.io account (free tier works)
|
||||||
|
- Model auth: Anthropic API key (or other provider keys)
|
||||||
|
- Channel credentials: Discord bot token, Telegram token, etc.
|
||||||
|
|
||||||
|
## Beginner quick path
|
||||||
|
|
||||||
|
1. Clone repo → customize `fly.toml`
|
||||||
|
2. Create app + volume → set secrets
|
||||||
|
3. Deploy with `fly deploy`
|
||||||
|
4. SSH in to create config or use Control UI
|
||||||
|
|
||||||
|
## 1) Create the Fly app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repo
|
||||||
|
git clone https://github.com/clawdbot/clawdbot.git
|
||||||
|
cd clawdbot
|
||||||
|
|
||||||
|
# Create a new Fly app (pick your own name)
|
||||||
|
fly apps create my-clawdbot
|
||||||
|
|
||||||
|
# Create a persistent volume (1GB is usually enough)
|
||||||
|
fly volumes create clawdbot_data --size 1 --region lhr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose).
|
||||||
|
|
||||||
|
## 2) Configure fly.toml
|
||||||
|
|
||||||
|
Edit `fly.toml` to match your app name and requirements:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
app = "my-clawdbot" # Your app name
|
||||||
|
primary_region = "lhr"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
NODE_ENV = "production"
|
||||||
|
CLAWDBOT_PREFER_PNPM = "1"
|
||||||
|
CLAWDBOT_STATE_DIR = "/data"
|
||||||
|
NODE_OPTIONS = "--max-old-space-size=1536"
|
||||||
|
|
||||||
|
[processes]
|
||||||
|
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
|
||||||
|
|
||||||
|
[http_service]
|
||||||
|
internal_port = 3000
|
||||||
|
force_https = true
|
||||||
|
auto_stop_machines = false
|
||||||
|
auto_start_machines = true
|
||||||
|
min_machines_running = 1
|
||||||
|
processes = ["app"]
|
||||||
|
|
||||||
|
[[vm]]
|
||||||
|
size = "shared-cpu-2x"
|
||||||
|
memory = "2048mb"
|
||||||
|
|
||||||
|
[mounts]
|
||||||
|
source = "clawdbot_data"
|
||||||
|
destination = "/data"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key settings:**
|
||||||
|
|
||||||
|
| Setting | Why |
|
||||||
|
|---------|-----|
|
||||||
|
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
|
||||||
|
| `--allow-unconfigured` | Starts without a config file (you'll create one after) |
|
||||||
|
| `memory = "2048mb"` | 512MB is too small; 2GB recommended |
|
||||||
|
| `CLAWDBOT_STATE_DIR = "/data"` | Persists state on the volume |
|
||||||
|
|
||||||
|
## 3) Set secrets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required: Gateway token (for non-loopback binding)
|
||||||
|
fly secrets set CLAWDBOT_GATEWAY_TOKEN=$(openssl rand -hex 32)
|
||||||
|
|
||||||
|
# Model provider API keys
|
||||||
|
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
|
||||||
|
# Optional: Other providers
|
||||||
|
fly secrets set OPENAI_API_KEY=sk-...
|
||||||
|
fly secrets set GOOGLE_API_KEY=...
|
||||||
|
|
||||||
|
# Channel tokens
|
||||||
|
fly secrets set DISCORD_BOT_TOKEN=MTQ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Non-loopback binds (`--bind lan`) require `CLAWDBOT_GATEWAY_TOKEN` for security.
|
||||||
|
- Treat these tokens like passwords.
|
||||||
|
|
||||||
|
## 4) Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fly deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
|
||||||
|
|
||||||
|
After deployment, verify:
|
||||||
|
```bash
|
||||||
|
fly status
|
||||||
|
fly logs
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
```
|
||||||
|
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
|
||||||
|
[discord] logged in to discord as xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) Create config file
|
||||||
|
|
||||||
|
SSH into the machine to create a proper config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fly ssh console
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the config directory and file:
|
||||||
|
```bash
|
||||||
|
mkdir -p /data/.clawdbot
|
||||||
|
cat > /data/.clawdbot/clawdbot.json << 'EOF'
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"model": {
|
||||||
|
"primary": "anthropic/claude-opus-4-5"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"anthropic/claude-sonnet-4-5": {}
|
||||||
|
},
|
||||||
|
"maxConcurrent": 4
|
||||||
|
},
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": "main",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"discord": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart to apply:
|
||||||
|
```bash
|
||||||
|
exit
|
||||||
|
fly machine restart <machine-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6) Access the Gateway
|
||||||
|
|
||||||
|
### Control UI
|
||||||
|
|
||||||
|
Open in browser:
|
||||||
|
```bash
|
||||||
|
fly open
|
||||||
|
```
|
||||||
|
|
||||||
|
Or visit `https://my-clawdbot.fly.dev/`
|
||||||
|
|
||||||
|
Paste your gateway token (the one from `CLAWDBOT_GATEWAY_TOKEN`) to authenticate.
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fly logs # Live logs
|
||||||
|
fly logs --no-tail # Recent logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH Console
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fly ssh console
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "App is not listening on expected address"
|
||||||
|
|
||||||
|
The gateway is binding to `127.0.0.1` instead of `0.0.0.0`.
|
||||||
|
|
||||||
|
**Fix:** Add `--bind lan` to your process command in `fly.toml`.
|
||||||
|
|
||||||
|
### OOM / Memory Issues
|
||||||
|
|
||||||
|
Container keeps restarting or getting killed.
|
||||||
|
|
||||||
|
**Fix:** Increase memory in `fly.toml`:
|
||||||
|
```toml
|
||||||
|
[[vm]]
|
||||||
|
memory = "2048mb"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gateway Lock Issues
|
||||||
|
|
||||||
|
Gateway refuses to start with "already running" errors.
|
||||||
|
|
||||||
|
This happens when the container restarts but the PID lock file persists on the volume.
|
||||||
|
|
||||||
|
**Fix:** Delete the lock file:
|
||||||
|
```bash
|
||||||
|
fly ssh console
|
||||||
|
rm /data/.clawdbot/run/gateway.*.lock
|
||||||
|
exit
|
||||||
|
fly machine restart <machine-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config Not Being Read
|
||||||
|
|
||||||
|
If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/.clawdbot/clawdbot.json` should be read on restart.
|
||||||
|
|
||||||
|
Verify the config exists:
|
||||||
|
```bash
|
||||||
|
fly ssh console --command "cat /data/.clawdbot/clawdbot.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest changes
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Redeploy
|
||||||
|
fly deploy
|
||||||
|
|
||||||
|
# Check health
|
||||||
|
fly status
|
||||||
|
fly logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Fly.io uses **x86 architecture** (not ARM)
|
||||||
|
- The Dockerfile is compatible with both architectures
|
||||||
|
- For WhatsApp/Telegram onboarding, use `fly ssh console`
|
||||||
|
- Persistent data lives on the volume at `/data`
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
With the recommended config (`shared-cpu-2x`, 2GB RAM):
|
||||||
|
- ~$10-15/month depending on usage
|
||||||
|
- Free tier includes some allowance
|
||||||
|
|
||||||
|
See [Fly.io pricing](https://fly.io/docs/about/pricing/) for details.
|
||||||
@@ -184,7 +184,7 @@ services:
|
|||||||
[
|
[
|
||||||
"node",
|
"node",
|
||||||
"dist/index.js",
|
"dist/index.js",
|
||||||
"gateway-daemon",
|
"gateway",
|
||||||
"--bind",
|
"--bind",
|
||||||
"${CLAWDBOT_GATEWAY_BIND}",
|
"${CLAWDBOT_GATEWAY_BIND}",
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
|||||||
|
|
||||||
## VPS & hosting
|
## VPS & hosting
|
||||||
|
|
||||||
|
- Fly.io: [Fly.io](/platforms/fly)
|
||||||
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
||||||
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ Plugins can register:
|
|||||||
- Background services
|
- Background services
|
||||||
- Optional config validation
|
- Optional config validation
|
||||||
- **Skills** (by listing `skills` directories in the plugin manifest)
|
- **Skills** (by listing `skills` directories in the plugin manifest)
|
||||||
|
- **Auto-reply commands** (execute without invoking the AI agent)
|
||||||
|
|
||||||
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
||||||
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
||||||
@@ -494,6 +495,65 @@ export default function (api) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Register auto-reply commands
|
||||||
|
|
||||||
|
Plugins can register custom slash commands that execute **without invoking the
|
||||||
|
AI agent**. This is useful for toggle commands, status checks, or quick actions
|
||||||
|
that don't need LLM processing.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default function (api) {
|
||||||
|
api.registerCommand({
|
||||||
|
name: "mystatus",
|
||||||
|
description: "Show plugin status",
|
||||||
|
handler: (ctx) => ({
|
||||||
|
text: `Plugin is running! Channel: ${ctx.channel}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Command handler context:
|
||||||
|
|
||||||
|
- `senderId`: The sender's ID (if available)
|
||||||
|
- `channel`: The channel where the command was sent
|
||||||
|
- `isAuthorizedSender`: Whether the sender is an authorized user
|
||||||
|
- `args`: Arguments passed after the command (if `acceptsArgs: true`)
|
||||||
|
- `commandBody`: The full command text
|
||||||
|
- `config`: The current Clawdbot config
|
||||||
|
|
||||||
|
Command options:
|
||||||
|
|
||||||
|
- `name`: Command name (without the leading `/`)
|
||||||
|
- `description`: Help text shown in command lists
|
||||||
|
- `acceptsArgs`: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlers
|
||||||
|
- `requireAuth`: Whether to require authorized sender (default: true)
|
||||||
|
- `handler`: Function that returns `{ text: string }` (can be async)
|
||||||
|
|
||||||
|
Example with authorization and arguments:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
api.registerCommand({
|
||||||
|
name: "setmode",
|
||||||
|
description: "Set plugin mode",
|
||||||
|
acceptsArgs: true,
|
||||||
|
requireAuth: true,
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const mode = ctx.args?.trim() || "default";
|
||||||
|
await saveMode(mode);
|
||||||
|
return { text: `Mode set to: ${mode}` };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Plugin commands are processed **before** built-in commands and the AI agent
|
||||||
|
- Commands are registered globally and work across all channels
|
||||||
|
- Command names are case-insensitive (`/MyStatus` matches `/mystatus`)
|
||||||
|
- Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
|
||||||
|
- Reserved command names (like `help`, `status`, `reset`, etc.) cannot be overridden by plugins
|
||||||
|
- Duplicate command registration across plugins will fail with a diagnostic error
|
||||||
|
|
||||||
### Register background services
|
### Register background services
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -112,6 +112,23 @@ Default heartbeat prompt:
|
|||||||
|
|
||||||
You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small to limit token burn.
|
You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small to limit token burn.
|
||||||
|
|
||||||
|
### Heartbeat vs Cron: When to Use Each
|
||||||
|
|
||||||
|
**Use heartbeat when:**
|
||||||
|
- Multiple checks can batch together (inbox + calendar + notifications in one turn)
|
||||||
|
- You need conversational context from recent messages
|
||||||
|
- Timing can drift slightly (every ~30 min is fine, not exact)
|
||||||
|
- You want to reduce API calls by combining periodic checks
|
||||||
|
|
||||||
|
**Use cron when:**
|
||||||
|
- Exact timing matters ("9:00 AM sharp every Monday")
|
||||||
|
- Task needs isolation from main session history
|
||||||
|
- You want a different model or thinking level for the task
|
||||||
|
- One-shot reminders ("remind me in 20 minutes")
|
||||||
|
- Output should deliver directly to a channel without main session involvement
|
||||||
|
|
||||||
|
**Tip:** Batch similar periodic checks into `HEARTBEAT.md` instead of creating multiple cron jobs. Use cron for precise schedules and standalone tasks.
|
||||||
|
|
||||||
**Things to check (rotate through these, 2-4 times per day):**
|
**Things to check (rotate through these, 2-4 times per day):**
|
||||||
- **Emails** - Any urgent unread messages?
|
- **Emails** - Any urgent unread messages?
|
||||||
- **Calendar** - Upcoming events in next 24-48h?
|
- **Calendar** - Upcoming events in next 24-48h?
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ read_when:
|
|||||||
---
|
---
|
||||||
# HEARTBEAT.md
|
# HEARTBEAT.md
|
||||||
|
|
||||||
Keep this file empty unless you want a tiny checklist. Keep it small.
|
# 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.
|
||||||
@@ -7,11 +7,16 @@ read_when:
|
|||||||
|
|
||||||
*Fill this in during your first conversation. Make it yours.*
|
*Fill this in during your first conversation. Make it yours.*
|
||||||
|
|
||||||
- **Name:** *(pick something you like)*
|
- **Name:**
|
||||||
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
*(pick something you like)*
|
||||||
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
- **Creature:**
|
||||||
- **Emoji:** *(your signature — pick one that feels right)*
|
*(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||||
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
|
- **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)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ Implementation:
|
|||||||
|
|
||||||
**OpenAI / OpenAI Codex**
|
**OpenAI / OpenAI Codex**
|
||||||
- Image sanitization only.
|
- Image sanitization only.
|
||||||
|
- On model switch into OpenAI Responses/Codex, drop orphaned reasoning signatures (standalone reasoning items without a following content block).
|
||||||
- No tool call id sanitization.
|
- No tool call id sanitization.
|
||||||
- No tool result pairing repair.
|
- No tool result pairing repair.
|
||||||
- No turn validation or reordering.
|
- No turn validation or reordering.
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ By default, Clawdbot runs a heartbeat every 30 minutes with the prompt:
|
|||||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||||
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||||
|
|
||||||
|
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||||
|
- If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||||
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
||||||
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
||||||
|
|
||||||
|
|||||||
@@ -971,6 +971,10 @@ Heartbeats run every **30m** by default. Tune or disable them:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||||
|
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||||
|
If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||||
|
|
||||||
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
||||||
|
|
||||||
### Do I need to add a “bot account” to a WhatsApp group?
|
### Do I need to add a “bot account” to a WhatsApp group?
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
|||||||
- [Exec tool](/tools/exec)
|
- [Exec tool](/tools/exec)
|
||||||
- [Elevated mode](/tools/elevated)
|
- [Elevated mode](/tools/elevated)
|
||||||
- [Cron jobs](/automation/cron-jobs)
|
- [Cron jobs](/automation/cron-jobs)
|
||||||
|
- [Cron vs Heartbeat](/automation/cron-vs-heartbeat)
|
||||||
- [Thinking + verbose](/tools/thinking)
|
- [Thinking + verbose](/tools/thinking)
|
||||||
- [Models](/concepts/models)
|
- [Models](/concepts/models)
|
||||||
- [Sub-agents](/tools/subagents)
|
- [Sub-agents](/tools/subagents)
|
||||||
|
|||||||
@@ -327,14 +327,8 @@ Full setup walkthrough (28m) by VelvetShark.
|
|||||||
|
|
||||||
<Card title="OpenRouter Transcription" icon="microphone" href="https://clawdhub.com/obviyus/openrouter-transcribe">
|
<Card title="OpenRouter Transcription" icon="microphone" href="https://clawdhub.com/obviyus/openrouter-transcribe">
|
||||||
**@obviyus** • `transcription` `multilingual` `skill`
|
**@obviyus** • `transcription` `multilingual` `skill`
|
||||||
|
|
||||||
Multi-lingual audio transcription via OpenRouter (Gemini, etc). Available on ClawdHub.
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="Google Docs Editor" icon="file-word">
|
Multi-lingual audio transcription via OpenRouter (Gemini, etc). Available on ClawdHub.
|
||||||
**Community** • `docs` `editing` `skill`
|
|
||||||
|
|
||||||
Rich-text Google Docs editing skill. Built rapidly with Claude Code.
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -166,6 +166,19 @@ Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting
|
|||||||
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
||||||
tokens instead of committing them to config files.
|
tokens instead of committing them to config files.
|
||||||
|
|
||||||
|
### Node browser proxy (zero-config default)
|
||||||
|
|
||||||
|
If you run a **node host** on the machine that has your browser, Clawdbot can
|
||||||
|
auto-route browser tool calls to that node without any custom `controlUrl`
|
||||||
|
setup. This is the default path for remote gateways.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The node host exposes its local browser control server via a **proxy command**.
|
||||||
|
- Profiles come from the node’s own `browser.profiles` config (same as local).
|
||||||
|
- Disable if you don’t want it:
|
||||||
|
- On the node: `nodeHost.browserProxy.enabled=false`
|
||||||
|
- On the gateway: `gateway.nodes.browser.mode="off"`
|
||||||
|
|
||||||
### Browserless (hosted remote CDP)
|
### Browserless (hosted remote CDP)
|
||||||
|
|
||||||
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Exec approvals are the **companion app / node host guardrail** for letting a san
|
|||||||
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
||||||
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
||||||
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
||||||
|
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used.
|
||||||
|
|
||||||
If the companion app UI is **not available**, any request that requires a prompt is
|
If the companion app UI is **not available**, any request that requires a prompt is
|
||||||
resolved by the **ask fallback** (default: deny).
|
resolved by the **ask fallback** (default: deny).
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Matching is case-insensitive.
|
||||||
|
- `*` wildcards are supported (`"*"` means all tools).
|
||||||
|
- If `tools.allow` only references unknown or unloaded plugin tool names, Clawdbot logs a warning and ignores the allowlist so core tools stay available.
|
||||||
|
|
||||||
## Tool profiles (base allowlist)
|
## Tool profiles (base allowlist)
|
||||||
|
|
||||||
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
||||||
@@ -156,6 +161,7 @@ alongside tools (for example, the voice-call plugin).
|
|||||||
|
|
||||||
Optional plugin tools:
|
Optional plugin tools:
|
||||||
- [Lobster](/tools/lobster): typed workflow runtime with resumable approvals (requires the Lobster CLI on the gateway host).
|
- [Lobster](/tools/lobster): typed workflow runtime with resumable approvals (requires the Lobster CLI on the gateway host).
|
||||||
|
- [LLM Task](/tools/llm-task): JSON-only LLM step for structured workflow output (optional schema validation).
|
||||||
|
|
||||||
## Tool inventory
|
## Tool inventory
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
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,6 +65,52 @@ gog.gmail.search --query 'newer_than:1d' \
|
|||||||
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
|
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## JSON-only LLM steps (llm-task)
|
||||||
|
|
||||||
|
For workflows that need a **structured LLM step**, enable the optional
|
||||||
|
`llm-task` plugin tool and call it from Lobster. This keeps the workflow
|
||||||
|
deterministic while still letting you classify/summarize/draft with a model.
|
||||||
|
|
||||||
|
Enable the tool:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"entries": {
|
||||||
|
"llm-task": { "enabled": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": "main",
|
||||||
|
"tools": { "allow": ["llm-task"] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use it in a pipeline:
|
||||||
|
|
||||||
|
```lobster
|
||||||
|
clawd.invoke --tool llm-task --action json --args-json '{
|
||||||
|
"prompt": "Given the input email, return intent and draft.",
|
||||||
|
"input": { "subject": "Hello", "body": "Can you help?" },
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"intent": { "type": "string" },
|
||||||
|
"draft": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["intent", "draft"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
See [LLM Task](/tools/llm-task) for details and configuration options.
|
||||||
|
|
||||||
## Workflow files (.lobster)
|
## Workflow files (.lobster)
|
||||||
|
|
||||||
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
|
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ Text + native (when enabled):
|
|||||||
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
|
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
|
||||||
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
|
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
|
||||||
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
||||||
|
- `/tts_on` (enable TTS replies)
|
||||||
|
- `/tts_off` (disable TTS replies)
|
||||||
|
- `/tts_provider [openai|elevenlabs]` (set or show TTS provider)
|
||||||
|
- `/tts_limit <chars>` (max chars before TTS summarization)
|
||||||
|
- `/tts_summary on|off` (toggle TTS auto-summary)
|
||||||
|
- `/tts_status` (show TTS status)
|
||||||
|
- `/audio <text>` (convert text to a TTS audio reply)
|
||||||
- `/stop`
|
- `/stop`
|
||||||
- `/restart`
|
- `/restart`
|
||||||
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
normalizeAccountId,
|
normalizeAccountId,
|
||||||
PAIRING_APPROVED_MESSAGE,
|
PAIRING_APPROVED_MESSAGE,
|
||||||
resolveBlueBubblesGroupRequireMention,
|
resolveBlueBubblesGroupRequireMention,
|
||||||
|
resolveBlueBubblesGroupToolPolicy,
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
||||||
|
resolveToolPolicy: resolveBlueBubblesGroupToolPolicy,
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -21,6 +21,7 @@ const bluebubblesActionSchema = z
|
|||||||
|
|
||||||
const bluebubblesGroupConfigSchema = z.object({
|
const bluebubblesGroupConfigSchema = z.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
tools: ToolPolicySchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bluebubblesAccountSchema = z.object({
|
const bluebubblesAccountSchema = z.object({
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export type GroupPolicy = "open" | "disabled" | "allowlist";
|
|||||||
export type BlueBubblesGroupConfig = {
|
export type BlueBubblesGroupConfig = {
|
||||||
/** If true, only respond in this group when mentioned. */
|
/** If true, only respond in this group when mentioned. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
/** Optional tool policy overrides for this group. */
|
||||||
|
tools?: { allow?: string[]; deny?: string[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BlueBubblesAccountConfig = {
|
export type BlueBubblesAccountConfig = {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
resolveDiscordAccount,
|
resolveDiscordAccount,
|
||||||
resolveDefaultDiscordAccountId,
|
resolveDefaultDiscordAccountId,
|
||||||
resolveDiscordGroupRequireMention,
|
resolveDiscordGroupRequireMention,
|
||||||
|
resolveDiscordGroupToolPolicy,
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
type ChannelMessageActionAdapter,
|
type ChannelMessageActionAdapter,
|
||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
@@ -144,6 +145,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveDiscordGroupRequireMention,
|
resolveRequireMention: resolveDiscordGroupRequireMention,
|
||||||
|
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
stripPatterns: () => ["<@!?\\d+>"],
|
stripPatterns: () => ["<@!?\\d+>"],
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
resolveDefaultIMessageAccountId,
|
resolveDefaultIMessageAccountId,
|
||||||
resolveIMessageAccount,
|
resolveIMessageAccount,
|
||||||
resolveIMessageGroupRequireMention,
|
resolveIMessageGroupRequireMention,
|
||||||
|
resolveIMessageGroupToolPolicy,
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
type ResolvedIMessageAccount,
|
type ResolvedIMessageAccount,
|
||||||
@@ -106,6 +107,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||||
|
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
||||||
},
|
},
|
||||||
messaging: {
|
messaging: {
|
||||||
targetResolver: {
|
targetResolver: {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@clawdbot/llm-task",
|
||||||
|
"version": "2026.1.23",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Clawdbot JSON-only LLM task plugin",
|
||||||
|
"clawdbot": {
|
||||||
|
"extensions": [
|
||||||
|
"./index.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -25,9 +25,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||||
"clawdbot": "workspace:*",
|
|
||||||
"markdown-it": "14.1.0",
|
"markdown-it": "14.1.0",
|
||||||
"matrix-bot-sdk": "0.8.0",
|
"matrix-bot-sdk": "0.8.0",
|
||||||
"music-metadata": "^11.10.6"
|
"music-metadata": "^11.10.6",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"clawdbot": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
import { matrixMessageActions } from "./actions.js";
|
import { matrixMessageActions } from "./actions.js";
|
||||||
import { MatrixConfigSchema } from "./config-schema.js";
|
import { MatrixConfigSchema } from "./config-schema.js";
|
||||||
import { resolveMatrixGroupRequireMention } from "./group-mentions.js";
|
import { resolveMatrixGroupRequireMention, resolveMatrixGroupToolPolicy } from "./group-mentions.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
import {
|
import {
|
||||||
listMatrixAccountIds,
|
listMatrixAccountIds,
|
||||||
@@ -167,6 +167,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveMatrixGroupRequireMention,
|
resolveRequireMention: resolveMatrixGroupRequireMention,
|
||||||
|
resolveToolPolicy: resolveMatrixGroupToolPolicy,
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) =>
|
resolveReplyToMode: ({ cfg }) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -26,6 +26,7 @@ const matrixRoomSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
tools: ToolPolicySchema,
|
||||||
autoReply: z.boolean().optional(),
|
autoReply: z.boolean().optional(),
|
||||||
users: z.array(allowFromEntry).optional(),
|
users: z.array(allowFromEntry).optional(),
|
||||||
skills: z.array(z.string()).optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChannelGroupContext } from "clawdbot/plugin-sdk";
|
import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
@@ -32,3 +32,30 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveMatrixGroupToolPolicy(
|
||||||
|
params: ChannelGroupContext,
|
||||||
|
): GroupToolPolicyConfig | undefined {
|
||||||
|
const rawGroupId = params.groupId?.trim() ?? "";
|
||||||
|
let roomId = rawGroupId;
|
||||||
|
const lower = roomId.toLowerCase();
|
||||||
|
if (lower.startsWith("matrix:")) {
|
||||||
|
roomId = roomId.slice("matrix:".length).trim();
|
||||||
|
}
|
||||||
|
if (roomId.toLowerCase().startsWith("channel:")) {
|
||||||
|
roomId = roomId.slice("channel:".length).trim();
|
||||||
|
}
|
||||||
|
if (roomId.toLowerCase().startsWith("room:")) {
|
||||||
|
roomId = roomId.slice("room:".length).trim();
|
||||||
|
}
|
||||||
|
const groupChannel = params.groupChannel?.trim() ?? "";
|
||||||
|
const aliases = groupChannel ? [groupChannel] : [];
|
||||||
|
const cfg = params.cfg as CoreConfig;
|
||||||
|
const resolved = resolveMatrixRoomConfig({
|
||||||
|
rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
|
||||||
|
roomId,
|
||||||
|
aliases,
|
||||||
|
name: groupChannel || undefined,
|
||||||
|
}).config;
|
||||||
|
return resolved?.tools;
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ export type MatrixRoomConfig = {
|
|||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
/** Require mentioning the bot to trigger replies. */
|
/** Require mentioning the bot to trigger replies. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
/** Optional tool policy overrides for this room. */
|
||||||
|
tools?: { allow?: string[]; deny?: string[] };
|
||||||
/** If true, reply without mention requirements. */
|
/** If true, reply without mention requirements. */
|
||||||
autoReply?: boolean;
|
autoReply?: boolean;
|
||||||
/** Optional allowlist for room senders (user IDs or localparts). */
|
/** Optional allowlist for room senders (user IDs or localparts). */
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
||||||
import { msteamsOutbound } from "./outbound.js";
|
import { msteamsOutbound } from "./outbound.js";
|
||||||
import { probeMSTeams } from "./probe.js";
|
import { probeMSTeams } from "./probe.js";
|
||||||
|
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||||
import {
|
import {
|
||||||
normalizeMSTeamsMessagingTarget,
|
normalizeMSTeamsMessagingTarget,
|
||||||
normalizeMSTeamsUserInput,
|
normalizeMSTeamsUserInput,
|
||||||
@@ -77,6 +78,9 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
|||||||
hasRepliedRef,
|
hasRepliedRef,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
groups: {
|
||||||
|
resolveToolPolicy: resolveMSTeamsGroupToolPolicy,
|
||||||
|
},
|
||||||
reload: { configPrefixes: ["channels.msteams"] },
|
reload: { configPrefixes: ["channels.msteams"] },
|
||||||
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
AllowlistMatch,
|
AllowlistMatch,
|
||||||
|
ChannelGroupContext,
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
|
GroupToolPolicyConfig,
|
||||||
MSTeamsChannelConfig,
|
MSTeamsChannelConfig,
|
||||||
MSTeamsConfig,
|
MSTeamsConfig,
|
||||||
MSTeamsReplyStyle,
|
MSTeamsReplyStyle,
|
||||||
@@ -86,6 +88,50 @@ export function resolveMSTeamsRouteConfig(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveMSTeamsGroupToolPolicy(
|
||||||
|
params: ChannelGroupContext,
|
||||||
|
): GroupToolPolicyConfig | undefined {
|
||||||
|
const cfg = params.cfg.channels?.msteams;
|
||||||
|
if (!cfg) return undefined;
|
||||||
|
const groupId = params.groupId?.trim();
|
||||||
|
const groupChannel = params.groupChannel?.trim();
|
||||||
|
const groupSpace = params.groupSpace?.trim();
|
||||||
|
|
||||||
|
const resolved = resolveMSTeamsRouteConfig({
|
||||||
|
cfg,
|
||||||
|
teamId: groupSpace,
|
||||||
|
teamName: groupSpace,
|
||||||
|
conversationId: groupId,
|
||||||
|
channelName: groupChannel,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resolved.channelConfig) {
|
||||||
|
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools;
|
||||||
|
}
|
||||||
|
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
|
||||||
|
|
||||||
|
if (!groupId) return undefined;
|
||||||
|
|
||||||
|
const channelCandidates = buildChannelKeyCandidates(
|
||||||
|
groupId,
|
||||||
|
groupChannel,
|
||||||
|
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
||||||
|
);
|
||||||
|
for (const teamConfig of Object.values(cfg.teams ?? {})) {
|
||||||
|
const match = resolveChannelEntryMatchWithFallback({
|
||||||
|
entries: teamConfig?.channels ?? {},
|
||||||
|
keys: channelCandidates,
|
||||||
|
wildcardKey: "*",
|
||||||
|
normalizeKey: normalizeChannelSlug,
|
||||||
|
});
|
||||||
|
if (match.entry) {
|
||||||
|
return match.entry.tools ?? teamConfig?.tools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export type MSTeamsReplyPolicy = {
|
export type MSTeamsReplyPolicy = {
|
||||||
requireMention: boolean;
|
requireMention: boolean;
|
||||||
replyStyle: MSTeamsReplyStyle;
|
replyStyle: MSTeamsReplyStyle;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
|
|||||||
try {
|
try {
|
||||||
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
||||||
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
|
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
|
||||||
await tokenProvider.getAccessToken("https://api.botframework.com/.default");
|
await tokenProvider.getAccessToken("https://api.botframework.com");
|
||||||
let graph:
|
let graph:
|
||||||
| {
|
| {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
|
|||||||
import { getNextcloudTalkRuntime } from "./runtime.js";
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
||||||
import { sendMessageNextcloudTalk } from "./send.js";
|
import { sendMessageNextcloudTalk } from "./send.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
|
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
id: "nextcloud-talk",
|
id: "nextcloud-talk",
|
||||||
@@ -159,6 +160,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
|
||||||
},
|
},
|
||||||
messaging: {
|
messaging: {
|
||||||
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
DmPolicySchema,
|
DmPolicySchema,
|
||||||
GroupPolicySchema,
|
GroupPolicySchema,
|
||||||
MarkdownConfigSchema,
|
MarkdownConfigSchema,
|
||||||
|
ToolPolicySchema,
|
||||||
requireOpenAllowFrom,
|
requireOpenAllowFrom,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -11,6 +12,7 @@ import { z } from "zod";
|
|||||||
export const NextcloudTalkRoomSchema = z
|
export const NextcloudTalkRoomSchema = z
|
||||||
.object({
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
tools: ToolPolicySchema,
|
||||||
skills: z.array(z.string()).optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allowFrom: z.array(z.string()).optional(),
|
allowFrom: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AllowlistMatch, GroupPolicy } from "clawdbot/plugin-sdk";
|
import type { AllowlistMatch, ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
||||||
import {
|
import {
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
normalizeChannelSlug,
|
normalizeChannelSlug,
|
||||||
@@ -86,6 +86,21 @@ export function resolveNextcloudTalkRoomMatch(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveNextcloudTalkGroupToolPolicy(
|
||||||
|
params: ChannelGroupContext,
|
||||||
|
): GroupToolPolicyConfig | undefined {
|
||||||
|
const cfg = params.cfg as { channels?: { "nextcloud-talk"?: { rooms?: Record<string, NextcloudTalkRoomConfig> } } };
|
||||||
|
const roomToken = params.groupId?.trim();
|
||||||
|
if (!roomToken) return undefined;
|
||||||
|
const roomName = params.groupChannel?.trim() || undefined;
|
||||||
|
const match = resolveNextcloudTalkRoomMatch({
|
||||||
|
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
|
||||||
|
roomToken,
|
||||||
|
roomName,
|
||||||
|
});
|
||||||
|
return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveNextcloudTalkRequireMention(params: {
|
export function resolveNextcloudTalkRequireMention(params: {
|
||||||
roomConfig?: NextcloudTalkRoomConfig;
|
roomConfig?: NextcloudTalkRoomConfig;
|
||||||
wildcardConfig?: NextcloudTalkRoomConfig;
|
wildcardConfig?: NextcloudTalkRoomConfig;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type {
|
|||||||
|
|
||||||
export type NextcloudTalkRoomConfig = {
|
export type NextcloudTalkRoomConfig = {
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
|
/** Optional tool policy overrides for this room. */
|
||||||
|
tools?: { allow?: string[]; deny?: string[] };
|
||||||
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
|
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
/** If false, disable the bot for this room. */
|
/** If false, disable the bot for this room. */
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
resolveSlackAccount,
|
resolveSlackAccount,
|
||||||
resolveSlackReplyToMode,
|
resolveSlackReplyToMode,
|
||||||
resolveSlackGroupRequireMention,
|
resolveSlackGroupRequireMention,
|
||||||
|
resolveSlackGroupToolPolicy,
|
||||||
buildSlackThreadingToolContext,
|
buildSlackThreadingToolContext,
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
slackOnboardingAdapter,
|
slackOnboardingAdapter,
|
||||||
@@ -161,6 +162,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveSlackGroupRequireMention,
|
resolveRequireMention: resolveSlackGroupRequireMention,
|
||||||
|
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
resolveDefaultTelegramAccountId,
|
resolveDefaultTelegramAccountId,
|
||||||
resolveTelegramAccount,
|
resolveTelegramAccount,
|
||||||
resolveTelegramGroupRequireMention,
|
resolveTelegramGroupRequireMention,
|
||||||
|
resolveTelegramGroupToolPolicy,
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
telegramOnboardingAdapter,
|
telegramOnboardingAdapter,
|
||||||
TelegramConfigSchema,
|
TelegramConfigSchema,
|
||||||
@@ -154,6 +155,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||||
|
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
resolveDefaultWhatsAppAccountId,
|
resolveDefaultWhatsAppAccountId,
|
||||||
resolveWhatsAppAccount,
|
resolveWhatsAppAccount,
|
||||||
resolveWhatsAppGroupRequireMention,
|
resolveWhatsAppGroupRequireMention,
|
||||||
|
resolveWhatsAppGroupToolPolicy,
|
||||||
resolveWhatsAppHeartbeatRecipients,
|
resolveWhatsAppHeartbeatRecipients,
|
||||||
whatsappOnboardingAdapter,
|
whatsappOnboardingAdapter,
|
||||||
WhatsAppConfigSchema,
|
WhatsAppConfigSchema,
|
||||||
@@ -198,6 +199,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
||||||
|
resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
|
||||||
resolveGroupIntroHint: () =>
|
resolveGroupIntroHint: () =>
|
||||||
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import type {
|
|||||||
ChannelAccountSnapshot,
|
ChannelAccountSnapshot,
|
||||||
ChannelDirectoryEntry,
|
ChannelDirectoryEntry,
|
||||||
ChannelDock,
|
ChannelDock,
|
||||||
|
ChannelGroupContext,
|
||||||
ChannelPlugin,
|
ChannelPlugin,
|
||||||
ClawdbotConfig,
|
ClawdbotConfig,
|
||||||
|
GroupToolPolicyConfig,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
import {
|
import {
|
||||||
applyAccountNameToChannelSection,
|
applyAccountNameToChannelSection,
|
||||||
@@ -79,6 +81,26 @@ function mapGroup(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveZalouserGroupToolPolicy(
|
||||||
|
params: ChannelGroupContext,
|
||||||
|
): GroupToolPolicyConfig | undefined {
|
||||||
|
const account = resolveZalouserAccountSync({
|
||||||
|
cfg: params.cfg as ClawdbotConfig,
|
||||||
|
accountId: params.accountId ?? undefined,
|
||||||
|
});
|
||||||
|
const groups = account.config.groups ?? {};
|
||||||
|
const groupId = params.groupId?.trim();
|
||||||
|
const groupChannel = params.groupChannel?.trim();
|
||||||
|
const candidates = [groupId, groupChannel, "*"].filter(
|
||||||
|
(value): value is string => Boolean(value),
|
||||||
|
);
|
||||||
|
for (const key of candidates) {
|
||||||
|
const entry = groups[key];
|
||||||
|
if (entry?.tools) return entry.tools;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const zalouserDock: ChannelDock = {
|
export const zalouserDock: ChannelDock = {
|
||||||
id: "zalouser",
|
id: "zalouser",
|
||||||
capabilities: {
|
capabilities: {
|
||||||
@@ -101,6 +123,7 @@ export const zalouserDock: ChannelDock = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: () => true,
|
resolveRequireMention: () => true,
|
||||||
|
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: () => "off",
|
resolveReplyToMode: () => "off",
|
||||||
@@ -188,6 +211,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: () => true,
|
resolveRequireMention: () => true,
|
||||||
|
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: () => "off",
|
resolveReplyToMode: () => "off",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -6,6 +6,7 @@ const allowFromEntry = z.union([z.string(), z.number()]);
|
|||||||
const groupConfigSchema = z.object({
|
const groupConfigSchema = z.object({
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
tools: ToolPolicySchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zalouserAccountSchema = z.object({
|
const zalouserAccountSchema = z.object({
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export type ZalouserAccountConfig = {
|
|||||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||||
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
||||||
messagePrefix?: string;
|
messagePrefix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ export type ZalouserConfig = {
|
|||||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||||
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
||||||
messagePrefix?: string;
|
messagePrefix?: string;
|
||||||
accounts?: Record<string, ZalouserAccountConfig>;
|
accounts?: Record<string, ZalouserAccountConfig>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Clawdbot Fly.io deployment configuration
|
||||||
|
# See https://fly.io/docs/reference/configuration/
|
||||||
|
|
||||||
|
app = "clawdbot"
|
||||||
|
primary_region = "lhr" # London
|
||||||
|
|
||||||
|
[build]
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
NODE_ENV = "production"
|
||||||
|
# Fly uses x86, but keep this for consistency
|
||||||
|
CLAWDBOT_PREFER_PNPM = "1"
|
||||||
|
|
||||||
|
[http_service]
|
||||||
|
internal_port = 3000
|
||||||
|
force_https = true
|
||||||
|
auto_stop_machines = false # Keep running for persistent connections
|
||||||
|
auto_start_machines = true
|
||||||
|
min_machines_running = 1
|
||||||
|
|
||||||
|
[[vm]]
|
||||||
|
size = "shared-cpu-1x"
|
||||||
|
memory = "512mb"
|
||||||
|
|
||||||
|
[mounts]
|
||||||
|
source = "clawdbot_data"
|
||||||
|
destination = "/data"
|
||||||
@@ -147,6 +147,7 @@
|
|||||||
"packageManager": "pnpm@10.23.0",
|
"packageManager": "pnpm@10.23.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "0.13.0",
|
"@agentclientprotocol/sdk": "0.13.0",
|
||||||
|
"@aws-sdk/client-bedrock": "^3.975.0",
|
||||||
"@buape/carbon": "0.14.0",
|
"@buape/carbon": "0.14.0",
|
||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@grammyjs/runner": "^2.0.3",
|
"@grammyjs/runner": "^2.0.3",
|
||||||
|
|||||||
Generated
+586
-5
@@ -16,6 +16,9 @@ importers:
|
|||||||
'@agentclientprotocol/sdk':
|
'@agentclientprotocol/sdk':
|
||||||
specifier: 0.13.0
|
specifier: 0.13.0
|
||||||
version: 0.13.0(zod@4.3.5)
|
version: 0.13.0(zod@4.3.5)
|
||||||
|
'@aws-sdk/client-bedrock':
|
||||||
|
specifier: ^3.975.0
|
||||||
|
version: 3.975.0
|
||||||
'@buape/carbon':
|
'@buape/carbon':
|
||||||
specifier: 0.14.0
|
specifier: 0.14.0
|
||||||
version: 0.14.0(hono@4.11.4)
|
version: 0.14.0(hono@4.11.4)
|
||||||
@@ -301,6 +304,8 @@ importers:
|
|||||||
|
|
||||||
extensions/imessage: {}
|
extensions/imessage: {}
|
||||||
|
|
||||||
|
extensions/llm-task: {}
|
||||||
|
|
||||||
extensions/lobster: {}
|
extensions/lobster: {}
|
||||||
|
|
||||||
extensions/matrix:
|
extensions/matrix:
|
||||||
@@ -308,9 +313,6 @@ importers:
|
|||||||
'@matrix-org/matrix-sdk-crypto-nodejs':
|
'@matrix-org/matrix-sdk-crypto-nodejs':
|
||||||
specifier: ^0.4.0
|
specifier: ^0.4.0
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
clawdbot:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../..
|
|
||||||
markdown-it:
|
markdown-it:
|
||||||
specifier: 14.1.0
|
specifier: 14.1.0
|
||||||
version: 14.1.0
|
version: 14.1.0
|
||||||
@@ -320,6 +322,13 @@ importers:
|
|||||||
music-metadata:
|
music-metadata:
|
||||||
specifier: ^11.10.6
|
specifier: ^11.10.6
|
||||||
version: 11.10.6
|
version: 11.10.6
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.5
|
||||||
|
version: 4.3.5
|
||||||
|
devDependencies:
|
||||||
|
clawdbot:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../..
|
||||||
|
|
||||||
extensions/mattermost: {}
|
extensions/mattermost: {}
|
||||||
|
|
||||||
@@ -384,6 +393,8 @@ importers:
|
|||||||
|
|
||||||
extensions/telegram: {}
|
extensions/telegram: {}
|
||||||
|
|
||||||
|
extensions/telegram-tts: {}
|
||||||
|
|
||||||
extensions/tlon:
|
extensions/tlon:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@urbit/aura':
|
'@urbit/aura':
|
||||||
@@ -493,46 +504,90 @@ packages:
|
|||||||
resolution: {integrity: sha512-rzSuqgMkL488bR9TnZEALBa+SV1FfR3B7CkYvs6R5uZm2AqBMfq7xNZR/pgMiAH/YLlI9FWAh1aPmdnG7iXxnA==}
|
resolution: {integrity: sha512-rzSuqgMkL488bR9TnZEALBa+SV1FfR3B7CkYvs6R5uZm2AqBMfq7xNZR/pgMiAH/YLlI9FWAh1aPmdnG7iXxnA==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/client-sso@3.972.0':
|
||||||
resolution: {integrity: sha512-5qw6qLiRE4SUiz0hWy878dSR13tSVhbTWhsvFT8mGHe37NRRiaobm5MA2sWD0deRAuO98djSiV+dhWXa1xIFNw==}
|
resolution: {integrity: sha512-5qw6qLiRE4SUiz0hWy878dSR13tSVhbTWhsvFT8mGHe37NRRiaobm5MA2sWD0deRAuO98djSiV+dhWXa1xIFNw==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/core@3.972.0':
|
||||||
resolution: {integrity: sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==}
|
resolution: {integrity: sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-env@3.972.0':
|
||||||
resolution: {integrity: sha512-kKHoNv+maHlPQOAhYamhap0PObd16SAb3jwaY0KYgNTiSbeXlbGUZPLioo9oA3wU10zItJzx83ClU7d7h40luA==}
|
resolution: {integrity: sha512-kKHoNv+maHlPQOAhYamhap0PObd16SAb3jwaY0KYgNTiSbeXlbGUZPLioo9oA3wU10zItJzx83ClU7d7h40luA==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-http@3.972.0':
|
||||||
resolution: {integrity: sha512-xzEi81L7I5jGUbpmqEHCe7zZr54hCABdj4H+3LzktHYuovV/oqnvoDdvZpGFR0e/KAw1+PL38NbGrpG30j6qlA==}
|
resolution: {integrity: sha512-xzEi81L7I5jGUbpmqEHCe7zZr54hCABdj4H+3LzktHYuovV/oqnvoDdvZpGFR0e/KAw1+PL38NbGrpG30j6qlA==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-ini@3.972.0':
|
||||||
resolution: {integrity: sha512-ruhAMceUIq2aknFd3jhWxmO0P0Efab5efjyIXOkI9i80g+zDY5VekeSxfqRKStEEJSKSCHDLQuOu0BnAn4Rzew==}
|
resolution: {integrity: sha512-ruhAMceUIq2aknFd3jhWxmO0P0Efab5efjyIXOkI9i80g+zDY5VekeSxfqRKStEEJSKSCHDLQuOu0BnAn4Rzew==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-login@3.972.0':
|
||||||
resolution: {integrity: sha512-SsrsFJsEYAJHO4N/r2P0aK6o8si6f1lprR+Ej8J731XJqTckSGs/HFHcbxOyW/iKt+LNUvZa59/VlJmjhF4bEQ==}
|
resolution: {integrity: sha512-SsrsFJsEYAJHO4N/r2P0aK6o8si6f1lprR+Ej8J731XJqTckSGs/HFHcbxOyW/iKt+LNUvZa59/VlJmjhF4bEQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-node@3.972.0':
|
||||||
resolution: {integrity: sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==}
|
resolution: {integrity: sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-process@3.972.0':
|
||||||
resolution: {integrity: sha512-nmzYhamLDJ8K+v3zWck79IaKMc350xZnWsf/GeaXO6E3MewSzd3lYkTiMi7lEp3/UwDm9NHfPguoPm+mhlSWQQ==}
|
resolution: {integrity: sha512-nmzYhamLDJ8K+v3zWck79IaKMc350xZnWsf/GeaXO6E3MewSzd3lYkTiMi7lEp3/UwDm9NHfPguoPm+mhlSWQQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-sso@3.972.0':
|
||||||
resolution: {integrity: sha512-6mYyfk1SrMZ15cH9T53yAF4YSnvq4yU1Xlgm3nqV1gZVQzmF5kr4t/F3BU3ygbvzi4uSwWxG3I3TYYS5eMlAyg==}
|
resolution: {integrity: sha512-6mYyfk1SrMZ15cH9T53yAF4YSnvq4yU1Xlgm3nqV1gZVQzmF5kr4t/F3BU3ygbvzi4uSwWxG3I3TYYS5eMlAyg==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/credential-provider-web-identity@3.972.0':
|
||||||
resolution: {integrity: sha512-vsJXBGL8H54kz4T6do3p5elATj5d1izVGUXMluRJntm9/I0be/zUYtdd4oDTM2kSUmd4Zhyw3fMQ9lw7CVhd4A==}
|
resolution: {integrity: sha512-vsJXBGL8H54kz4T6do3p5elATj5d1izVGUXMluRJntm9/I0be/zUYtdd4oDTM2kSUmd4Zhyw3fMQ9lw7CVhd4A==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/eventstream-handler-node@3.972.0':
|
||||||
resolution: {integrity: sha512-B1AEv+TQOVxg2t60GMfrcagJvQjpx1p6UASUoFMLevV9K3WNI5qYTjtutMiifKY0HwK6g86zXgN/dpeaSi3q5Q==}
|
resolution: {integrity: sha512-B1AEv+TQOVxg2t60GMfrcagJvQjpx1p6UASUoFMLevV9K3WNI5qYTjtutMiifKY0HwK6g86zXgN/dpeaSi3q5Q==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -545,18 +600,34 @@ packages:
|
|||||||
resolution: {integrity: sha512-3eztFI6F9/eHtkIaWKN3nT+PM+eQ6p1MALDuNshFk323ixuCZzOOVT8oUqtZa30Z6dycNXJwhlIq7NhUVFfimw==}
|
resolution: {integrity: sha512-3eztFI6F9/eHtkIaWKN3nT+PM+eQ6p1MALDuNshFk323ixuCZzOOVT8oUqtZa30Z6dycNXJwhlIq7NhUVFfimw==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/middleware-logger@3.972.0':
|
||||||
resolution: {integrity: sha512-ZvdyVRwzK+ra31v1pQrgbqR/KsLD+wwJjHgko6JfoKUBIcEfAwJzQKO6HspHxdHWTVUz6MgvwskheR/TTYZl2g==}
|
resolution: {integrity: sha512-ZvdyVRwzK+ra31v1pQrgbqR/KsLD+wwJjHgko6JfoKUBIcEfAwJzQKO6HspHxdHWTVUz6MgvwskheR/TTYZl2g==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/middleware-recursion-detection@3.972.0':
|
||||||
resolution: {integrity: sha512-F2SmUeO+S6l1h6dydNet3BQIk173uAkcfU1HDkw/bUdRLAnh15D3HP9vCZ7oCPBNcdEICbXYDmx0BR9rRUHGlQ==}
|
resolution: {integrity: sha512-F2SmUeO+S6l1h6dydNet3BQIk173uAkcfU1HDkw/bUdRLAnh15D3HP9vCZ7oCPBNcdEICbXYDmx0BR9rRUHGlQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/middleware-user-agent@3.972.0':
|
||||||
resolution: {integrity: sha512-kFHQm2OCBJCzGWRafgdWHGFjitUXY/OxXngymcX4l8CiyiNDZB27HDDBg2yLj3OUJc4z4fexLMmP8r9vgag19g==}
|
resolution: {integrity: sha512-kFHQm2OCBJCzGWRafgdWHGFjitUXY/OxXngymcX4l8CiyiNDZB27HDDBg2yLj3OUJc4z4fexLMmP8r9vgag19g==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/middleware-websocket@3.972.0':
|
||||||
resolution: {integrity: sha512-3pvbb/HtE7A8U38jk24RQ9T92d40NNSzjDEVEkBYZYhxExVcJ/Lk5Z+NM283FEtoi1T++oYrLuYDr1CIQxnaXQ==}
|
resolution: {integrity: sha512-3pvbb/HtE7A8U38jk24RQ9T92d40NNSzjDEVEkBYZYhxExVcJ/Lk5Z+NM283FEtoi1T++oYrLuYDr1CIQxnaXQ==}
|
||||||
engines: {node: '>= 14.0.0'}
|
engines: {node: '>= 14.0.0'}
|
||||||
@@ -565,18 +636,42 @@ packages:
|
|||||||
resolution: {integrity: sha512-QGlbnuGzSQJVG6bR9Qw6G0Blh6abFR4VxNa61ttMbzy9jt28xmk2iGtrYLrQPlCCPhY6enHqjTWm3n3LOb0wAw==}
|
resolution: {integrity: sha512-QGlbnuGzSQJVG6bR9Qw6G0Blh6abFR4VxNa61ttMbzy9jt28xmk2iGtrYLrQPlCCPhY6enHqjTWm3n3LOb0wAw==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/region-config-resolver@3.972.0':
|
||||||
resolution: {integrity: sha512-JyOf+R/6vJW8OEVFCAyzEOn2reri/Q+L0z9zx4JQSKWvTmJ1qeFO25sOm8VIfB8URKhfGRTQF30pfYaH2zxt/A==}
|
resolution: {integrity: sha512-JyOf+R/6vJW8OEVFCAyzEOn2reri/Q+L0z9zx4JQSKWvTmJ1qeFO25sOm8VIfB8URKhfGRTQF30pfYaH2zxt/A==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/token-providers@3.972.0':
|
||||||
resolution: {integrity: sha512-kWlXG+y5nZhgXGEtb72Je+EvqepBPs8E3vZse//1PYLWs2speFqbGE/ywCXmzEJgHgVqSB/u/lqBvs5WlYmSqQ==}
|
resolution: {integrity: sha512-kWlXG+y5nZhgXGEtb72Je+EvqepBPs8E3vZse//1PYLWs2speFqbGE/ywCXmzEJgHgVqSB/u/lqBvs5WlYmSqQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/types@3.972.0':
|
||||||
resolution: {integrity: sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==}
|
resolution: {integrity: sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws-sdk/util-endpoints@3.972.0':
|
||||||
resolution: {integrity: sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==}
|
resolution: {integrity: sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -592,6 +687,9 @@ packages:
|
|||||||
'@aws-sdk/util-user-agent-browser@3.972.0':
|
'@aws-sdk/util-user-agent-browser@3.972.0':
|
||||||
resolution: {integrity: sha512-eOLdkQyoRbDgioTS3Orr7iVsVEutJyMZxvyZ6WAF95IrF0kfWx5Rd/KXnfbnG/VKa2CvjZiitWfouLzfVEyvJA==}
|
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':
|
'@aws-sdk/util-user-agent-node@3.972.0':
|
||||||
resolution: {integrity: sha512-GOy+AiSrE9kGiojiwlZvVVSXwylu4+fmP0MJfvras/MwP09RB/YtQuOVR1E0fKQc6OMwaTNBjgAbOEhxuWFbAw==}
|
resolution: {integrity: sha512-GOy+AiSrE9kGiojiwlZvVVSXwylu4+fmP0MJfvras/MwP09RB/YtQuOVR1E0fKQc6OMwaTNBjgAbOEhxuWFbAw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -601,10 +699,23 @@ packages:
|
|||||||
aws-crt:
|
aws-crt:
|
||||||
optional: true
|
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':
|
'@aws-sdk/xml-builder@3.972.0':
|
||||||
resolution: {integrity: sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==}
|
resolution: {integrity: sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==}
|
||||||
engines: {node: '>=20.0.0'}
|
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':
|
'@aws/lambda-invoke-store@0.2.3':
|
||||||
resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==}
|
resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -2221,6 +2332,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-bg2TfzgsERyETAxc/Ims/eJX8eAnIeTi4r4LHpMpfF/2NyO6RsWis0rjKcCPaGksljmOb23BZRiCeT/3NvwkXw==}
|
resolution: {integrity: sha512-bg2TfzgsERyETAxc/Ims/eJX8eAnIeTi4r4LHpMpfF/2NyO6RsWis0rjKcCPaGksljmOb23BZRiCeT/3NvwkXw==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@smithy/credential-provider-imds@4.2.8':
|
||||||
resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==}
|
resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -2273,10 +2388,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-kwWpNltpxrvPabnjEFvwSmA+66l6s2ReCvgVSzW/z92LU4T28fTdgZ18IdYRYOrisu2NMQ0jUndRScbO65A/zg==}
|
resolution: {integrity: sha512-kwWpNltpxrvPabnjEFvwSmA+66l6s2ReCvgVSzW/z92LU4T28fTdgZ18IdYRYOrisu2NMQ0jUndRScbO65A/zg==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@smithy/middleware-retry@4.4.26':
|
||||||
resolution: {integrity: sha512-ozZMoTAr+B2aVYfLYfkssFvc8ZV3p/vLpVQ7/k277xxUOA9ykSPe5obL2j6yHfbdrM/SZV7qj0uk/hSqavHrLw==}
|
resolution: {integrity: sha512-ozZMoTAr+B2aVYfLYfkssFvc8ZV3p/vLpVQ7/k277xxUOA9ykSPe5obL2j6yHfbdrM/SZV7qj0uk/hSqavHrLw==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@smithy/middleware-serde@4.2.9':
|
||||||
resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==}
|
resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -2325,6 +2448,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-6o804SCyHGMXAb5mFJ+iTy9kVKv7F91a9szN0J+9X6p8A0NrdpUxdaC57aye2ipQkP2C4IAqETEpGZ0Zj77Haw==}
|
resolution: {integrity: sha512-6o804SCyHGMXAb5mFJ+iTy9kVKv7F91a9szN0J+9X6p8A0NrdpUxdaC57aye2ipQkP2C4IAqETEpGZ0Zj77Haw==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@smithy/types@4.12.0':
|
||||||
resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==}
|
resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -2361,10 +2488,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-8ugoNMtss2dJHsXnqsibGPqoaafvWJPACmYKxJ4E6QWaDrixsAemmiMMAVbvwYadjR0H9G2+AlzsInSzRi8PSw==}
|
resolution: {integrity: sha512-8ugoNMtss2dJHsXnqsibGPqoaafvWJPACmYKxJ4E6QWaDrixsAemmiMMAVbvwYadjR0H9G2+AlzsInSzRi8PSw==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@smithy/util-defaults-mode-node@4.2.28':
|
||||||
resolution: {integrity: sha512-mjUdcP8h3E0K/XvNMi9oBXRV3DMCzeRiYIieZ1LQ7jq5tu6GH/GTWym7a1xIIE0pKSoLcpGsaImuQhGPSIJzAA==}
|
resolution: {integrity: sha512-mjUdcP8h3E0K/XvNMi9oBXRV3DMCzeRiYIieZ1LQ7jq5tu6GH/GTWym7a1xIIE0pKSoLcpGsaImuQhGPSIJzAA==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@smithy/util-endpoints@3.2.8':
|
||||||
resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==}
|
resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -5331,7 +5466,7 @@ snapshots:
|
|||||||
'@aws-crypto/sha256-js': 5.2.0
|
'@aws-crypto/sha256-js': 5.2.0
|
||||||
'@aws-crypto/supports-web-crypto': 5.2.0
|
'@aws-crypto/supports-web-crypto': 5.2.0
|
||||||
'@aws-crypto/util': 5.2.0
|
'@aws-crypto/util': 5.2.0
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.973.0
|
||||||
'@aws-sdk/util-locate-window': 3.965.3
|
'@aws-sdk/util-locate-window': 3.965.3
|
||||||
'@smithy/util-utf8': 2.3.0
|
'@smithy/util-utf8': 2.3.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -5339,7 +5474,7 @@ snapshots:
|
|||||||
'@aws-crypto/sha256-js@5.2.0':
|
'@aws-crypto/sha256-js@5.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/util': 5.2.0
|
'@aws-crypto/util': 5.2.0
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.973.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-crypto/supports-web-crypto@5.2.0':
|
'@aws-crypto/supports-web-crypto@5.2.0':
|
||||||
@@ -5404,6 +5539,51 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/client-sso@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha256-browser': 5.2.0
|
'@aws-crypto/sha256-browser': 5.2.0
|
||||||
@@ -5447,6 +5627,49 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/core@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.972.0
|
||||||
@@ -5463,6 +5686,22 @@ snapshots:
|
|||||||
'@smithy/util-utf8': 4.2.0
|
'@smithy/util-utf8': 4.2.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/credential-provider-env@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5471,6 +5710,14 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/credential-provider-http@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5484,6 +5731,19 @@ snapshots:
|
|||||||
'@smithy/util-stream': 4.5.10
|
'@smithy/util-stream': 4.5.10
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/credential-provider-ini@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5503,6 +5763,25 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/credential-provider-login@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5516,6 +5795,19 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/credential-provider-node@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/credential-provider-env': 3.972.0
|
'@aws-sdk/credential-provider-env': 3.972.0
|
||||||
@@ -5533,6 +5825,23 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/credential-provider-process@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5542,6 +5851,15 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/credential-provider-sso@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-sso': 3.972.0
|
'@aws-sdk/client-sso': 3.972.0
|
||||||
@@ -5555,6 +5873,19 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/credential-provider-web-identity@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5567,6 +5898,18 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/eventstream-handler-node@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.972.0
|
||||||
@@ -5588,12 +5931,25 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/middleware-logger@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.972.0
|
||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/middleware-recursion-detection@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.972.0
|
||||||
@@ -5602,6 +5958,14 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/middleware-user-agent@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5612,6 +5976,16 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/middleware-websocket@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.972.0
|
||||||
@@ -5668,6 +6042,92 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/region-config-resolver@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.972.0
|
||||||
@@ -5676,6 +6136,14 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/token-providers@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.972.0
|
'@aws-sdk/core': 3.972.0
|
||||||
@@ -5688,11 +6156,40 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- 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':
|
'@aws-sdk/types@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/util-endpoints@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.972.0
|
'@aws-sdk/types': 3.972.0
|
||||||
@@ -5719,6 +6216,13 @@ snapshots:
|
|||||||
bowser: 2.13.1
|
bowser: 2.13.1
|
||||||
tslib: 2.8.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':
|
'@aws-sdk/util-user-agent-node@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.0
|
'@aws-sdk/middleware-user-agent': 3.972.0
|
||||||
@@ -5727,12 +6231,26 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@aws-sdk/xml-builder@3.972.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
fast-xml-parser: 5.2.5
|
fast-xml-parser: 5.2.5
|
||||||
tslib: 2.8.1
|
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': {}
|
'@aws/lambda-invoke-store@0.2.3': {}
|
||||||
|
|
||||||
'@azure/abort-controller@2.1.2':
|
'@azure/abort-controller@2.1.2':
|
||||||
@@ -7302,6 +7820,19 @@ snapshots:
|
|||||||
'@smithy/uuid': 1.1.0
|
'@smithy/uuid': 1.1.0
|
||||||
tslib: 2.8.1
|
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':
|
'@smithy/credential-provider-imds@4.2.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/node-config-provider': 4.3.8
|
'@smithy/node-config-provider': 4.3.8
|
||||||
@@ -7385,6 +7916,17 @@ snapshots:
|
|||||||
'@smithy/util-middleware': 4.2.8
|
'@smithy/util-middleware': 4.2.8
|
||||||
tslib: 2.8.1
|
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':
|
'@smithy/middleware-retry@4.4.26':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/node-config-provider': 4.3.8
|
'@smithy/node-config-provider': 4.3.8
|
||||||
@@ -7397,6 +7939,18 @@ snapshots:
|
|||||||
'@smithy/uuid': 1.1.0
|
'@smithy/uuid': 1.1.0
|
||||||
tslib: 2.8.1
|
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':
|
'@smithy/middleware-serde@4.2.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/protocol-http': 5.3.8
|
'@smithy/protocol-http': 5.3.8
|
||||||
@@ -7474,6 +8028,16 @@ snapshots:
|
|||||||
'@smithy/util-stream': 4.5.10
|
'@smithy/util-stream': 4.5.10
|
||||||
tslib: 2.8.1
|
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':
|
'@smithy/types@4.12.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -7519,6 +8083,13 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@smithy/util-defaults-mode-node@4.2.28':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/config-resolver': 4.4.6
|
'@smithy/config-resolver': 4.4.6
|
||||||
@@ -7529,6 +8100,16 @@ snapshots:
|
|||||||
'@smithy/types': 4.12.0
|
'@smithy/types': 4.12.0
|
||||||
tslib: 2.8.1
|
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':
|
'@smithy/util-endpoints@3.2.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/node-config-provider': 4.3.8
|
'@smithy/node-config-provider': 4.3.8
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: bird
|
name: bird
|
||||||
description: X/Twitter CLI for reading, searching, posting, and engagement via cookies.
|
description: X/Twitter CLI for reading, searching, posting, and engagement via cookies.
|
||||||
homepage: https://bird.fast
|
homepage: https://bird.fast
|
||||||
metadata: {"clawdbot":{"emoji":"🐦","requires":{"bins":["bird"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/bird","bins":["bird"],"label":"Install bird (brew)"},{"id":"npm","kind":"node","package":"@steipete/bird","bins":["bird"],"label":"Install bird (npm)"}]}}
|
metadata: {"clawdbot":{"emoji":"🐦","requires":{"bins":["bird"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/bird","bins":["bird"],"label":"Install bird (brew)","os":["darwin"]},{"id":"npm","kind":"node","package":"@steipete/bird","bins":["bird"],"label":"Install bird (npm)"}]}}
|
||||||
---
|
---
|
||||||
|
|
||||||
# bird 🐦
|
# bird 🐦
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core";
|
||||||
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
|
import { parseBooleanValue } from "../utils/boolean.js";
|
||||||
|
import { resolveUserPath } from "../utils.js";
|
||||||
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
|
||||||
|
type PayloadLogStage = "request" | "usage";
|
||||||
|
|
||||||
|
type PayloadLogEvent = {
|
||||||
|
ts: string;
|
||||||
|
stage: PayloadLogStage;
|
||||||
|
runId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
provider?: string;
|
||||||
|
modelId?: string;
|
||||||
|
modelApi?: string | null;
|
||||||
|
workspaceDir?: string;
|
||||||
|
payload?: unknown;
|
||||||
|
usage?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
payloadDigest?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PayloadLogConfig = {
|
||||||
|
enabled: boolean;
|
||||||
|
filePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PayloadLogWriter = {
|
||||||
|
filePath: string;
|
||||||
|
write: (line: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writers = new Map<string, PayloadLogWriter>();
|
||||||
|
const log = createSubsystemLogger("agent/anthropic-payload");
|
||||||
|
|
||||||
|
function resolvePayloadLogConfig(env: NodeJS.ProcessEnv): PayloadLogConfig {
|
||||||
|
const enabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG) ?? false;
|
||||||
|
const fileOverride = env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim();
|
||||||
|
const filePath = fileOverride
|
||||||
|
? resolveUserPath(fileOverride)
|
||||||
|
: path.join(resolveStateDir(env), "logs", "anthropic-payload.jsonl");
|
||||||
|
return { enabled, filePath };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWriter(filePath: string): PayloadLogWriter {
|
||||||
|
const existing = writers.get(filePath);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
const writer: PayloadLogWriter = {
|
||||||
|
filePath,
|
||||||
|
write: (line: string) => {
|
||||||
|
queue = queue
|
||||||
|
.then(() => ready)
|
||||||
|
.then(() => fs.appendFile(filePath, line, "utf8"))
|
||||||
|
.catch(() => undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
writers.set(filePath, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJsonStringify(value: unknown): string | null {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, (_key, val) => {
|
||||||
|
if (typeof val === "bigint") return val.toString();
|
||||||
|
if (typeof val === "function") return "[Function]";
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return { name: val.name, message: val.message, stack: val.stack };
|
||||||
|
}
|
||||||
|
if (val instanceof Uint8Array) {
|
||||||
|
return { type: "Uint8Array", data: Buffer.from(val).toString("base64") };
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(error: unknown): string | undefined {
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
if (typeof error === "string") return error;
|
||||||
|
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
if (error && typeof error === "object") {
|
||||||
|
return safeJsonStringify(error) ?? "unknown error";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function digest(value: unknown): string | undefined {
|
||||||
|
const serialized = safeJsonStringify(value);
|
||||||
|
if (!serialized) return undefined;
|
||||||
|
return crypto.createHash("sha256").update(serialized).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAnthropicModel(model: Model<Api> | undefined | null): boolean {
|
||||||
|
return (model as { api?: unknown })?.api === "anthropic-messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLastAssistantUsage(messages: AgentMessage[]): Record<string, unknown> | null {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||||
|
const msg = messages[i] as { role?: unknown; usage?: unknown };
|
||||||
|
if (msg?.role === "assistant" && msg.usage && typeof msg.usage === "object") {
|
||||||
|
return msg.usage as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnthropicPayloadLogger = {
|
||||||
|
enabled: true;
|
||||||
|
wrapStreamFn: (streamFn: StreamFn) => StreamFn;
|
||||||
|
recordUsage: (messages: AgentMessage[], error?: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createAnthropicPayloadLogger(params: {
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
runId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
provider?: string;
|
||||||
|
modelId?: string;
|
||||||
|
modelApi?: string | null;
|
||||||
|
workspaceDir?: string;
|
||||||
|
}): AnthropicPayloadLogger | null {
|
||||||
|
const env = params.env ?? process.env;
|
||||||
|
const cfg = resolvePayloadLogConfig(env);
|
||||||
|
if (!cfg.enabled) return null;
|
||||||
|
|
||||||
|
const writer = getWriter(cfg.filePath);
|
||||||
|
const base: Omit<PayloadLogEvent, "ts" | "stage"> = {
|
||||||
|
runId: params.runId,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
provider: params.provider,
|
||||||
|
modelId: params.modelId,
|
||||||
|
modelApi: params.modelApi,
|
||||||
|
workspaceDir: params.workspaceDir,
|
||||||
|
};
|
||||||
|
|
||||||
|
const record = (event: PayloadLogEvent) => {
|
||||||
|
const line = safeJsonStringify(event);
|
||||||
|
if (!line) return;
|
||||||
|
writer.write(`${line}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapStreamFn: AnthropicPayloadLogger["wrapStreamFn"] = (streamFn) => {
|
||||||
|
const wrapped: StreamFn = (model, context, options) => {
|
||||||
|
if (!isAnthropicModel(model as Model<Api>)) {
|
||||||
|
return streamFn(model, context, options);
|
||||||
|
}
|
||||||
|
const nextOnPayload = (payload: unknown) => {
|
||||||
|
record({
|
||||||
|
...base,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
stage: "request",
|
||||||
|
payload,
|
||||||
|
payloadDigest: digest(payload),
|
||||||
|
});
|
||||||
|
options?.onPayload?.(payload);
|
||||||
|
};
|
||||||
|
return streamFn(model, context, {
|
||||||
|
...options,
|
||||||
|
onPayload: nextOnPayload,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return wrapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordUsage: AnthropicPayloadLogger["recordUsage"] = (messages, error) => {
|
||||||
|
const usage = findLastAssistantUsage(messages);
|
||||||
|
const errorMessage = formatError(error);
|
||||||
|
if (!usage) {
|
||||||
|
if (errorMessage) {
|
||||||
|
record({
|
||||||
|
...base,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
stage: "usage",
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
record({
|
||||||
|
...base,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
stage: "usage",
|
||||||
|
usage,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
log.info("anthropic usage", {
|
||||||
|
runId: params.runId,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
usage,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info("anthropic payload logger enabled", { filePath: writer.filePath });
|
||||||
|
return { enabled: true, wrapStreamFn, recordUsage };
|
||||||
|
}
|
||||||
@@ -129,4 +129,25 @@ describe("exec approvals", () => {
|
|||||||
expect(calls).toContain("node.invoke");
|
expect(calls).toContain("node.invoke");
|
||||||
expect(calls).not.toContain("exec.approval.request");
|
expect(calls).not.toContain("exec.approval.request");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("honors ask=off for elevated gateway exec without prompting", async () => {
|
||||||
|
const { callGatewayTool } = await import("./tools/gateway.js");
|
||||||
|
const calls: string[] = [];
|
||||||
|
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
|
||||||
|
calls.push(method);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { createExecTool } = await import("./bash-tools.exec.js");
|
||||||
|
const tool = createExecTool({
|
||||||
|
ask: "off",
|
||||||
|
security: "full",
|
||||||
|
approvalRunningNoticeMs: 0,
|
||||||
|
elevated: { enabled: true, allowed: true, defaultLevel: "ask" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await tool.execute("call3", { command: "echo ok", elevated: true });
|
||||||
|
expect(result.details.status).toBe("completed");
|
||||||
|
expect(calls).not.toContain("exec.approval.request");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -838,10 +838,7 @@ export function createExecTool(
|
|||||||
applyPathPrepend(env, defaultPathPrepend);
|
applyPathPrepend(env, defaultPathPrepend);
|
||||||
|
|
||||||
if (host === "node") {
|
if (host === "node") {
|
||||||
const approvals = resolveExecApprovals(
|
const approvals = resolveExecApprovals(agentId, { security, ask });
|
||||||
agentId,
|
|
||||||
host === "node" ? { security: "allowlist" } : undefined,
|
|
||||||
);
|
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
@@ -1112,7 +1109,7 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (host === "gateway" && !bypassApprovals) {
|
if (host === "gateway" && !bypassApprovals) {
|
||||||
const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
|
const approvals = resolveExecApprovals(agentId, { security, ask });
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import {
|
||||||
|
BedrockClient,
|
||||||
|
ListFoundationModelsCommand,
|
||||||
|
type ListFoundationModelsCommandOutput,
|
||||||
|
} from "@aws-sdk/client-bedrock";
|
||||||
|
|
||||||
|
import type { BedrockDiscoveryConfig, ModelDefinitionConfig } from "../config/types.js";
|
||||||
|
|
||||||
|
const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600;
|
||||||
|
const DEFAULT_CONTEXT_WINDOW = 32000;
|
||||||
|
const DEFAULT_MAX_TOKENS = 4096;
|
||||||
|
const DEFAULT_COST = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
type BedrockModelSummary = NonNullable<ListFoundationModelsCommandOutput["modelSummaries"]>[number];
|
||||||
|
|
||||||
|
type BedrockDiscoveryCacheEntry = {
|
||||||
|
expiresAt: number;
|
||||||
|
value?: ModelDefinitionConfig[];
|
||||||
|
inFlight?: Promise<ModelDefinitionConfig[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const discoveryCache = new Map<string, BedrockDiscoveryCacheEntry>();
|
||||||
|
let hasLoggedBedrockError = false;
|
||||||
|
|
||||||
|
function normalizeProviderFilter(filter?: string[]): string[] {
|
||||||
|
if (!filter || filter.length === 0) return [];
|
||||||
|
const normalized = new Set(
|
||||||
|
filter.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0),
|
||||||
|
);
|
||||||
|
return Array.from(normalized).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCacheKey(params: {
|
||||||
|
region: string;
|
||||||
|
providerFilter: string[];
|
||||||
|
refreshIntervalSeconds: number;
|
||||||
|
defaultContextWindow: number;
|
||||||
|
defaultMaxTokens: number;
|
||||||
|
}): string {
|
||||||
|
return JSON.stringify(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
function includesTextModalities(modalities?: Array<string>): boolean {
|
||||||
|
return (modalities ?? []).some((entry) => entry.toLowerCase() === "text");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(summary: BedrockModelSummary): boolean {
|
||||||
|
const status = summary.modelLifecycle?.status;
|
||||||
|
return typeof status === "string" ? status.toUpperCase() === "ACTIVE" : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image"> {
|
||||||
|
const inputs = summary.inputModalities ?? [];
|
||||||
|
const mapped = new Set<"text" | "image">();
|
||||||
|
for (const modality of inputs) {
|
||||||
|
const lower = modality.toLowerCase();
|
||||||
|
if (lower === "text") mapped.add("text");
|
||||||
|
if (lower === "image") mapped.add("image");
|
||||||
|
}
|
||||||
|
if (mapped.size === 0) mapped.add("text");
|
||||||
|
return Array.from(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferReasoningSupport(summary: BedrockModelSummary): boolean {
|
||||||
|
const haystack = `${summary.modelId ?? ""} ${summary.modelName ?? ""}`.toLowerCase();
|
||||||
|
return haystack.includes("reasoning") || haystack.includes("thinking");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDefaultContextWindow(config?: BedrockDiscoveryConfig): number {
|
||||||
|
const value = Math.floor(config?.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW);
|
||||||
|
return value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDefaultMaxTokens(config?: BedrockDiscoveryConfig): number {
|
||||||
|
const value = Math.floor(config?.defaultMaxTokens ?? DEFAULT_MAX_TOKENS);
|
||||||
|
return value > 0 ? value : DEFAULT_MAX_TOKENS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesProviderFilter(summary: BedrockModelSummary, filter: string[]): boolean {
|
||||||
|
if (filter.length === 0) return true;
|
||||||
|
const providerName =
|
||||||
|
summary.providerName ??
|
||||||
|
(typeof summary.modelId === "string" ? summary.modelId.split(".")[0] : undefined);
|
||||||
|
const normalized = providerName?.trim().toLowerCase();
|
||||||
|
if (!normalized) return false;
|
||||||
|
return filter.includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIncludeSummary(summary: BedrockModelSummary, filter: string[]): boolean {
|
||||||
|
if (!summary.modelId?.trim()) return false;
|
||||||
|
if (!matchesProviderFilter(summary, filter)) return false;
|
||||||
|
if (summary.responseStreamingSupported !== true) return false;
|
||||||
|
if (!includesTextModalities(summary.outputModalities)) return false;
|
||||||
|
if (!isActive(summary)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toModelDefinition(
|
||||||
|
summary: BedrockModelSummary,
|
||||||
|
defaults: { contextWindow: number; maxTokens: number },
|
||||||
|
): ModelDefinitionConfig {
|
||||||
|
const id = summary.modelId?.trim() ?? "";
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: summary.modelName?.trim() || id,
|
||||||
|
reasoning: inferReasoningSupport(summary),
|
||||||
|
input: mapInputModalities(summary),
|
||||||
|
cost: DEFAULT_COST,
|
||||||
|
contextWindow: defaults.contextWindow,
|
||||||
|
maxTokens: defaults.maxTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetBedrockDiscoveryCacheForTest(): void {
|
||||||
|
discoveryCache.clear();
|
||||||
|
hasLoggedBedrockError = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverBedrockModels(params: {
|
||||||
|
region: string;
|
||||||
|
config?: BedrockDiscoveryConfig;
|
||||||
|
now?: () => number;
|
||||||
|
clientFactory?: (region: string) => BedrockClient;
|
||||||
|
}): Promise<ModelDefinitionConfig[]> {
|
||||||
|
const refreshIntervalSeconds = Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor(params.config?.refreshInterval ?? DEFAULT_REFRESH_INTERVAL_SECONDS),
|
||||||
|
);
|
||||||
|
const providerFilter = normalizeProviderFilter(params.config?.providerFilter);
|
||||||
|
const defaultContextWindow = resolveDefaultContextWindow(params.config);
|
||||||
|
const defaultMaxTokens = resolveDefaultMaxTokens(params.config);
|
||||||
|
const cacheKey = buildCacheKey({
|
||||||
|
region: params.region,
|
||||||
|
providerFilter,
|
||||||
|
refreshIntervalSeconds,
|
||||||
|
defaultContextWindow,
|
||||||
|
defaultMaxTokens,
|
||||||
|
});
|
||||||
|
const now = params.now?.() ?? Date.now();
|
||||||
|
|
||||||
|
if (refreshIntervalSeconds > 0) {
|
||||||
|
const cached = discoveryCache.get(cacheKey);
|
||||||
|
if (cached?.value && cached.expiresAt > now) {
|
||||||
|
return cached.value;
|
||||||
|
}
|
||||||
|
if (cached?.inFlight) {
|
||||||
|
return cached.inFlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientFactory = params.clientFactory ?? ((region: string) => new BedrockClient({ region }));
|
||||||
|
const client = clientFactory(params.region);
|
||||||
|
|
||||||
|
const discoveryPromise = (async () => {
|
||||||
|
const response = await client.send(new ListFoundationModelsCommand({}));
|
||||||
|
const discovered: ModelDefinitionConfig[] = [];
|
||||||
|
for (const summary of response.modelSummaries ?? []) {
|
||||||
|
if (!shouldIncludeSummary(summary, providerFilter)) continue;
|
||||||
|
discovered.push(
|
||||||
|
toModelDefinition(summary, {
|
||||||
|
contextWindow: defaultContextWindow,
|
||||||
|
maxTokens: defaultMaxTokens,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return discovered.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (refreshIntervalSeconds > 0) {
|
||||||
|
discoveryCache.set(cacheKey, {
|
||||||
|
expiresAt: now + refreshIntervalSeconds * 1000,
|
||||||
|
inFlight: discoveryPromise,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = await discoveryPromise;
|
||||||
|
if (refreshIntervalSeconds > 0) {
|
||||||
|
discoveryCache.set(cacheKey, {
|
||||||
|
expiresAt: now + refreshIntervalSeconds * 1000,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} catch (error) {
|
||||||
|
if (refreshIntervalSeconds > 0) {
|
||||||
|
discoveryCache.delete(cacheKey);
|
||||||
|
}
|
||||||
|
if (!hasLoggedBedrockError) {
|
||||||
|
hasLoggedBedrockError = true;
|
||||||
|
console.warn(`[bedrock-discovery] Failed to list models: ${String(error)}`);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { createSessionsListTool } from "./tools/sessions-list-tool.js";
|
|||||||
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
||||||
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
||||||
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
|
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
|
||||||
|
import { createTtsTool } from "./tools/tts-tool.js";
|
||||||
|
|
||||||
export function createClawdbotTools(options?: {
|
export function createClawdbotTools(options?: {
|
||||||
browserControlUrl?: string;
|
browserControlUrl?: string;
|
||||||
@@ -31,6 +32,12 @@ export function createClawdbotTools(options?: {
|
|||||||
agentTo?: string;
|
agentTo?: string;
|
||||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||||
agentThreadId?: string | number;
|
agentThreadId?: string | number;
|
||||||
|
/** Group id for channel-level tool policy inheritance. */
|
||||||
|
agentGroupId?: string | null;
|
||||||
|
/** Group channel label for channel-level tool policy inheritance. */
|
||||||
|
agentGroupChannel?: string | null;
|
||||||
|
/** Group space label for channel-level tool policy inheritance. */
|
||||||
|
agentGroupSpace?: string | null;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
sandboxRoot?: string;
|
sandboxRoot?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
@@ -90,6 +97,10 @@ export function createClawdbotTools(options?: {
|
|||||||
replyToMode: options?.replyToMode,
|
replyToMode: options?.replyToMode,
|
||||||
hasRepliedRef: options?.hasRepliedRef,
|
hasRepliedRef: options?.hasRepliedRef,
|
||||||
}),
|
}),
|
||||||
|
createTtsTool({
|
||||||
|
agentChannel: options?.agentChannel,
|
||||||
|
config: options?.config,
|
||||||
|
}),
|
||||||
createGatewayTool({
|
createGatewayTool({
|
||||||
agentSessionKey: options?.agentSessionKey,
|
agentSessionKey: options?.agentSessionKey,
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
@@ -114,6 +125,9 @@ export function createClawdbotTools(options?: {
|
|||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
agentTo: options?.agentTo,
|
agentTo: options?.agentTo,
|
||||||
agentThreadId: options?.agentThreadId,
|
agentThreadId: options?.agentThreadId,
|
||||||
|
agentGroupId: options?.agentGroupId,
|
||||||
|
agentGroupChannel: options?.agentGroupChannel,
|
||||||
|
agentGroupSpace: options?.agentGroupSpace,
|
||||||
sandboxed: options?.sandboxed,
|
sandboxed: options?.sandboxed,
|
||||||
}),
|
}),
|
||||||
createSessionStatusTool({
|
createSessionStatusTool({
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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,6 +12,30 @@ export type AgentIdentityFile = {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IDENTITY_PLACEHOLDER_VALUES = new Set([
|
||||||
|
"pick something you like",
|
||||||
|
"ai? robot? familiar? ghost in the machine? something weirder?",
|
||||||
|
"how do you come across? sharp? warm? chaotic? calm?",
|
||||||
|
"your signature - pick one that feels right",
|
||||||
|
"workspace-relative path, http(s) url, or data uri",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normalizeIdentityValue(value: string): string {
|
||||||
|
let normalized = value.trim();
|
||||||
|
normalized = normalized.replace(/^[*_]+|[*_]+$/g, "").trim();
|
||||||
|
if (normalized.startsWith("(") && normalized.endsWith(")")) {
|
||||||
|
normalized = normalized.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
normalized = normalized.replace(/[\u2013\u2014]/g, "-");
|
||||||
|
normalized = normalized.replace(/\s+/g, " ").toLowerCase();
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIdentityPlaceholder(value: string): boolean {
|
||||||
|
const normalized = normalizeIdentityValue(value);
|
||||||
|
return IDENTITY_PLACEHOLDER_VALUES.has(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
||||||
const identity: AgentIdentityFile = {};
|
const identity: AgentIdentityFile = {};
|
||||||
const lines = content.split(/\r?\n/);
|
const lines = content.split(/\r?\n/);
|
||||||
@@ -25,6 +49,7 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
|||||||
.replace(/^[*_]+|[*_]+$/g, "")
|
.replace(/^[*_]+|[*_]+$/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
|
if (isIdentityPlaceholder(value)) continue;
|
||||||
if (label === "name") identity.name = value;
|
if (label === "name") identity.name = value;
|
||||||
if (label === "emoji") identity.emoji = value;
|
if (label === "emoji") identity.emoji = value;
|
||||||
if (label === "creature") identity.creature = value;
|
if (label === "creature") identity.creature = value;
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ function resolveEnvSourceLabel(params: {
|
|||||||
return `${prefix}${params.label}`;
|
return `${prefix}${params.label}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAwsSdkEnvVarName(): string | undefined {
|
export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
||||||
if (process.env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
|
if (env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
|
||||||
if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) {
|
if (env[AWS_ACCESS_KEY_ENV]?.trim() && env[AWS_SECRET_KEY_ENV]?.trim()) {
|
||||||
return AWS_ACCESS_KEY_ENV;
|
return AWS_ACCESS_KEY_ENV;
|
||||||
}
|
}
|
||||||
if (process.env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
|
if (env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "../providers/github-copilot-token.js";
|
} from "../providers/github-copilot-token.js";
|
||||||
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
||||||
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
||||||
|
import { discoverBedrockModels } from "./bedrock-discovery.js";
|
||||||
import {
|
import {
|
||||||
buildSyntheticModelDefinition,
|
buildSyntheticModelDefinition,
|
||||||
SYNTHETIC_BASE_URL,
|
SYNTHETIC_BASE_URL,
|
||||||
@@ -375,3 +376,27 @@ export async function resolveImplicitCopilotProvider(params: {
|
|||||||
models: [],
|
models: [],
|
||||||
} satisfies ProviderConfig;
|
} satisfies ProviderConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveImplicitBedrockProvider(params: {
|
||||||
|
agentDir: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
}): Promise<ProviderConfig | null> {
|
||||||
|
const env = params.env ?? process.env;
|
||||||
|
const discoveryConfig = params.config?.models?.bedrockDiscovery;
|
||||||
|
const enabled = discoveryConfig?.enabled;
|
||||||
|
const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined;
|
||||||
|
if (enabled === false) return null;
|
||||||
|
if (enabled !== true && !hasAwsCreds) return null;
|
||||||
|
|
||||||
|
const region = discoveryConfig?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
|
||||||
|
const models = await discoverBedrockModels({ region, config: discoveryConfig });
|
||||||
|
if (models.length === 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseUrl: `https://bedrock-runtime.${region}.amazonaws.com`,
|
||||||
|
api: "bedrock-converse-stream",
|
||||||
|
auth: "aws-sdk",
|
||||||
|
models,
|
||||||
|
} satisfies ProviderConfig;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
|||||||
import {
|
import {
|
||||||
normalizeProviders,
|
normalizeProviders,
|
||||||
type ProviderConfig,
|
type ProviderConfig,
|
||||||
|
resolveImplicitBedrockProvider,
|
||||||
resolveImplicitCopilotProvider,
|
resolveImplicitCopilotProvider,
|
||||||
resolveImplicitProviders,
|
resolveImplicitProviders,
|
||||||
} from "./models-config.providers.js";
|
} from "./models-config.providers.js";
|
||||||
@@ -84,6 +85,13 @@ export async function ensureClawdbotModelsJson(
|
|||||||
implicit: implicitProviders,
|
implicit: implicitProviders,
|
||||||
explicit: explicitProviders,
|
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 });
|
const implicitCopilot = await resolveImplicitCopilotProvider({ agentDir });
|
||||||
if (implicitCopilot && !providers["github-copilot"]) {
|
if (implicitCopilot && !providers["github-copilot"]) {
|
||||||
providers["github-copilot"] = implicitCopilot;
|
providers["github-copilot"] = implicitCopilot;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { downgradeOpenAIReasoningBlocks } from "./pi-embedded-helpers.js";
|
||||||
|
|
||||||
|
describe("downgradeOpenAIReasoningBlocks", () => {
|
||||||
|
it("keeps reasoning signatures when followed by content", () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinking: "internal reasoning",
|
||||||
|
thinkingSignature: JSON.stringify({ id: "rs_123", type: "reasoning" }),
|
||||||
|
},
|
||||||
|
{ type: "text", text: "answer" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops orphaned reasoning blocks without following content", () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinkingSignature: JSON.stringify({ id: "rs_abc", type: "reasoning" }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ role: "user", content: "next" },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual([
|
||||||
|
{ role: "user", content: "next" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops object-form orphaned signatures", () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinkingSignature: { id: "rs_obj", type: "reasoning" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps non-reasoning thinking signatures", () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinking: "t",
|
||||||
|
thinkingSignature: "reasoning_content",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,6 +31,8 @@ export {
|
|||||||
parseImageDimensionError,
|
parseImageDimensionError,
|
||||||
} from "./pi-embedded-helpers/errors.js";
|
} from "./pi-embedded-helpers/errors.js";
|
||||||
export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js";
|
export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js";
|
||||||
|
|
||||||
|
export { downgradeOpenAIReasoningBlocks } from "./pi-embedded-helpers/openai.js";
|
||||||
export {
|
export {
|
||||||
isEmptyAssistantMessageContent,
|
isEmptyAssistantMessageContent,
|
||||||
sanitizeSessionMessagesImages,
|
sanitizeSessionMessagesImages,
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
|
|
||||||
|
type OpenAIThinkingBlock = {
|
||||||
|
type?: unknown;
|
||||||
|
thinking?: unknown;
|
||||||
|
thinkingSignature?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OpenAIReasoningSignature = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseOpenAIReasoningSignature(value: unknown): OpenAIReasoningSignature | null {
|
||||||
|
if (!value) return null;
|
||||||
|
let candidate: { id?: unknown; type?: unknown } | null = null;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
||||||
|
try {
|
||||||
|
candidate = JSON.parse(trimmed) as { id?: unknown; type?: unknown };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (typeof value === "object") {
|
||||||
|
candidate = value as { id?: unknown; type?: unknown };
|
||||||
|
}
|
||||||
|
if (!candidate) return null;
|
||||||
|
const id = typeof candidate.id === "string" ? candidate.id : "";
|
||||||
|
const type = typeof candidate.type === "string" ? candidate.type : "";
|
||||||
|
if (!id.startsWith("rs_")) return null;
|
||||||
|
if (type === "reasoning" || type.startsWith("reasoning.")) {
|
||||||
|
return { id, type };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFollowingNonThinkingBlock(
|
||||||
|
content: Extract<AgentMessage, { role: "assistant" }>["content"],
|
||||||
|
index: number,
|
||||||
|
): boolean {
|
||||||
|
for (let i = index + 1; i < content.length; i++) {
|
||||||
|
const block = content[i];
|
||||||
|
if (!block || typeof block !== "object") return true;
|
||||||
|
if ((block as { type?: unknown }).type !== "thinking") return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI Responses API can reject transcripts that contain a standalone `reasoning` item id
|
||||||
|
* without the required following item.
|
||||||
|
*
|
||||||
|
* Clawdbot persists provider-specific reasoning metadata in `thinkingSignature`; if that metadata
|
||||||
|
* is incomplete, drop the block to keep history usable.
|
||||||
|
*/
|
||||||
|
export function downgradeOpenAIReasoningBlocks(messages: AgentMessage[]): AgentMessage[] {
|
||||||
|
const out: AgentMessage[] = [];
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (!msg || typeof msg !== "object") {
|
||||||
|
out.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = (msg as { role?: unknown }).role;
|
||||||
|
if (role !== "assistant") {
|
||||||
|
out.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantMsg = msg as Extract<AgentMessage, { role: "assistant" }>;
|
||||||
|
if (!Array.isArray(assistantMsg.content)) {
|
||||||
|
out.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
type AssistantContentBlock = (typeof assistantMsg.content)[number];
|
||||||
|
|
||||||
|
const nextContent: AssistantContentBlock[] = [];
|
||||||
|
for (let i = 0; i < assistantMsg.content.length; i++) {
|
||||||
|
const block = assistantMsg.content[i];
|
||||||
|
if (!block || typeof block !== "object") {
|
||||||
|
nextContent.push(block as AssistantContentBlock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const record = block as OpenAIThinkingBlock;
|
||||||
|
if (record.type !== "thinking") {
|
||||||
|
nextContent.push(block as AssistantContentBlock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const signature = parseOpenAIReasoningSignature(record.thinkingSignature);
|
||||||
|
if (!signature) {
|
||||||
|
nextContent.push(block as AssistantContentBlock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (hasFollowingNonThinkingBlock(assistantMsg.content, i)) {
|
||||||
|
nextContent.push(block as AssistantContentBlock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
out.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextContent.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push({ ...assistantMsg, content: nextContent } as AgentMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -63,12 +63,12 @@ const makeAttempt = (overrides: Partial<EmbeddedRunAttemptResult>): EmbeddedRunA
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeConfig = (): ClawdbotConfig =>
|
const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotConfig =>
|
||||||
({
|
({
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
model: {
|
model: {
|
||||||
fallbacks: [],
|
fallbacks: opts?.fallbacks ?? [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ const makeConfig = (): ClawdbotConfig =>
|
|||||||
providers: {
|
providers: {
|
||||||
openai: {
|
openai: {
|
||||||
api: "openai-responses",
|
api: "openai-responses",
|
||||||
apiKey: "sk-test",
|
apiKey: opts?.apiKey ?? "sk-test",
|
||||||
baseUrl: "https://example.com",
|
baseUrl: "https://example.com",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
@@ -94,7 +94,13 @@ const makeConfig = (): ClawdbotConfig =>
|
|||||||
},
|
},
|
||||||
}) satisfies ClawdbotConfig;
|
}) satisfies ClawdbotConfig;
|
||||||
|
|
||||||
const writeAuthStore = async (agentDir: string, opts?: { includeAnthropic?: boolean }) => {
|
const writeAuthStore = async (
|
||||||
|
agentDir: string,
|
||||||
|
opts?: {
|
||||||
|
includeAnthropic?: boolean;
|
||||||
|
usageStats?: Record<string, { lastUsed?: number; cooldownUntil?: number }>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
const payload = {
|
const payload = {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -105,10 +111,12 @@ const writeAuthStore = async (agentDir: string, opts?: { includeAnthropic?: bool
|
|||||||
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
|
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
usageStats: {
|
usageStats:
|
||||||
"openai:p1": { lastUsed: 1 },
|
opts?.usageStats ??
|
||||||
"openai:p2": { lastUsed: 2 },
|
({
|
||||||
},
|
"openai:p1": { lastUsed: 1 },
|
||||||
|
"openai:p2": { lastUsed: 2 },
|
||||||
|
} as Record<string, { lastUsed?: number }>),
|
||||||
};
|
};
|
||||||
await fs.writeFile(authPath, JSON.stringify(payload));
|
await fs.writeFile(authPath, JSON.stringify(payload));
|
||||||
};
|
};
|
||||||
@@ -384,6 +392,92 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails over when all profiles are in cooldown and fallbacks are configured", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
||||||
|
const now = Date.now();
|
||||||
|
vi.setSystemTime(now);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeAuthStore(agentDir, {
|
||||||
|
usageStats: {
|
||||||
|
"openai:p1": { lastUsed: 1, cooldownUntil: now + 60 * 60 * 1000 },
|
||||||
|
"openai:p2": { lastUsed: 2, cooldownUntil: now + 60 * 60 * 1000 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runEmbeddedPiAgent({
|
||||||
|
sessionId: "session:test",
|
||||||
|
sessionKey: "agent:test:cooldown-failover",
|
||||||
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
|
workspaceDir,
|
||||||
|
agentDir,
|
||||||
|
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
|
||||||
|
prompt: "hello",
|
||||||
|
provider: "openai",
|
||||||
|
model: "mock-1",
|
||||||
|
authProfileIdSource: "auto",
|
||||||
|
timeoutMs: 5_000,
|
||||||
|
runId: "run:cooldown-failover",
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
name: "FailoverError",
|
||||||
|
reason: "rate_limit",
|
||||||
|
provider: "openai",
|
||||||
|
model: "mock-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
await fs.rm(agentDir, { recursive: true, force: true });
|
||||||
|
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails over when auth is unavailable and fallbacks are configured", async () => {
|
||||||
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
||||||
|
const previousOpenAiKey = process.env.OPENAI_API_KEY;
|
||||||
|
delete process.env.OPENAI_API_KEY;
|
||||||
|
try {
|
||||||
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
|
await fs.writeFile(authPath, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runEmbeddedPiAgent({
|
||||||
|
sessionId: "session:test",
|
||||||
|
sessionKey: "agent:test:auth-unavailable",
|
||||||
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
|
workspaceDir,
|
||||||
|
agentDir,
|
||||||
|
config: makeConfig({ fallbacks: ["openai/mock-2"], apiKey: "" }),
|
||||||
|
prompt: "hello",
|
||||||
|
provider: "openai",
|
||||||
|
model: "mock-1",
|
||||||
|
authProfileIdSource: "auto",
|
||||||
|
timeoutMs: 5_000,
|
||||||
|
runId: "run:auth-unavailable",
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({ name: "FailoverError", reason: "auth" });
|
||||||
|
|
||||||
|
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
if (previousOpenAiKey === undefined) {
|
||||||
|
delete process.env.OPENAI_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.OPENAI_API_KEY = previousOpenAiKey;
|
||||||
|
}
|
||||||
|
await fs.rm(agentDir, { recursive: true, force: true });
|
||||||
|
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("skips profiles in cooldown when rotating after failure", async () => {
|
it("skips profiles in cooldown when rotating after failure", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -161,4 +161,92 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]?.role).toBe("assistant");
|
expect(result[0]?.role).toBe("assistant");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not downgrade openai reasoning when the model has not changed", async () => {
|
||||||
|
const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [
|
||||||
|
{
|
||||||
|
type: "custom",
|
||||||
|
customType: "model-snapshot",
|
||||||
|
data: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
provider: "openai",
|
||||||
|
modelApi: "openai-responses",
|
||||||
|
modelId: "gpt-5.2-codex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const sessionManager = {
|
||||||
|
getEntries: vi.fn(() => sessionEntries),
|
||||||
|
appendCustomEntry: vi.fn((customType: string, data: unknown) => {
|
||||||
|
sessionEntries.push({ type: "custom", customType, data });
|
||||||
|
}),
|
||||||
|
} as unknown as SessionManager;
|
||||||
|
const messages: AgentMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinking: "reasoning",
|
||||||
|
thinkingSignature: JSON.stringify({ id: "rs_test", type: "reasoning" }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sanitizeSessionHistory({
|
||||||
|
messages,
|
||||||
|
modelApi: "openai-responses",
|
||||||
|
provider: "openai",
|
||||||
|
modelId: "gpt-5.2-codex",
|
||||||
|
sessionManager,
|
||||||
|
sessionId: "test-session",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(messages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("downgrades openai reasoning only when the model changes", async () => {
|
||||||
|
const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [
|
||||||
|
{
|
||||||
|
type: "custom",
|
||||||
|
customType: "model-snapshot",
|
||||||
|
data: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
provider: "anthropic",
|
||||||
|
modelApi: "anthropic-messages",
|
||||||
|
modelId: "claude-3-7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const sessionManager = {
|
||||||
|
getEntries: vi.fn(() => sessionEntries),
|
||||||
|
appendCustomEntry: vi.fn((customType: string, data: unknown) => {
|
||||||
|
sessionEntries.push({ type: "custom", customType, data });
|
||||||
|
}),
|
||||||
|
} as unknown as SessionManager;
|
||||||
|
const messages: AgentMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinking: "reasoning",
|
||||||
|
thinkingSignature: { id: "rs_test", type: "reasoning" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sanitizeSessionHistory({
|
||||||
|
messages,
|
||||||
|
modelApi: "openai-responses",
|
||||||
|
provider: "openai",
|
||||||
|
modelId: "gpt-5.2-codex",
|
||||||
|
sessionManager,
|
||||||
|
sessionId: "test-session",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -70,7 +70,7 @@ vi.mock("@mariozechner/pi-ai", async () => {
|
|||||||
},
|
},
|
||||||
streamSimple: (model: { api: string; provider: string; id: string }) => {
|
streamSimple: (model: { api: string; provider: string; id: string }) => {
|
||||||
const stream = new actual.AssistantMessageEventStream();
|
const stream = new actual.AssistantMessageEventStream();
|
||||||
queueMicrotask(() => {
|
setTimeout(() => {
|
||||||
stream.push({
|
stream.push({
|
||||||
type: "done",
|
type: "done",
|
||||||
reason: "stop",
|
reason: "stop",
|
||||||
@@ -80,7 +80,7 @@ vi.mock("@mariozechner/pi-ai", async () => {
|
|||||||
: buildAssistantMessage(model),
|
: buildAssistantMessage(model),
|
||||||
});
|
});
|
||||||
stream.end();
|
stream.end();
|
||||||
});
|
}, 0);
|
||||||
return stream;
|
return stream;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -165,6 +165,7 @@ const readSessionMessages = async (sessionFile: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("runEmbeddedPiAgent", () => {
|
describe("runEmbeddedPiAgent", () => {
|
||||||
|
const itIfNotWin32 = process.platform === "win32" ? it.skip : it;
|
||||||
it("writes models.json into the provided agentDir", async () => {
|
it("writes models.json into the provided agentDir", async () => {
|
||||||
const sessionFile = nextSessionFile();
|
const sessionFile = nextSessionFile();
|
||||||
|
|
||||||
@@ -210,35 +211,39 @@ describe("runEmbeddedPiAgent", () => {
|
|||||||
await expect(fs.stat(path.join(agentDir, "models.json"))).resolves.toBeTruthy();
|
await expect(fs.stat(path.join(agentDir, "models.json"))).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("persists the first user message before assistant output", { timeout: 60_000 }, async () => {
|
itIfNotWin32(
|
||||||
const sessionFile = nextSessionFile();
|
"persists the first user message before assistant output",
|
||||||
const cfg = makeOpenAiConfig(["mock-1"]);
|
{ timeout: 60_000 },
|
||||||
await ensureModels(cfg);
|
async () => {
|
||||||
|
const sessionFile = nextSessionFile();
|
||||||
|
const cfg = makeOpenAiConfig(["mock-1"]);
|
||||||
|
await ensureModels(cfg);
|
||||||
|
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgent({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: testSessionKey,
|
sessionKey: testSessionKey,
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
prompt: "hello",
|
prompt: "hello",
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "mock-1",
|
model: "mock-1",
|
||||||
timeoutMs: 5_000,
|
timeoutMs: 5_000,
|
||||||
agentDir,
|
agentDir,
|
||||||
enqueue: immediateEnqueue,
|
enqueue: immediateEnqueue,
|
||||||
});
|
});
|
||||||
|
|
||||||
const messages = await readSessionMessages(sessionFile);
|
const messages = await readSessionMessages(sessionFile);
|
||||||
const firstUserIndex = messages.findIndex(
|
const firstUserIndex = messages.findIndex(
|
||||||
(message) => message?.role === "user" && textFromContent(message.content) === "hello",
|
(message) => message?.role === "user" && textFromContent(message.content) === "hello",
|
||||||
);
|
);
|
||||||
const firstAssistantIndex = messages.findIndex((message) => message?.role === "assistant");
|
const firstAssistantIndex = messages.findIndex((message) => message?.role === "assistant");
|
||||||
expect(firstUserIndex).toBeGreaterThanOrEqual(0);
|
expect(firstUserIndex).toBeGreaterThanOrEqual(0);
|
||||||
if (firstAssistantIndex !== -1) {
|
if (firstAssistantIndex !== -1) {
|
||||||
expect(firstUserIndex).toBeLessThan(firstAssistantIndex);
|
expect(firstUserIndex).toBeLessThan(firstAssistantIndex);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("persists the user message when prompt fails before assistant output", async () => {
|
it("persists the user message when prompt fails before assistant output", async () => {
|
||||||
const sessionFile = nextSessionFile();
|
const sessionFile = nextSessionFile();
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
messageChannel?: string;
|
messageChannel?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
|
/** Group id for channel-level tool policy resolution. */
|
||||||
|
groupId?: string | null;
|
||||||
|
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||||
|
groupChannel?: string | null;
|
||||||
|
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||||
|
groupSpace?: string | null;
|
||||||
|
/** Parent session key for subagent policy inheritance. */
|
||||||
|
spawnedBy?: string | null;
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
@@ -207,6 +215,10 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
|
groupId: params.groupId,
|
||||||
|
groupChannel: params.groupChannel,
|
||||||
|
groupSpace: params.groupSpace,
|
||||||
|
spawnedBy: params.spawnedBy,
|
||||||
agentDir,
|
agentDir,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { SessionManager } from "@mariozechner/pi-coding-agent";
|
|||||||
|
|
||||||
import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js";
|
import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js";
|
||||||
import {
|
import {
|
||||||
|
downgradeOpenAIReasoningBlocks,
|
||||||
isCompactionFailureError,
|
isCompactionFailureError,
|
||||||
isGoogleModelApi,
|
isGoogleModelApi,
|
||||||
sanitizeGoogleTurnOrdering,
|
sanitizeGoogleTurnOrdering,
|
||||||
@@ -211,7 +212,50 @@ registerUnhandledRejectionHandler((reason) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
type CustomEntryLike = { type?: unknown; customType?: unknown };
|
type CustomEntryLike = { type?: unknown; customType?: unknown; data?: unknown };
|
||||||
|
|
||||||
|
type ModelSnapshotEntry = {
|
||||||
|
timestamp: number;
|
||||||
|
provider?: string;
|
||||||
|
modelApi?: string | null;
|
||||||
|
modelId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODEL_SNAPSHOT_CUSTOM_TYPE = "model-snapshot";
|
||||||
|
|
||||||
|
function readLastModelSnapshot(sessionManager: SessionManager): ModelSnapshotEntry | null {
|
||||||
|
try {
|
||||||
|
const entries = sessionManager.getEntries();
|
||||||
|
for (let i = entries.length - 1; i >= 0; i--) {
|
||||||
|
const entry = entries[i] as CustomEntryLike;
|
||||||
|
if (entry?.type !== "custom" || entry?.customType !== MODEL_SNAPSHOT_CUSTOM_TYPE) continue;
|
||||||
|
const data = entry?.data as ModelSnapshotEntry | undefined;
|
||||||
|
if (data && typeof data === "object") {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendModelSnapshot(sessionManager: SessionManager, data: ModelSnapshotEntry): void {
|
||||||
|
try {
|
||||||
|
sessionManager.appendCustomEntry(MODEL_SNAPSHOT_CUSTOM_TYPE, data);
|
||||||
|
} catch {
|
||||||
|
// ignore persistence failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameModelSnapshot(a: ModelSnapshotEntry, b: ModelSnapshotEntry): boolean {
|
||||||
|
const normalize = (value?: string | null) => value ?? "";
|
||||||
|
return (
|
||||||
|
normalize(a.provider) === normalize(b.provider) &&
|
||||||
|
normalize(a.modelApi) === normalize(b.modelApi) &&
|
||||||
|
normalize(a.modelId) === normalize(b.modelId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasGoogleTurnOrderingMarker(sessionManager: SessionManager): boolean {
|
function hasGoogleTurnOrderingMarker(sessionManager: SessionManager): boolean {
|
||||||
try {
|
try {
|
||||||
@@ -292,12 +336,38 @@ export async function sanitizeSessionHistory(params: {
|
|||||||
? sanitizeToolUseResultPairing(sanitizedThinking)
|
? sanitizeToolUseResultPairing(sanitizedThinking)
|
||||||
: sanitizedThinking;
|
: sanitizedThinking;
|
||||||
|
|
||||||
|
const isOpenAIResponsesApi =
|
||||||
|
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
|
||||||
|
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
|
||||||
|
const priorSnapshot = hasSnapshot ? readLastModelSnapshot(params.sessionManager) : null;
|
||||||
|
const modelChanged = priorSnapshot
|
||||||
|
? !isSameModelSnapshot(priorSnapshot, {
|
||||||
|
timestamp: 0,
|
||||||
|
provider: params.provider,
|
||||||
|
modelApi: params.modelApi,
|
||||||
|
modelId: params.modelId,
|
||||||
|
})
|
||||||
|
: false;
|
||||||
|
const sanitizedOpenAI =
|
||||||
|
isOpenAIResponsesApi && modelChanged
|
||||||
|
? downgradeOpenAIReasoningBlocks(repairedTools)
|
||||||
|
: repairedTools;
|
||||||
|
|
||||||
|
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
|
||||||
|
appendModelSnapshot(params.sessionManager, {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
provider: params.provider,
|
||||||
|
modelApi: params.modelApi,
|
||||||
|
modelId: params.modelId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!policy.applyGoogleTurnOrdering) {
|
if (!policy.applyGoogleTurnOrdering) {
|
||||||
return repairedTools;
|
return sanitizedOpenAI;
|
||||||
}
|
}
|
||||||
|
|
||||||
return applyGoogleTurnOrderingFix({
|
return applyGoogleTurnOrderingFix({
|
||||||
messages: repairedTools,
|
messages: sanitizedOpenAI,
|
||||||
modelApi: params.modelApi,
|
modelApi: params.modelApi,
|
||||||
sessionManager: params.sessionManager,
|
sessionManager: params.sessionManager,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
isRateLimitAssistantError,
|
isRateLimitAssistantError,
|
||||||
isTimeoutErrorMessage,
|
isTimeoutErrorMessage,
|
||||||
pickFallbackThinkingLevel,
|
pickFallbackThinkingLevel,
|
||||||
|
type FailoverReason,
|
||||||
} from "../pi-embedded-helpers.js";
|
} from "../pi-embedded-helpers.js";
|
||||||
import { normalizeUsage, type UsageLike } from "../usage.js";
|
import { normalizeUsage, type UsageLike } from "../usage.js";
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ export async function runEmbeddedPiAgent(
|
|||||||
const globalLane = resolveGlobalLane(params.lane);
|
const globalLane = resolveGlobalLane(params.lane);
|
||||||
const enqueueGlobal =
|
const enqueueGlobal =
|
||||||
params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
|
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 channelHint = params.messageChannel ?? params.messageProvider;
|
||||||
const resolvedToolResultFormat =
|
const resolvedToolResultFormat =
|
||||||
params.toolResultFormat ??
|
params.toolResultFormat ??
|
||||||
@@ -81,7 +84,7 @@ export async function runEmbeddedPiAgent(
|
|||||||
: "markdown");
|
: "markdown");
|
||||||
const isProbeSession = params.sessionId?.startsWith("probe-") ?? false;
|
const isProbeSession = params.sessionId?.startsWith("probe-") ?? false;
|
||||||
|
|
||||||
return enqueueCommandInLane(sessionLane, () =>
|
return enqueueSession(() =>
|
||||||
enqueueGlobal(async () => {
|
enqueueGlobal(async () => {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
@@ -90,6 +93,8 @@ export async function runEmbeddedPiAgent(
|
|||||||
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||||
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
||||||
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
||||||
|
const fallbackConfigured =
|
||||||
|
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||||
await ensureClawdbotModelsJson(params.config, agentDir);
|
await ensureClawdbotModelsJson(params.config, agentDir);
|
||||||
|
|
||||||
const { model, error, authStorage, modelRegistry } = resolveModel(
|
const { model, error, authStorage, modelRegistry } = resolveModel(
|
||||||
@@ -163,6 +168,42 @@ export async function runEmbeddedPiAgent(
|
|||||||
let apiKeyInfo: ApiKeyInfo | null = null;
|
let apiKeyInfo: ApiKeyInfo | null = null;
|
||||||
let lastProfileId: string | undefined;
|
let lastProfileId: string | undefined;
|
||||||
|
|
||||||
|
const resolveAuthProfileFailoverReason = (params: {
|
||||||
|
allInCooldown: boolean;
|
||||||
|
message: string;
|
||||||
|
}): FailoverReason => {
|
||||||
|
if (params.allInCooldown) return "rate_limit";
|
||||||
|
const classified = classifyFailoverReason(params.message);
|
||||||
|
return classified ?? "auth";
|
||||||
|
};
|
||||||
|
|
||||||
|
const throwAuthProfileFailover = (params: {
|
||||||
|
allInCooldown: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: unknown;
|
||||||
|
}): never => {
|
||||||
|
const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
|
||||||
|
const message =
|
||||||
|
params.message?.trim() ||
|
||||||
|
(params.error ? describeUnknownError(params.error).trim() : "") ||
|
||||||
|
fallbackMessage;
|
||||||
|
const reason = resolveAuthProfileFailoverReason({
|
||||||
|
allInCooldown: params.allInCooldown,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
if (fallbackConfigured) {
|
||||||
|
throw new FailoverError(message, {
|
||||||
|
reason,
|
||||||
|
provider,
|
||||||
|
model: modelId,
|
||||||
|
status: resolveFailoverStatus(reason),
|
||||||
|
cause: params.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (params.error instanceof Error) throw params.error;
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
const resolveApiKeyForCandidate = async (candidate?: string) => {
|
const resolveApiKeyForCandidate = async (candidate?: string) => {
|
||||||
return getApiKeyForModel({
|
return getApiKeyForModel({
|
||||||
model,
|
model,
|
||||||
@@ -236,14 +277,17 @@ export async function runEmbeddedPiAgent(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (profileIndex >= profileCandidates.length) {
|
if (profileIndex >= profileCandidates.length) {
|
||||||
throw new Error(
|
throwAuthProfileFailover({ allInCooldown: true });
|
||||||
`No available auth profile for ${provider} (all in cooldown or unavailable).`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (profileCandidates[profileIndex] === lockedProfileId) throw err;
|
if (err instanceof FailoverError) throw err;
|
||||||
|
if (profileCandidates[profileIndex] === lockedProfileId) {
|
||||||
|
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
||||||
|
}
|
||||||
const advanced = await advanceAuthProfile();
|
const advanced = await advanceAuthProfile();
|
||||||
if (!advanced) throw err;
|
if (!advanced) {
|
||||||
|
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -262,6 +306,10 @@ export async function runEmbeddedPiAgent(
|
|||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
messageTo: params.messageTo,
|
messageTo: params.messageTo,
|
||||||
messageThreadId: params.messageThreadId,
|
messageThreadId: params.messageThreadId,
|
||||||
|
groupId: params.groupId,
|
||||||
|
groupChannel: params.groupChannel,
|
||||||
|
groupSpace: params.groupSpace,
|
||||||
|
spawnedBy: params.spawnedBy,
|
||||||
currentChannelId: params.currentChannelId,
|
currentChannelId: params.currentChannelId,
|
||||||
currentThreadTs: params.currentThreadTs,
|
currentThreadTs: params.currentThreadTs,
|
||||||
replyToMode: params.replyToMode,
|
replyToMode: params.replyToMode,
|
||||||
@@ -273,6 +321,7 @@ export async function runEmbeddedPiAgent(
|
|||||||
skillsSnapshot: params.skillsSnapshot,
|
skillsSnapshot: params.skillsSnapshot,
|
||||||
prompt,
|
prompt,
|
||||||
images: params.images,
|
images: params.images,
|
||||||
|
disableTools: params.disableTools,
|
||||||
provider,
|
provider,
|
||||||
modelId,
|
modelId,
|
||||||
model,
|
model,
|
||||||
@@ -386,9 +435,7 @@ export async function runEmbeddedPiAgent(
|
|||||||
}
|
}
|
||||||
// FIX: Throw FailoverError for prompt errors when fallbacks configured
|
// FIX: Throw FailoverError for prompt errors when fallbacks configured
|
||||||
// This enables model fallback for quota/rate limit errors during prompt submission
|
// This enables model fallback for quota/rate limit errors during prompt submission
|
||||||
const promptFallbackConfigured =
|
if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
|
||||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
|
||||||
if (promptFallbackConfigured && isFailoverErrorMessage(errorText)) {
|
|
||||||
throw new FailoverError(errorText, {
|
throw new FailoverError(errorText, {
|
||||||
reason: promptFailoverReason ?? "unknown",
|
reason: promptFailoverReason ?? "unknown",
|
||||||
provider,
|
provider,
|
||||||
@@ -412,8 +459,6 @@ export async function runEmbeddedPiAgent(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackConfigured =
|
|
||||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
|
||||||
const authFailure = isAuthAssistantError(lastAssistant);
|
const authFailure = isAuthAssistantError(lastAssistant);
|
||||||
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
||||||
const failoverFailure = isFailoverAssistantError(lastAssistant);
|
const failoverFailure = isFailoverAssistantError(lastAssistant);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
|||||||
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||||
import { resolveUserPath } from "../../../utils.js";
|
import { resolveUserPath } from "../../../utils.js";
|
||||||
import { createCacheTrace } from "../../cache-trace.js";
|
import { createCacheTrace } from "../../cache-trace.js";
|
||||||
|
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
||||||
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
||||||
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||||
@@ -196,30 +197,36 @@ export async function runEmbeddedAttempt(
|
|||||||
|
|
||||||
// Check if the model supports native image input
|
// Check if the model supports native image input
|
||||||
const modelHasVision = params.model.input?.includes("image") ?? false;
|
const modelHasVision = params.model.input?.includes("image") ?? false;
|
||||||
const toolsRaw = createClawdbotCodingTools({
|
const toolsRaw = params.disableTools
|
||||||
exec: {
|
? []
|
||||||
...params.execOverrides,
|
: createClawdbotCodingTools({
|
||||||
elevated: params.bashElevated,
|
exec: {
|
||||||
},
|
...params.execOverrides,
|
||||||
sandbox,
|
elevated: params.bashElevated,
|
||||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
},
|
||||||
agentAccountId: params.agentAccountId,
|
sandbox,
|
||||||
messageTo: params.messageTo,
|
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||||
messageThreadId: params.messageThreadId,
|
agentAccountId: params.agentAccountId,
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
messageTo: params.messageTo,
|
||||||
agentDir,
|
messageThreadId: params.messageThreadId,
|
||||||
workspaceDir: effectiveWorkspace,
|
groupId: params.groupId,
|
||||||
config: params.config,
|
groupChannel: params.groupChannel,
|
||||||
abortSignal: runAbortController.signal,
|
groupSpace: params.groupSpace,
|
||||||
modelProvider: params.model.provider,
|
spawnedBy: params.spawnedBy,
|
||||||
modelId: params.modelId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
|
agentDir,
|
||||||
currentChannelId: params.currentChannelId,
|
workspaceDir: effectiveWorkspace,
|
||||||
currentThreadTs: params.currentThreadTs,
|
config: params.config,
|
||||||
replyToMode: params.replyToMode,
|
abortSignal: runAbortController.signal,
|
||||||
hasRepliedRef: params.hasRepliedRef,
|
modelProvider: params.model.provider,
|
||||||
modelHasVision,
|
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 });
|
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
|
||||||
logToolSchemasForGoogle({ tools, provider: params.provider });
|
logToolSchemasForGoogle({ tools, provider: params.provider });
|
||||||
|
|
||||||
@@ -452,6 +459,16 @@ export async function runEmbeddedAttempt(
|
|||||||
modelApi: params.model.api,
|
modelApi: params.model.api,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
});
|
});
|
||||||
|
const anthropicPayloadLogger = createAnthropicPayloadLogger({
|
||||||
|
env: process.env,
|
||||||
|
runId: params.runId,
|
||||||
|
sessionId: activeSession.sessionId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
provider: params.provider,
|
||||||
|
modelId: params.modelId,
|
||||||
|
modelApi: params.model.api,
|
||||||
|
workspaceDir: params.workspaceDir,
|
||||||
|
});
|
||||||
|
|
||||||
// Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai.
|
// Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai.
|
||||||
activeSession.agent.streamFn = streamSimple;
|
activeSession.agent.streamFn = streamSimple;
|
||||||
@@ -472,6 +489,11 @@ export async function runEmbeddedAttempt(
|
|||||||
});
|
});
|
||||||
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
|
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
|
||||||
}
|
}
|
||||||
|
if (anthropicPayloadLogger) {
|
||||||
|
activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(
|
||||||
|
activeSession.agent.streamFn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prior = await sanitizeSessionHistory({
|
const prior = await sanitizeSessionHistory({
|
||||||
@@ -766,6 +788,7 @@ export async function runEmbeddedAttempt(
|
|||||||
messages: messagesSnapshot,
|
messages: messagesSnapshot,
|
||||||
note: promptError ? "prompt error" : undefined,
|
note: promptError ? "prompt error" : undefined,
|
||||||
});
|
});
|
||||||
|
anthropicPayloadLogger?.recordUsage(messagesSnapshot, promptError);
|
||||||
|
|
||||||
// Run agent_end hooks to allow plugins to analyze the conversation
|
// Run agent_end hooks to allow plugins to analyze the conversation
|
||||||
// This is fire-and-forget, so we don't await
|
// This is fire-and-forget, so we don't await
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ export type RunEmbeddedPiAgentParams = {
|
|||||||
messageTo?: string;
|
messageTo?: string;
|
||||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||||
messageThreadId?: string | number;
|
messageThreadId?: string | number;
|
||||||
|
/** Group id for channel-level tool policy resolution. */
|
||||||
|
groupId?: string | null;
|
||||||
|
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||||
|
groupChannel?: string | null;
|
||||||
|
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||||
|
groupSpace?: string | null;
|
||||||
|
/** Parent session key for subagent policy inheritance. */
|
||||||
|
spawnedBy?: string | null;
|
||||||
/** Current channel ID for auto-threading (Slack). */
|
/** Current channel ID for auto-threading (Slack). */
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
/** Current thread timestamp for auto-threading (Slack). */
|
/** Current thread timestamp for auto-threading (Slack). */
|
||||||
@@ -44,6 +52,8 @@ export type RunEmbeddedPiAgentParams = {
|
|||||||
images?: ImageContent[];
|
images?: ImageContent[];
|
||||||
/** Optional client-provided tools (OpenResponses hosted tools). */
|
/** Optional client-provided tools (OpenResponses hosted tools). */
|
||||||
clientTools?: ClientToolDefinition[];
|
clientTools?: ClientToolDefinition[];
|
||||||
|
/** Disable built-in tools for this run (LLM-only mode). */
|
||||||
|
disableTools?: boolean;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
authProfileId?: string;
|
authProfileId?: string;
|
||||||
|
|||||||
@@ -148,6 +148,35 @@ describe("buildEmbeddedRunPayloads", () => {
|
|||||||
expect(payloads[0]?.text).toBe("All good");
|
expect(payloads[0]?.text).toBe("All good");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds tool error fallback when the assistant only invoked tools", () => {
|
||||||
|
const payloads = buildEmbeddedRunPayloads({
|
||||||
|
assistantTexts: [],
|
||||||
|
toolMetas: [],
|
||||||
|
lastAssistant: {
|
||||||
|
stopReason: "toolUse",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "toolCall",
|
||||||
|
id: "toolu_01",
|
||||||
|
name: "exec",
|
||||||
|
arguments: { command: "echo hi" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as AssistantMessage,
|
||||||
|
lastToolError: { toolName: "exec", error: "Command exited with code 1" },
|
||||||
|
sessionKey: "session:telegram",
|
||||||
|
inlineToolResultsAllowed: false,
|
||||||
|
verboseLevel: "off",
|
||||||
|
reasoningLevel: "off",
|
||||||
|
toolResultFormat: "plain",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payloads).toHaveLength(1);
|
||||||
|
expect(payloads[0]?.isError).toBe(true);
|
||||||
|
expect(payloads[0]?.text).toContain("Exec");
|
||||||
|
expect(payloads[0]?.text).toContain("code 1");
|
||||||
|
});
|
||||||
|
|
||||||
it("suppresses recoverable tool errors containing 'required'", () => {
|
it("suppresses recoverable tool errors containing 'required'", () => {
|
||||||
const payloads = buildEmbeddedRunPayloads({
|
const payloads = buildEmbeddedRunPayloads({
|
||||||
assistantTexts: [],
|
assistantTexts: [],
|
||||||
|
|||||||
@@ -169,7 +169,16 @@ export function buildEmbeddedRunPayloads(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.lastToolError) {
|
if (params.lastToolError) {
|
||||||
const hasUserFacingReply = replyItems.length > 0;
|
const lastAssistantHasToolCalls =
|
||||||
|
Array.isArray(params.lastAssistant?.content) &&
|
||||||
|
params.lastAssistant?.content.some((block) =>
|
||||||
|
block && typeof block === "object"
|
||||||
|
? (block as { type?: unknown }).type === "toolCall"
|
||||||
|
: false,
|
||||||
|
);
|
||||||
|
const lastAssistantWasToolUse = params.lastAssistant?.stopReason === "toolUse";
|
||||||
|
const hasUserFacingReply =
|
||||||
|
replyItems.length > 0 && !lastAssistantHasToolCalls && !lastAssistantWasToolUse;
|
||||||
// Check if this is a recoverable/internal tool error that shouldn't be shown to users
|
// Check if this is a recoverable/internal tool error that shouldn't be shown to users
|
||||||
// when there's already a user-facing reply (the model should have retried).
|
// when there's already a user-facing reply (the model should have retried).
|
||||||
const errorLower = (params.lastToolError.error ?? "").toLowerCase();
|
const errorLower = (params.lastToolError.error ?? "").toLowerCase();
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export type EmbeddedRunAttemptParams = {
|
|||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
messageTo?: string;
|
messageTo?: string;
|
||||||
messageThreadId?: string | number;
|
messageThreadId?: string | number;
|
||||||
|
/** Group id for channel-level tool policy resolution. */
|
||||||
|
groupId?: string | null;
|
||||||
|
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||||
|
groupChannel?: string | null;
|
||||||
|
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||||
|
groupSpace?: string | null;
|
||||||
|
/** Parent session key for subagent policy inheritance. */
|
||||||
|
spawnedBy?: string | null;
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
@@ -36,6 +44,8 @@ export type EmbeddedRunAttemptParams = {
|
|||||||
images?: ImageContent[];
|
images?: ImageContent[];
|
||||||
/** Optional client-provided tools (OpenResponses hosted tools). */
|
/** Optional client-provided tools (OpenResponses hosted tools). */
|
||||||
clientTools?: ClientToolDefinition[];
|
clientTools?: ClientToolDefinition[];
|
||||||
|
/** Disable built-in tools for this run (LLM-only mode). */
|
||||||
|
disableTools?: boolean;
|
||||||
provider: string;
|
provider: string;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
model: Model<Api>;
|
model: Model<Api>;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function abortEmbeddedPiRun(sessionId: string): boolean {
|
|||||||
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
|
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
diag.info(`aborting run: sessionId=${sessionId}`);
|
diag.debug(`aborting run: sessionId=${sessionId}`);
|
||||||
handle.abort();
|
handle.abort();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ export function setActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueueH
|
|||||||
reason: wasActive ? "run_replaced" : "run_started",
|
reason: wasActive ? "run_replaced" : "run_started",
|
||||||
});
|
});
|
||||||
if (!sessionId.startsWith("probe-")) {
|
if (!sessionId.startsWith("probe-")) {
|
||||||
diag.info(`run registered: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
diag.debug(`run registered: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export function clearActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueu
|
|||||||
ACTIVE_EMBEDDED_RUNS.delete(sessionId);
|
ACTIVE_EMBEDDED_RUNS.delete(sessionId);
|
||||||
logSessionStateChange({ sessionId, state: "idle", reason: "run_completed" });
|
logSessionStateChange({ sessionId, state: "idle", reason: "run_completed" });
|
||||||
if (!sessionId.startsWith("probe-")) {
|
if (!sessionId.startsWith("probe-")) {
|
||||||
diag.info(`run cleared: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
diag.debug(`run cleared: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
||||||
}
|
}
|
||||||
notifyEmbeddedRunEnded(sessionId);
|
notifyEmbeddedRunEnded(sessionId);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -231,6 +231,95 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
expect(familyToolNames).not.toContain("apply_patch");
|
expect(familyToolNames).not.toContain("apply_patch");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should apply group tool policy overrides (group-specific beats wildcard)", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
channels: {
|
||||||
|
whatsapp: {
|
||||||
|
groups: {
|
||||||
|
"*": {
|
||||||
|
tools: { allow: ["read"] },
|
||||||
|
},
|
||||||
|
trusted: {
|
||||||
|
tools: { allow: ["read", "exec"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const trustedTools = createClawdbotCodingTools({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:main:whatsapp:group:trusted",
|
||||||
|
messageProvider: "whatsapp",
|
||||||
|
workspaceDir: "/tmp/test-group-trusted",
|
||||||
|
agentDir: "/tmp/agent-group",
|
||||||
|
});
|
||||||
|
const trustedNames = trustedTools.map((t) => t.name);
|
||||||
|
expect(trustedNames).toContain("read");
|
||||||
|
expect(trustedNames).toContain("exec");
|
||||||
|
|
||||||
|
const defaultTools = createClawdbotCodingTools({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:main:whatsapp:group:unknown",
|
||||||
|
messageProvider: "whatsapp",
|
||||||
|
workspaceDir: "/tmp/test-group-default",
|
||||||
|
agentDir: "/tmp/agent-group",
|
||||||
|
});
|
||||||
|
const defaultNames = defaultTools.map((t) => t.name);
|
||||||
|
expect(defaultNames).toContain("read");
|
||||||
|
expect(defaultNames).not.toContain("exec");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve telegram group tool policy for topic session keys", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
groups: {
|
||||||
|
"123": {
|
||||||
|
tools: { allow: ["read"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools = createClawdbotCodingTools({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:main:telegram:group:123:topic:456",
|
||||||
|
messageProvider: "telegram",
|
||||||
|
workspaceDir: "/tmp/test-telegram-topic",
|
||||||
|
agentDir: "/tmp/agent-telegram",
|
||||||
|
});
|
||||||
|
const names = tools.map((t) => t.name);
|
||||||
|
expect(names).toContain("read");
|
||||||
|
expect(names).not.toContain("exec");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should inherit group tool policy for subagents from spawnedBy session keys", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
channels: {
|
||||||
|
whatsapp: {
|
||||||
|
groups: {
|
||||||
|
trusted: {
|
||||||
|
tools: { allow: ["read"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools = createClawdbotCodingTools({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:main:subagent:test",
|
||||||
|
spawnedBy: "agent:main:whatsapp:group:trusted",
|
||||||
|
workspaceDir: "/tmp/test-subagent-group",
|
||||||
|
agentDir: "/tmp/agent-subagent",
|
||||||
|
});
|
||||||
|
const names = tools.map((t) => t.name);
|
||||||
|
expect(names).toContain("read");
|
||||||
|
expect(names).not.toContain("exec");
|
||||||
|
});
|
||||||
|
|
||||||
it("should apply global tool policy before agent-specific policy", () => {
|
it("should apply global tool policy before agent-specific policy", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
tools: {
|
tools: {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+114
-12
@@ -1,8 +1,58 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import { getChannelDock } from "../channels/dock.js";
|
||||||
|
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
|
||||||
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
||||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||||
import type { SandboxToolPolicy } from "./sandbox.js";
|
import type { SandboxToolPolicy } from "./sandbox.js";
|
||||||
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
||||||
|
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||||
|
import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js";
|
||||||
|
|
||||||
|
type CompiledPattern =
|
||||||
|
| { 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 = [
|
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||||
// Session management - main agent orchestrates
|
// Session management - main agent orchestrates
|
||||||
@@ -35,22 +85,13 @@ export function resolveSubagentToolPolicy(cfg?: ClawdbotConfig): SandboxToolPoli
|
|||||||
|
|
||||||
export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean {
|
export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean {
|
||||||
if (!policy) return true;
|
if (!policy) return true;
|
||||||
const deny = new Set(expandToolGroups(policy.deny));
|
return makeToolPolicyMatcher(policy)(name);
|
||||||
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) {
|
export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) {
|
||||||
if (!policy) return tools;
|
if (!policy) return tools;
|
||||||
return tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy));
|
const matcher = makeToolPolicyMatcher(policy);
|
||||||
|
return tools.filter((tool) => matcher(tool.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolPolicyConfig = {
|
type ToolPolicyConfig = {
|
||||||
@@ -71,6 +112,26 @@ function normalizeProviderKey(value: string): string {
|
|||||||
return value.trim().toLowerCase();
|
return value.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveGroupContextFromSessionKey(sessionKey?: string | null): {
|
||||||
|
channel?: string;
|
||||||
|
groupId?: string;
|
||||||
|
} {
|
||||||
|
const raw = (sessionKey ?? "").trim();
|
||||||
|
if (!raw) return {};
|
||||||
|
const base = resolveThreadParentSessionKey(raw) ?? raw;
|
||||||
|
const parts = base.split(":").filter(Boolean);
|
||||||
|
let body = parts[0] === "agent" ? parts.slice(2) : parts;
|
||||||
|
if (body[0] === "subagent") {
|
||||||
|
body = body.slice(1);
|
||||||
|
}
|
||||||
|
if (body.length < 3) return {};
|
||||||
|
const [channel, kind, ...rest] = body;
|
||||||
|
if (kind !== "group" && kind !== "channel") return {};
|
||||||
|
const groupId = rest.join(":").trim();
|
||||||
|
if (!groupId) return {};
|
||||||
|
return { channel: channel.trim().toLowerCase(), groupId };
|
||||||
|
}
|
||||||
|
|
||||||
function resolveProviderToolPolicy(params: {
|
function resolveProviderToolPolicy(params: {
|
||||||
byProvider?: Record<string, ToolPolicyConfig>;
|
byProvider?: Record<string, ToolPolicyConfig>;
|
||||||
modelProvider?: string;
|
modelProvider?: string;
|
||||||
@@ -137,6 +198,47 @@ export function resolveEffectiveToolPolicy(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveGroupToolPolicy(params: {
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
sessionKey?: string;
|
||||||
|
spawnedBy?: string | null;
|
||||||
|
messageProvider?: string;
|
||||||
|
groupId?: string | null;
|
||||||
|
groupChannel?: string | null;
|
||||||
|
groupSpace?: string | null;
|
||||||
|
accountId?: string | null;
|
||||||
|
}): SandboxToolPolicy | undefined {
|
||||||
|
if (!params.config) return undefined;
|
||||||
|
const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey);
|
||||||
|
const spawnedContext = resolveGroupContextFromSessionKey(params.spawnedBy);
|
||||||
|
const groupId = params.groupId ?? sessionContext.groupId ?? spawnedContext.groupId;
|
||||||
|
if (!groupId) return undefined;
|
||||||
|
const channelRaw = params.messageProvider ?? sessionContext.channel ?? spawnedContext.channel;
|
||||||
|
const channel = normalizeMessageChannel(channelRaw);
|
||||||
|
if (!channel) return undefined;
|
||||||
|
let dock;
|
||||||
|
try {
|
||||||
|
dock = getChannelDock(channel);
|
||||||
|
} catch {
|
||||||
|
dock = undefined;
|
||||||
|
}
|
||||||
|
const toolsConfig =
|
||||||
|
dock?.groups?.resolveToolPolicy?.({
|
||||||
|
cfg: params.config,
|
||||||
|
groupId,
|
||||||
|
groupChannel: params.groupChannel,
|
||||||
|
groupSpace: params.groupSpace,
|
||||||
|
accountId: params.accountId,
|
||||||
|
}) ??
|
||||||
|
resolveChannelGroupToolsPolicy({
|
||||||
|
cfg: params.config,
|
||||||
|
channel,
|
||||||
|
groupId,
|
||||||
|
accountId: params.accountId,
|
||||||
|
});
|
||||||
|
return pickToolPolicy(toolsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
export function isToolAllowedByPolicies(
|
export function isToolAllowedByPolicies(
|
||||||
name: string,
|
name: string,
|
||||||
policies: Array<SandboxToolPolicy | undefined>,
|
policies: Array<SandboxToolPolicy | undefined>,
|
||||||
|
|||||||
+63
-28
@@ -23,6 +23,7 @@ import {
|
|||||||
filterToolsByPolicy,
|
filterToolsByPolicy,
|
||||||
isToolAllowedByPolicies,
|
isToolAllowedByPolicies,
|
||||||
resolveEffectiveToolPolicy,
|
resolveEffectiveToolPolicy,
|
||||||
|
resolveGroupToolPolicy,
|
||||||
resolveSubagentToolPolicy,
|
resolveSubagentToolPolicy,
|
||||||
} from "./pi-tools.policy.js";
|
} from "./pi-tools.policy.js";
|
||||||
import {
|
import {
|
||||||
@@ -43,10 +44,12 @@ import {
|
|||||||
buildPluginToolGroups,
|
buildPluginToolGroups,
|
||||||
collectExplicitAllowlist,
|
collectExplicitAllowlist,
|
||||||
expandPolicyWithPluginGroups,
|
expandPolicyWithPluginGroups,
|
||||||
|
normalizeToolName,
|
||||||
resolveToolProfilePolicy,
|
resolveToolProfilePolicy,
|
||||||
stripPluginOnlyAllowlist,
|
stripPluginOnlyAllowlist,
|
||||||
} from "./tool-policy.js";
|
} from "./tool-policy.js";
|
||||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||||
|
import { logWarn } from "../logger.js";
|
||||||
|
|
||||||
function isOpenAIProvider(provider?: string) {
|
function isOpenAIProvider(provider?: string) {
|
||||||
const normalized = provider?.trim().toLowerCase();
|
const normalized = provider?.trim().toLowerCase();
|
||||||
@@ -128,6 +131,14 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
/** Current thread timestamp for auto-threading (Slack). */
|
/** Current thread timestamp for auto-threading (Slack). */
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
|
/** Group id for channel-level tool policy resolution. */
|
||||||
|
groupId?: string | null;
|
||||||
|
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||||
|
groupChannel?: string | null;
|
||||||
|
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||||
|
groupSpace?: string | null;
|
||||||
|
/** Parent session key for subagent group policy inheritance. */
|
||||||
|
spawnedBy?: string | null;
|
||||||
/** Reply-to mode for Slack auto-threading. */
|
/** Reply-to mode for Slack auto-threading. */
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||||
@@ -151,6 +162,16 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
modelProvider: options?.modelProvider,
|
modelProvider: options?.modelProvider,
|
||||||
modelId: options?.modelId,
|
modelId: options?.modelId,
|
||||||
});
|
});
|
||||||
|
const groupPolicy = resolveGroupToolPolicy({
|
||||||
|
config: options?.config,
|
||||||
|
sessionKey: options?.sessionKey,
|
||||||
|
spawnedBy: options?.spawnedBy,
|
||||||
|
messageProvider: options?.messageProvider,
|
||||||
|
groupId: options?.groupId,
|
||||||
|
groupChannel: options?.groupChannel,
|
||||||
|
groupSpace: options?.groupSpace,
|
||||||
|
accountId: options?.agentAccountId,
|
||||||
|
});
|
||||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||||
@@ -165,6 +186,7 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
globalProviderPolicy,
|
globalProviderPolicy,
|
||||||
agentPolicy,
|
agentPolicy,
|
||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
|
groupPolicy,
|
||||||
sandbox?.tools,
|
sandbox?.tools,
|
||||||
subagentPolicy,
|
subagentPolicy,
|
||||||
]);
|
]);
|
||||||
@@ -233,11 +255,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
const bashTool = {
|
|
||||||
...(execTool as unknown as AnyAgentTool),
|
|
||||||
name: "bash",
|
|
||||||
label: "bash",
|
|
||||||
} satisfies AnyAgentTool;
|
|
||||||
const processTool = createProcessTool({
|
const processTool = createProcessTool({
|
||||||
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
|
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
|
||||||
scopeKey,
|
scopeKey,
|
||||||
@@ -258,7 +275,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
: []),
|
: []),
|
||||||
...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
|
...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
|
||||||
execTool as unknown as AnyAgentTool,
|
execTool as unknown as AnyAgentTool,
|
||||||
bashTool,
|
|
||||||
processTool as unknown as AnyAgentTool,
|
processTool as unknown as AnyAgentTool,
|
||||||
// Channel docking: include channel-defined agent tools (login, etc.).
|
// Channel docking: include channel-defined agent tools (login, etc.).
|
||||||
...listChannelAgentTools({ cfg: options?.config }),
|
...listChannelAgentTools({ cfg: options?.config }),
|
||||||
@@ -273,6 +289,9 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
agentTo: options?.messageTo,
|
agentTo: options?.messageTo,
|
||||||
agentThreadId: options?.messageThreadId,
|
agentThreadId: options?.messageThreadId,
|
||||||
|
agentGroupId: options?.groupId ?? null,
|
||||||
|
agentGroupChannel: options?.groupChannel ?? null,
|
||||||
|
agentGroupSpace: options?.groupSpace ?? null,
|
||||||
agentDir: options?.agentDir,
|
agentDir: options?.agentDir,
|
||||||
sandboxRoot,
|
sandboxRoot,
|
||||||
workspaceDir: options?.workspaceDir,
|
workspaceDir: options?.workspaceDir,
|
||||||
@@ -285,6 +304,7 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
globalProviderPolicy,
|
globalProviderPolicy,
|
||||||
agentPolicy,
|
agentPolicy,
|
||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
|
groupPolicy,
|
||||||
sandbox?.tools,
|
sandbox?.tools,
|
||||||
subagentPolicy,
|
subagentPolicy,
|
||||||
]),
|
]),
|
||||||
@@ -295,34 +315,46 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
modelHasVision: options?.modelHasVision,
|
modelHasVision: options?.modelHasVision,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
const coreToolNames = new Set(
|
||||||
|
tools
|
||||||
|
.filter((tool) => !getPluginToolMeta(tool as AnyAgentTool))
|
||||||
|
.map((tool) => normalizeToolName(tool.name))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
const pluginGroups = buildPluginToolGroups({
|
const pluginGroups = buildPluginToolGroups({
|
||||||
tools,
|
tools,
|
||||||
toolMeta: (tool) => getPluginToolMeta(tool as AnyAgentTool),
|
toolMeta: (tool) => getPluginToolMeta(tool as AnyAgentTool),
|
||||||
});
|
});
|
||||||
const profilePolicyExpanded = expandPolicyWithPluginGroups(
|
const resolvePolicy = (policy: typeof profilePolicy, label: string) => {
|
||||||
stripPluginOnlyAllowlist(profilePolicy, pluginGroups),
|
const resolved = stripPluginOnlyAllowlist(policy, pluginGroups, coreToolNames);
|
||||||
pluginGroups,
|
if (resolved.unknownAllowlist.length > 0) {
|
||||||
|
const entries = resolved.unknownAllowlist.join(", ");
|
||||||
|
const suffix = resolved.strippedAllowlist
|
||||||
|
? "Ignoring allowlist so core tools remain available."
|
||||||
|
: "These entries won't match any tool unless the plugin is enabled.";
|
||||||
|
logWarn(`tools: ${label} allowlist contains unknown entries (${entries}). ${suffix}`);
|
||||||
|
}
|
||||||
|
return expandPolicyWithPluginGroups(resolved.policy, pluginGroups);
|
||||||
|
};
|
||||||
|
const profilePolicyExpanded = resolvePolicy(
|
||||||
|
profilePolicy,
|
||||||
|
profile ? `tools.profile (${profile})` : "tools.profile",
|
||||||
);
|
);
|
||||||
const providerProfileExpanded = expandPolicyWithPluginGroups(
|
const providerProfileExpanded = resolvePolicy(
|
||||||
stripPluginOnlyAllowlist(providerProfilePolicy, pluginGroups),
|
providerProfilePolicy,
|
||||||
pluginGroups,
|
providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile",
|
||||||
);
|
);
|
||||||
const globalPolicyExpanded = expandPolicyWithPluginGroups(
|
const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow");
|
||||||
stripPluginOnlyAllowlist(globalPolicy, pluginGroups),
|
const globalProviderExpanded = resolvePolicy(globalProviderPolicy, "tools.byProvider.allow");
|
||||||
pluginGroups,
|
const agentPolicyExpanded = resolvePolicy(
|
||||||
|
agentPolicy,
|
||||||
|
agentId ? `agents.${agentId}.tools.allow` : "agent tools.allow",
|
||||||
);
|
);
|
||||||
const globalProviderExpanded = expandPolicyWithPluginGroups(
|
const agentProviderExpanded = resolvePolicy(
|
||||||
stripPluginOnlyAllowlist(globalProviderPolicy, pluginGroups),
|
agentProviderPolicy,
|
||||||
pluginGroups,
|
agentId ? `agents.${agentId}.tools.byProvider.allow` : "agent tools.byProvider.allow",
|
||||||
);
|
|
||||||
const agentPolicyExpanded = expandPolicyWithPluginGroups(
|
|
||||||
stripPluginOnlyAllowlist(agentPolicy, pluginGroups),
|
|
||||||
pluginGroups,
|
|
||||||
);
|
|
||||||
const agentProviderExpanded = expandPolicyWithPluginGroups(
|
|
||||||
stripPluginOnlyAllowlist(agentProviderPolicy, pluginGroups),
|
|
||||||
pluginGroups,
|
|
||||||
);
|
);
|
||||||
|
const groupPolicyExpanded = resolvePolicy(groupPolicy, "group tools.allow");
|
||||||
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
||||||
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
||||||
|
|
||||||
@@ -344,9 +376,12 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
const agentProviderFiltered = agentProviderExpanded
|
const agentProviderFiltered = agentProviderExpanded
|
||||||
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
||||||
: agentFiltered;
|
: agentFiltered;
|
||||||
const sandboxed = sandboxPolicyExpanded
|
const groupFiltered = groupPolicyExpanded
|
||||||
? filterToolsByPolicy(agentProviderFiltered, sandboxPolicyExpanded)
|
? filterToolsByPolicy(agentProviderFiltered, groupPolicyExpanded)
|
||||||
: agentProviderFiltered;
|
: agentProviderFiltered;
|
||||||
|
const sandboxed = sandboxPolicyExpanded
|
||||||
|
? filterToolsByPolicy(groupFiltered, sandboxPolicyExpanded)
|
||||||
|
: groupFiltered;
|
||||||
const subagentFiltered = subagentPolicyExpanded
|
const subagentFiltered = subagentPolicyExpanded
|
||||||
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
||||||
: sandboxed;
|
: sandboxed;
|
||||||
|
|||||||
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user