Compare commits
2 Commits
pr/remove-
...
fix/trigge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fbf728298 | ||
|
|
16c4a3aea6 |
15
AGENTS.md
15
AGENTS.md
@@ -7,7 +7,6 @@
|
||||
- Tests: colocated `*.test.ts`.
|
||||
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
|
||||
- Plugins/extensions: live under `extensions/*` (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them.
|
||||
- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `clawdbot` in `devDependencies` or `peerDependencies` instead (runtime resolves `clawdbot/plugin-sdk` via jiti alias).
|
||||
- Installers served from `https://clawd.bot/*`: live in the sibling repo `../clawd.bot` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`).
|
||||
- Messaging channels: always consider **all** built-in + extension channels when refactoring shared logic (routing, allowlists, pairing, command gating, onboarding, docs).
|
||||
- Core channel docs: `docs/channels/`
|
||||
@@ -24,14 +23,13 @@
|
||||
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”.
|
||||
|
||||
## exe.dev VM ops (general)
|
||||
- Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set).
|
||||
- SSH flaky: use exe.dev web terminal or Shelley (web agent); keep a tmux session for long ops.
|
||||
- Update: `sudo npm i -g clawdbot@latest` (global install needs root on `/usr/lib/node_modules`).
|
||||
- Config: use `clawdbot config set ...`; ensure `gateway.mode=local` is set.
|
||||
- Discord: store raw token only (no `DISCORD_BOT_TOKEN=` prefix).
|
||||
- Restart: stop old gateway and run:
|
||||
- Access: SSH to the VM directly: `ssh vm-name.exe.xyz` (or use exe.dev web terminal).
|
||||
- Updates: `sudo npm i -g clawdbot@latest` (global install needs root on `/usr/lib/node_modules`).
|
||||
- Config: use `clawdbot config set ...`; set `gateway.mode=local` if unset.
|
||||
- Restart: exe.dev often lacks systemd user bus; stop old gateway and run:
|
||||
`pkill -9 -f clawdbot-gateway || true; nohup clawdbot gateway run --bind loopback --port 18789 --force > /tmp/clawdbot-gateway.log 2>&1 &`
|
||||
- Verify: `clawdbot channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/clawdbot-gateway.log`.
|
||||
- Verify: `clawdbot --version`, `clawdbot health`, `ss -ltnp | rg 18789`.
|
||||
- SSH flaky: use exe.dev web terminal or Shelley (web agent) instead of CLI SSH.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
|
||||
@@ -77,7 +75,6 @@
|
||||
- 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.
|
||||
- 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`.
|
||||
- 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.
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -2,61 +2,24 @@
|
||||
|
||||
Docs: https://docs.clawd.bot
|
||||
|
||||
## 2026.1.23 (Unreleased)
|
||||
## 2026.1.23
|
||||
|
||||
### 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: add live auth probes to `clawdbot models status` for per-profile verification.
|
||||
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
|
||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
||||
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
|
||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
||||
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
|
||||
|
||||
### Fixes
|
||||
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
|
||||
- Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
|
||||
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
||||
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
||||
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
||||
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
|
||||
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
||||
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
||||
- macOS: sync voice wake trigger words on submit/blur in settings. (#1506) Thanks @shiv19.
|
||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
||||
- CLI: suppress diagnostic session/run noise during auth probes.
|
||||
- CLI: hide auth probe timeout warnings from embedded runs.
|
||||
- CLI: render auth probe results as a table in `clawdbot models status`.
|
||||
- CLI: suppress probe-only embedded logs unless `--verbose` is set.
|
||||
- CLI: move auth probe errors below the table to reduce wrapping.
|
||||
- CLI: prevent ANSI color bleed when table cells wrap.
|
||||
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
||||
- CLI: drop the em dash when the banner tagline wraps to a second line.
|
||||
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
||||
- Telegram: render markdown in media captions. (#1478)
|
||||
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
|
||||
- Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
|
||||
- Daemon: use platform PATH delimiters when building minimal service paths.
|
||||
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
||||
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
||||
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
||||
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
|
||||
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
||||
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
||||
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
||||
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
||||
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
|
||||
- Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)
|
||||
|
||||
## 2026.1.22
|
||||
|
||||
|
||||
48
README.md
48
README.md
@@ -478,30 +478,28 @@ Thanks to all clawtributors:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
|
||||
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/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/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/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a>
|
||||
<a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a>
|
||||
<a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a>
|
||||
<a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a>
|
||||
<a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a>
|
||||
<a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a>
|
||||
<a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/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/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a>
|
||||
<a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a>
|
||||
<a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a>
|
||||
<a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a>
|
||||
<a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/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/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a>
|
||||
<a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
|
||||
<a href="https://github.com/search?q=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/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/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a>
|
||||
<a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
|
||||
<a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a>
|
||||
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a>
|
||||
<a href="https://github.com/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/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/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/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/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/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/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a>
|
||||
<a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/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/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a>
|
||||
<a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/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/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/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a>
|
||||
<a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a>
|
||||
<a href="https://github.com/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/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/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a>
|
||||
<a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a>
|
||||
<a href="https://github.com/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/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a>
|
||||
<a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a>
|
||||
<a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/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/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/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a>
|
||||
<a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a>
|
||||
<a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a>
|
||||
<a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a>
|
||||
<a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a>
|
||||
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a>
|
||||
<a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a>
|
||||
<a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/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/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/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/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a>
|
||||
<a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a>
|
||||
<a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/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/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a>
|
||||
<a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||
</p>
|
||||
|
||||
@@ -21,8 +21,8 @@ android {
|
||||
applicationId = "com.clawdbot.android"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 202601230
|
||||
versionName = "2026.1.23"
|
||||
versionCode = 202601210
|
||||
versionName = "2026.1.21"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -8,14 +8,10 @@ object WakeWords {
|
||||
return input.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun parseIfChanged(input: String, current: List<String>): List<String>? {
|
||||
val parsed = parseCommaSeparated(input)
|
||||
return if (parsed == current) null else parsed
|
||||
}
|
||||
|
||||
fun sanitize(words: List<String>, defaults: List<String>): List<String> {
|
||||
val cleaned =
|
||||
words.map { it.trim() }.filter { it.isNotEmpty() }.take(maxWords).map { it.take(maxWordLength) }
|
||||
return cleaned.ifEmpty { defaults }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
@@ -51,10 +49,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -63,7 +58,6 @@ import com.clawdbot.android.LocationMode
|
||||
import com.clawdbot.android.MainViewModel
|
||||
import com.clawdbot.android.NodeForegroundService
|
||||
import com.clawdbot.android.VoiceWakeMode
|
||||
import com.clawdbot.android.WakeWords
|
||||
|
||||
@Composable
|
||||
fun SettingsSheet(viewModel: MainViewModel) {
|
||||
@@ -92,8 +86,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
val listState = rememberLazyListState()
|
||||
val (wakeWordsText, setWakeWordsText) = remember { mutableStateOf("") }
|
||||
val (advancedExpanded, setAdvancedExpanded) = remember { mutableStateOf(false) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
var wakeWordsHadFocus by remember { mutableStateOf(false) }
|
||||
val deviceModel =
|
||||
remember {
|
||||
listOfNotNull(Build.MANUFACTURER, Build.MODEL)
|
||||
@@ -112,12 +104,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
|
||||
LaunchedEffect(wakeWords) { setWakeWordsText(wakeWords.joinToString(", ")) }
|
||||
val commitWakeWords = {
|
||||
val parsed = WakeWords.parseIfChanged(wakeWordsText, wakeWords)
|
||||
if (parsed != null) {
|
||||
viewModel.setWakeWords(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
val permissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms ->
|
||||
@@ -495,27 +481,25 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
value = wakeWordsText,
|
||||
onValueChange = setWakeWordsText,
|
||||
label = { Text("Wake Words (comma-separated)") },
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().onFocusChanged { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
wakeWordsHadFocus = true
|
||||
} else if (wakeWordsHadFocus) {
|
||||
wakeWordsHadFocus = false
|
||||
commitWakeWords()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onDone = {
|
||||
commitWakeWords()
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
item { Button(onClick = viewModel::resetWakeWordsDefaults) { Text("Reset defaults") } }
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
val parsed = com.clawdbot.android.WakeWords.parseCommaSeparated(wakeWordsText)
|
||||
viewModel.setWakeWords(parsed)
|
||||
},
|
||||
enabled = isConnected,
|
||||
) {
|
||||
Text("Save + Sync")
|
||||
}
|
||||
|
||||
Button(onClick = viewModel::resetWakeWordsDefaults) { Text("Reset defaults") }
|
||||
}
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
if (isConnected) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.clawdbot.android
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class WakeWordsTest {
|
||||
@@ -33,18 +32,5 @@ class WakeWordsTest {
|
||||
assertEquals("w1", sanitized.first())
|
||||
assertEquals("w${WakeWords.maxWords}", sanitized.last())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseIfChangedSkipsWhenUnchanged() {
|
||||
val current = listOf("clawd", "claude")
|
||||
val parsed = WakeWords.parseIfChanged(" clawd , claude ", current)
|
||||
assertNull(parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseIfChangedReturnsUpdatedList() {
|
||||
val current = listOf("clawd")
|
||||
val parsed = WakeWords.parseIfChanged(" clawd , jarvis ", current)
|
||||
assertEquals(listOf("clawd", "jarvis"), parsed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.23</string>
|
||||
<string>2026.1.21</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260123</string>
|
||||
<string>20260121</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct VoiceWakeWordsSettingsView: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
@State private var triggerWords: [String] = VoiceWakePreferences.loadTriggerWords()
|
||||
@FocusState private var focusedTriggerIndex: Int?
|
||||
@State private var syncTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
@@ -14,10 +12,6 @@ struct VoiceWakeWordsSettingsView: View {
|
||||
TextField("Wake word", text: self.binding(for: index))
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused(self.$focusedTriggerIndex, equals: index)
|
||||
.onSubmit {
|
||||
self.commitTriggerWords()
|
||||
}
|
||||
}
|
||||
.onDelete(perform: self.removeWords)
|
||||
|
||||
@@ -45,18 +39,17 @@ struct VoiceWakeWordsSettingsView: View {
|
||||
.onAppear {
|
||||
if self.triggerWords.isEmpty {
|
||||
self.triggerWords = VoiceWakePreferences.defaultTriggerWords
|
||||
self.commitTriggerWords()
|
||||
}
|
||||
}
|
||||
.onChange(of: self.focusedTriggerIndex) { oldValue, newValue in
|
||||
guard oldValue != nil, oldValue != newValue else { return }
|
||||
self.commitTriggerWords()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
|
||||
guard self.focusedTriggerIndex == nil else { return }
|
||||
let updated = VoiceWakePreferences.loadTriggerWords()
|
||||
if updated != self.triggerWords {
|
||||
self.triggerWords = updated
|
||||
.onChange(of: self.triggerWords) { _, newValue in
|
||||
// Keep local voice wake responsive even if the gateway isn't connected yet.
|
||||
VoiceWakePreferences.saveTriggerWords(newValue)
|
||||
|
||||
let snapshot = VoiceWakePreferences.sanitizeTriggerWords(newValue)
|
||||
self.syncTask?.cancel()
|
||||
self.syncTask = Task { [snapshot, weak appModel = self.appModel] in
|
||||
try? await Task.sleep(nanoseconds: 650_000_000)
|
||||
await appModel?.setGlobalWakeWords(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +63,6 @@ struct VoiceWakeWordsSettingsView: View {
|
||||
if self.triggerWords.isEmpty {
|
||||
self.triggerWords = VoiceWakePreferences.defaultTriggerWords
|
||||
}
|
||||
self.commitTriggerWords()
|
||||
}
|
||||
|
||||
private func binding(for index: Int) -> Binding<String> {
|
||||
@@ -84,15 +76,4 @@ struct VoiceWakeWordsSettingsView: View {
|
||||
self.triggerWords[index] = newValue
|
||||
})
|
||||
}
|
||||
|
||||
private func commitTriggerWords() {
|
||||
VoiceWakePreferences.saveTriggerWords(self.triggerWords)
|
||||
|
||||
let snapshot = VoiceWakePreferences.sanitizeTriggerWords(self.triggerWords)
|
||||
self.syncTask?.cancel()
|
||||
self.syncTask = Task { [snapshot, weak appModel = self.appModel] in
|
||||
try? await Task.sleep(nanoseconds: 650_000_000)
|
||||
await appModel?.setGlobalWakeWords(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ enum VoiceWakePreferences {
|
||||
|
||||
// Keep defaults aligned with the mac app.
|
||||
static let defaultTriggerWords: [String] = ["clawd", "claude"]
|
||||
static let maxWords = 32
|
||||
static let maxWordLength = 64
|
||||
|
||||
static func decodeGatewayTriggers(from payloadJSON: String) -> [String]? {
|
||||
guard let data = payloadJSON.data(using: .utf8) else { return nil }
|
||||
@@ -32,8 +30,6 @@ enum VoiceWakePreferences {
|
||||
let cleaned = words
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.prefix(Self.maxWords)
|
||||
.map { String($0.prefix(Self.maxWordLength)) }
|
||||
return cleaned.isEmpty ? Self.defaultTriggerWords : cleaned
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.23</string>
|
||||
<string>2026.1.21</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260123</string>
|
||||
<string>20260121</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -11,18 +11,6 @@ import Testing
|
||||
#expect(VoiceWakePreferences.sanitizeTriggerWords(["", " "]) == VoiceWakePreferences.defaultTriggerWords)
|
||||
}
|
||||
|
||||
@Test func sanitizeTriggerWordsLimitsWordLength() {
|
||||
let long = String(repeating: "x", count: VoiceWakePreferences.maxWordLength + 5)
|
||||
let cleaned = VoiceWakePreferences.sanitizeTriggerWords(["ok", long])
|
||||
#expect(cleaned[1].count == VoiceWakePreferences.maxWordLength)
|
||||
}
|
||||
|
||||
@Test func sanitizeTriggerWordsLimitsWordCount() {
|
||||
let words = (1...VoiceWakePreferences.maxWords + 3).map { "w\($0)" }
|
||||
let cleaned = VoiceWakePreferences.sanitizeTriggerWords(words)
|
||||
#expect(cleaned.count == VoiceWakePreferences.maxWords)
|
||||
}
|
||||
|
||||
@Test func displayStringUsesSanitizedWords() {
|
||||
#expect(VoiceWakePreferences.displayString(for: ["", " "]) == "clawd, claude")
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ targets:
|
||||
properties:
|
||||
CFBundleDisplayName: Clawdbot
|
||||
CFBundleIconName: AppIcon
|
||||
CFBundleShortVersionString: "2026.1.23"
|
||||
CFBundleVersion: "20260123"
|
||||
CFBundleShortVersionString: "2026.1.21"
|
||||
CFBundleVersion: "20260121"
|
||||
UILaunchScreen: {}
|
||||
UIApplicationSceneManifest:
|
||||
UIApplicationSupportsMultipleScenes: false
|
||||
@@ -130,5 +130,5 @@ targets:
|
||||
path: Tests/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: ClawdbotTests
|
||||
CFBundleShortVersionString: "2026.1.23"
|
||||
CFBundleVersion: "20260123"
|
||||
CFBundleShortVersionString: "2026.1.21"
|
||||
CFBundleVersion: "20260121"
|
||||
|
||||
@@ -12,8 +12,6 @@ let voiceWakeTriggerChimeKey = "clawdbot.voiceWakeTriggerChime"
|
||||
let voiceWakeSendChimeKey = "clawdbot.voiceWakeSendChime"
|
||||
let showDockIconKey = "clawdbot.showDockIcon"
|
||||
let defaultVoiceWakeTriggers = ["clawd", "claude"]
|
||||
let voiceWakeMaxWords = 32
|
||||
let voiceWakeMaxWordLength = 64
|
||||
let voiceWakeMicKey = "clawdbot.voiceWakeMicID"
|
||||
let voiceWakeMicNameKey = "clawdbot.voiceWakeMicName"
|
||||
let voiceWakeLocaleKey = "clawdbot.voiceWakeLocaleID"
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.23</string>
|
||||
<string>2026.1.21</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>202601230</string>
|
||||
<string>202601210</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Clawdbot</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -4,8 +4,6 @@ func sanitizeVoiceWakeTriggers(_ words: [String]) -> [String] {
|
||||
let cleaned = words
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.prefix(voiceWakeMaxWords)
|
||||
.map { String($0.prefix(voiceWakeMaxWordLength)) }
|
||||
return cleaned.isEmpty ? defaultVoiceWakeTriggers : cleaned
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ struct VoiceWakeSettings: View {
|
||||
@State private var micRefreshTask: Task<Void, Never>?
|
||||
@State private var availableLocales: [Locale] = []
|
||||
@State private var triggerEntries: [TriggerEntry] = []
|
||||
@FocusState private var focusedTriggerEntryID: UUID?
|
||||
private let fieldLabelWidth: CGFloat = 140
|
||||
private let controlWidth: CGFloat = 240
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
@@ -142,6 +143,11 @@ struct VoiceWakeSettings: View {
|
||||
Task { await self.meter.stop() }
|
||||
self.syncTriggerEntriesToState()
|
||||
}
|
||||
.onChange(of: self.state.swabbleTriggerWords) { _, _ in
|
||||
guard !self.isPreview else { return }
|
||||
guard self.focusedTriggerEntryID == nil else { return }
|
||||
self.loadTriggerEntries()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTriggerEntries() {
|
||||
@@ -177,6 +183,7 @@ struct VoiceWakeSettings: View {
|
||||
HStack(spacing: 8) {
|
||||
TextField("Wake word", text: $entry.value)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.focused(self.$focusedTriggerEntryID, equals: entry.id)
|
||||
.onSubmit {
|
||||
self.syncTriggerEntriesToState()
|
||||
}
|
||||
@@ -197,6 +204,10 @@ struct VoiceWakeSettings: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: self.focusedTriggerEntryID) { oldValue, newValue in
|
||||
guard oldValue != nil, oldValue != newValue else { return }
|
||||
self.syncTriggerEntriesToState()
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading)
|
||||
.background(Color(nsColor: .textBackgroundColor))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
||||
public let to: String
|
||||
public let message: String
|
||||
public let mediaurl: String?
|
||||
public let mediaurls: [String]?
|
||||
public let gifplayback: Bool?
|
||||
public let channel: String?
|
||||
public let accountid: String?
|
||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
||||
to: String,
|
||||
message: String,
|
||||
mediaurl: String?,
|
||||
mediaurls: [String]?,
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
accountid: String?,
|
||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
||||
self.to = to
|
||||
self.message = message
|
||||
self.mediaurl = mediaurl
|
||||
self.mediaurls = mediaurls
|
||||
self.gifplayback = gifplayback
|
||||
self.channel = channel
|
||||
self.accountid = accountid
|
||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
||||
case to
|
||||
case message
|
||||
case mediaurl = "mediaUrl"
|
||||
case mediaurls = "mediaUrls"
|
||||
case gifplayback = "gifPlayback"
|
||||
case channel
|
||||
case accountid = "accountId"
|
||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let accountid: String?
|
||||
public let replyaccountid: String?
|
||||
public let threadid: String?
|
||||
public let groupid: String?
|
||||
public let groupchannel: String?
|
||||
public let groupspace: String?
|
||||
public let timeout: Int?
|
||||
public let lane: String?
|
||||
public let extrasystemprompt: String?
|
||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
accountid: String?,
|
||||
replyaccountid: String?,
|
||||
threadid: String?,
|
||||
groupid: String?,
|
||||
groupchannel: String?,
|
||||
groupspace: String?,
|
||||
timeout: Int?,
|
||||
lane: String?,
|
||||
extrasystemprompt: String?,
|
||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.accountid = accountid
|
||||
self.replyaccountid = replyaccountid
|
||||
self.threadid = threadid
|
||||
self.groupid = groupid
|
||||
self.groupchannel = groupchannel
|
||||
self.groupspace = groupspace
|
||||
self.timeout = timeout
|
||||
self.lane = lane
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
case accountid = "accountId"
|
||||
case replyaccountid = "replyAccountId"
|
||||
case threadid = "threadId"
|
||||
case groupid = "groupId"
|
||||
case groupchannel = "groupChannel"
|
||||
case groupspace = "groupSpace"
|
||||
case timeout
|
||||
case lane
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
@@ -1989,25 +1973,25 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let id: String?
|
||||
public let command: String
|
||||
public let cwd: AnyCodable?
|
||||
public let host: AnyCodable?
|
||||
public let security: AnyCodable?
|
||||
public let ask: AnyCodable?
|
||||
public let agentid: AnyCodable?
|
||||
public let resolvedpath: AnyCodable?
|
||||
public let sessionkey: AnyCodable?
|
||||
public let cwd: String?
|
||||
public let host: String?
|
||||
public let security: String?
|
||||
public let ask: String?
|
||||
public let agentid: String?
|
||||
public let resolvedpath: String?
|
||||
public let sessionkey: String?
|
||||
public let timeoutms: Int?
|
||||
|
||||
public init(
|
||||
id: String?,
|
||||
command: String,
|
||||
cwd: AnyCodable?,
|
||||
host: AnyCodable?,
|
||||
security: AnyCodable?,
|
||||
ask: AnyCodable?,
|
||||
agentid: AnyCodable?,
|
||||
resolvedpath: AnyCodable?,
|
||||
sessionkey: AnyCodable?,
|
||||
cwd: String?,
|
||||
host: String?,
|
||||
security: String?,
|
||||
ask: String?,
|
||||
agentid: String?,
|
||||
resolvedpath: String?,
|
||||
sessionkey: String?,
|
||||
timeoutms: Int?
|
||||
) {
|
||||
self.id = id
|
||||
|
||||
@@ -12,18 +12,6 @@ struct VoiceWakeHelpersTests {
|
||||
#expect(cleaned == defaultVoiceWakeTriggers)
|
||||
}
|
||||
|
||||
@Test func sanitizeTriggersLimitsWordLength() {
|
||||
let long = String(repeating: "x", count: voiceWakeMaxWordLength + 5)
|
||||
let cleaned = sanitizeVoiceWakeTriggers(["ok", long])
|
||||
#expect(cleaned[1].count == voiceWakeMaxWordLength)
|
||||
}
|
||||
|
||||
@Test func sanitizeTriggersLimitsWordCount() {
|
||||
let words = (1...voiceWakeMaxWords + 3).map { "w\($0)" }
|
||||
let cleaned = sanitizeVoiceWakeTriggers(words)
|
||||
#expect(cleaned.count == voiceWakeMaxWords)
|
||||
}
|
||||
|
||||
@Test func normalizeLocaleStripsCollation() {
|
||||
#expect(normalizeLocaleIdentifier("en_US@collation=phonebook") == "en_US")
|
||||
}
|
||||
|
||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
||||
public let to: String
|
||||
public let message: String
|
||||
public let mediaurl: String?
|
||||
public let mediaurls: [String]?
|
||||
public let gifplayback: Bool?
|
||||
public let channel: String?
|
||||
public let accountid: String?
|
||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
||||
to: String,
|
||||
message: String,
|
||||
mediaurl: String?,
|
||||
mediaurls: [String]?,
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
accountid: String?,
|
||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
||||
self.to = to
|
||||
self.message = message
|
||||
self.mediaurl = mediaurl
|
||||
self.mediaurls = mediaurls
|
||||
self.gifplayback = gifplayback
|
||||
self.channel = channel
|
||||
self.accountid = accountid
|
||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
||||
case to
|
||||
case message
|
||||
case mediaurl = "mediaUrl"
|
||||
case mediaurls = "mediaUrls"
|
||||
case gifplayback = "gifPlayback"
|
||||
case channel
|
||||
case accountid = "accountId"
|
||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let accountid: String?
|
||||
public let replyaccountid: String?
|
||||
public let threadid: String?
|
||||
public let groupid: String?
|
||||
public let groupchannel: String?
|
||||
public let groupspace: String?
|
||||
public let timeout: Int?
|
||||
public let lane: String?
|
||||
public let extrasystemprompt: String?
|
||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
accountid: String?,
|
||||
replyaccountid: String?,
|
||||
threadid: String?,
|
||||
groupid: String?,
|
||||
groupchannel: String?,
|
||||
groupspace: String?,
|
||||
timeout: Int?,
|
||||
lane: String?,
|
||||
extrasystemprompt: String?,
|
||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.accountid = accountid
|
||||
self.replyaccountid = replyaccountid
|
||||
self.threadid = threadid
|
||||
self.groupid = groupid
|
||||
self.groupchannel = groupchannel
|
||||
self.groupspace = groupspace
|
||||
self.timeout = timeout
|
||||
self.lane = lane
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
case accountid = "accountId"
|
||||
case replyaccountid = "replyAccountId"
|
||||
case threadid = "threadId"
|
||||
case groupid = "groupId"
|
||||
case groupchannel = "groupChannel"
|
||||
case groupspace = "groupSpace"
|
||||
case timeout
|
||||
case lane
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
@@ -1989,25 +1973,25 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let id: String?
|
||||
public let command: String
|
||||
public let cwd: AnyCodable?
|
||||
public let host: AnyCodable?
|
||||
public let security: AnyCodable?
|
||||
public let ask: AnyCodable?
|
||||
public let agentid: AnyCodable?
|
||||
public let resolvedpath: AnyCodable?
|
||||
public let sessionkey: AnyCodable?
|
||||
public let cwd: String?
|
||||
public let host: String?
|
||||
public let security: String?
|
||||
public let ask: String?
|
||||
public let agentid: String?
|
||||
public let resolvedpath: String?
|
||||
public let sessionkey: String?
|
||||
public let timeoutms: Int?
|
||||
|
||||
public init(
|
||||
id: String?,
|
||||
command: String,
|
||||
cwd: AnyCodable?,
|
||||
host: AnyCodable?,
|
||||
security: AnyCodable?,
|
||||
ask: AnyCodable?,
|
||||
agentid: AnyCodable?,
|
||||
resolvedpath: AnyCodable?,
|
||||
sessionkey: AnyCodable?,
|
||||
cwd: String?,
|
||||
host: String?,
|
||||
security: String?,
|
||||
ask: String?,
|
||||
agentid: String?,
|
||||
resolvedpath: String?,
|
||||
sessionkey: String?,
|
||||
timeoutms: Int?
|
||||
) {
|
||||
self.id = id
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"gateway-daemon",
|
||||
"--bind",
|
||||
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
||||
"--port",
|
||||
|
||||
@@ -3,12 +3,9 @@ summary: "Cron jobs + wakeups for the Gateway scheduler"
|
||||
read_when:
|
||||
- Scheduling background jobs or wakeups
|
||||
- Wiring automation that should run with or alongside heartbeats
|
||||
- Deciding between heartbeat and cron for scheduled tasks
|
||||
---
|
||||
# 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
|
||||
the right time, and can optionally deliver output back to a chat.
|
||||
|
||||
@@ -263,15 +260,15 @@ Run history:
|
||||
clawdbot cron runs --id <jobId> --limit 50
|
||||
```
|
||||
|
||||
Immediate system event without creating a job:
|
||||
Immediate wake without creating a job:
|
||||
```bash
|
||||
clawdbot system event --mode now --text "Next heartbeat: check battery."
|
||||
clawdbot wake --mode now --text "Next heartbeat: check battery."
|
||||
```
|
||||
|
||||
## Gateway API surface
|
||||
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
||||
- `cron.run` (force or due), `cron.runs`
|
||||
For immediate system events without a job, use [`clawdbot system event`](/cli/system).
|
||||
- `wake` (enqueue system event + optional heartbeat)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
---
|
||||
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,37 +17,6 @@ not an API key.
|
||||
- Auth: AWS credentials (env vars, shared config, or instance role)
|
||||
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
|
||||
|
||||
## Automatic model discovery
|
||||
|
||||
If AWS credentials are detected, Clawdbot can automatically discover Bedrock
|
||||
models that support **streaming** and **text output**. Discovery uses
|
||||
`bedrock:ListFoundationModels` and is cached (default: 1 hour).
|
||||
|
||||
Config options live under `models.bedrockDiscovery`:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
bedrockDiscovery: {
|
||||
enabled: true,
|
||||
region: "us-east-1",
|
||||
providerFilter: ["anthropic", "amazon"],
|
||||
refreshInterval: 3600,
|
||||
defaultContextWindow: 32000,
|
||||
defaultMaxTokens: 4096
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `enabled` defaults to `true` when AWS credentials are present.
|
||||
- `region` defaults to `AWS_REGION` or `AWS_DEFAULT_REGION`, then `us-east-1`.
|
||||
- `providerFilter` matches Bedrock provider names (for example `anthropic`).
|
||||
- `refreshInterval` is seconds; set to `0` to disable caching.
|
||||
- `defaultContextWindow` (default: `32000`) and `defaultMaxTokens` (default: `4096`)
|
||||
are used for discovered models (override if you know your model limits).
|
||||
|
||||
## Setup (manual)
|
||||
|
||||
1) Ensure AWS credentials are available on the **gateway host**:
|
||||
@@ -98,7 +67,6 @@ export AWS_BEARER_TOKEN_BEDROCK="..."
|
||||
## Notes
|
||||
|
||||
- Bedrock requires **model access** enabled in your AWS account/region.
|
||||
- Automatic discovery needs the `bedrock:ListFoundationModels` permission.
|
||||
- If you use profiles, set `AWS_PROFILE` on the gateway host.
|
||||
- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`,
|
||||
then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the
|
||||
|
||||
@@ -23,7 +23,6 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
|
||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
||||
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
summary: "Tlon/Urbit support status, capabilities, and configuration"
|
||||
read_when:
|
||||
- Working on Tlon/Urbit channel features
|
||||
---
|
||||
# Tlon (plugin)
|
||||
|
||||
Tlon is a decentralized messenger built on Urbit. Clawdbot connects to your Urbit ship and can
|
||||
respond to DMs and group chat messages. Group replies require an @ mention by default and can
|
||||
be further restricted via allowlists.
|
||||
|
||||
Status: supported via plugin. DMs, group mentions, thread replies, and text-only media fallback
|
||||
(URL appended to caption). Reactions, polls, and native media uploads are not supported.
|
||||
|
||||
## Plugin required
|
||||
|
||||
Tlon ships as a plugin and is not bundled with the core install.
|
||||
|
||||
Install via CLI (npm registry):
|
||||
|
||||
```bash
|
||||
clawdbot plugins install @clawdbot/tlon
|
||||
```
|
||||
|
||||
Local checkout (when running from a git repo):
|
||||
|
||||
```bash
|
||||
clawdbot plugins install ./extensions/tlon
|
||||
```
|
||||
|
||||
Details: [Plugins](/plugin)
|
||||
|
||||
## Setup
|
||||
|
||||
1) Install the Tlon plugin.
|
||||
2) Gather your ship URL and login code.
|
||||
3) Configure `channels.tlon`.
|
||||
4) Restart the gateway.
|
||||
5) DM the bot or mention it in a group channel.
|
||||
|
||||
Minimal config (single account):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
tlon: {
|
||||
enabled: true,
|
||||
ship: "~sampel-palnet",
|
||||
url: "https://your-ship-host",
|
||||
code: "lidlut-tabwed-pillex-ridrup"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Group channels
|
||||
|
||||
Auto-discovery is enabled by default. You can also pin channels manually:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
tlon: {
|
||||
groupChannels: [
|
||||
"chat/~host-ship/general",
|
||||
"chat/~host-ship/support"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Disable auto-discovery:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
tlon: {
|
||||
autoDiscoverChannels: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Access control
|
||||
|
||||
DM allowlist (empty = allow all):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
tlon: {
|
||||
dmAllowlist: ["~zod", "~nec"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Group authorization (restricted by default):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
tlon: {
|
||||
defaultAuthorizedShips: ["~zod"],
|
||||
authorization: {
|
||||
channelRules: {
|
||||
"chat/~host-ship/general": {
|
||||
mode: "restricted",
|
||||
allowedShips: ["~zod", "~nec"]
|
||||
},
|
||||
"chat/~host-ship/announcements": {
|
||||
mode: "open"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Delivery targets (CLI/cron)
|
||||
|
||||
Use these with `clawdbot message send` or cron delivery:
|
||||
|
||||
- DM: `~sampel-palnet` or `dm/~sampel-palnet`
|
||||
- Group: `chat/~host-ship/channel` or `group:~host-ship/channel`
|
||||
|
||||
## Notes
|
||||
|
||||
- Group replies require a mention (e.g. `~your-bot-ship`) to respond.
|
||||
- Thread replies: if the inbound message is in a thread, Clawdbot replies in-thread.
|
||||
- Media: `sendMedia` falls back to text + URL (no native upload).
|
||||
@@ -44,7 +44,6 @@ clawdbot channels logout --channel whatsapp
|
||||
|
||||
- Run `clawdbot status --deep` for a broad probe.
|
||||
- Use `clawdbot doctor` for guided fixes.
|
||||
- `clawdbot channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude Code CLI.
|
||||
|
||||
## Capabilities probe
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`sessions`](/cli/sessions)
|
||||
- [`gateway`](/cli/gateway)
|
||||
- [`logs`](/cli/logs)
|
||||
- [`system`](/cli/system)
|
||||
- [`models`](/cli/models)
|
||||
- [`memory`](/cli/memory)
|
||||
- [`nodes`](/cli/nodes)
|
||||
@@ -39,6 +38,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`sandbox`](/cli/sandbox)
|
||||
- [`tui`](/cli/tui)
|
||||
- [`browser`](/cli/browser)
|
||||
- [`wake`](/cli/wake)
|
||||
- [`cron`](/cli/cron)
|
||||
- [`dns`](/cli/dns)
|
||||
- [`docs`](/cli/docs)
|
||||
@@ -145,10 +145,6 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
restart
|
||||
run
|
||||
logs
|
||||
system
|
||||
event
|
||||
heartbeat last|enable|disable
|
||||
presence
|
||||
models
|
||||
list
|
||||
status
|
||||
@@ -164,6 +160,7 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
list
|
||||
recreate
|
||||
explain
|
||||
wake
|
||||
cron
|
||||
status
|
||||
list
|
||||
@@ -766,9 +763,9 @@ Options:
|
||||
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
||||
- `clear`: `--provider <name>`, `--agent <id>`
|
||||
|
||||
## System
|
||||
## Cron + wake
|
||||
|
||||
### `system event`
|
||||
### `wake`
|
||||
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
||||
|
||||
Required:
|
||||
@@ -779,21 +776,7 @@ Options:
|
||||
- `--json`
|
||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||
|
||||
### `system heartbeat last|enable|disable`
|
||||
Heartbeat controls (Gateway RPC).
|
||||
|
||||
Options:
|
||||
- `--json`
|
||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||
|
||||
### `system presence`
|
||||
List system presence entries (Gateway RPC).
|
||||
|
||||
Options:
|
||||
- `--json`
|
||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||
|
||||
## Cron
|
||||
### `cron`
|
||||
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
||||
|
||||
Subcommands:
|
||||
|
||||
@@ -23,24 +23,6 @@ Common use cases:
|
||||
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
||||
node host, so you can keep command access scoped and explicit.
|
||||
|
||||
## Browser proxy (zero-config)
|
||||
|
||||
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
|
||||
disabled on the node. This lets the agent use browser automation on that node
|
||||
without extra configuration.
|
||||
|
||||
Disable it on the node if needed:
|
||||
|
||||
```json5
|
||||
{
|
||||
nodeHost: {
|
||||
browserProxy: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Run (foreground)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot system` (system events, heartbeat, presence)"
|
||||
read_when:
|
||||
- You want to enqueue a system event without creating a cron job
|
||||
- You need to enable or disable heartbeats
|
||||
- You want to inspect system presence entries
|
||||
---
|
||||
|
||||
# `clawdbot system`
|
||||
|
||||
System-level helpers for the Gateway: enqueue system events, control heartbeats,
|
||||
and view presence.
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
||||
clawdbot system heartbeat enable
|
||||
clawdbot system heartbeat last
|
||||
clawdbot system presence
|
||||
```
|
||||
|
||||
## `system event`
|
||||
|
||||
Enqueue a system event on the **main** session. The next heartbeat will inject
|
||||
it as a `System:` line in the prompt. Use `--mode now` to trigger the heartbeat
|
||||
immediately; `next-heartbeat` waits for the next scheduled tick.
|
||||
|
||||
Flags:
|
||||
- `--text <text>`: required system event text.
|
||||
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## `system heartbeat last|enable|disable`
|
||||
|
||||
Heartbeat controls:
|
||||
- `last`: show the last heartbeat event.
|
||||
- `enable`: turn heartbeats back on (use this if they were disabled).
|
||||
- `disable`: pause heartbeats.
|
||||
|
||||
Flags:
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## `system presence`
|
||||
|
||||
List the current system presence entries the Gateway knows about (nodes,
|
||||
instances, and similar status lines).
|
||||
|
||||
Flags:
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires a running Gateway reachable by your current config (local or remote).
|
||||
- System events are ephemeral and not persisted across restarts.
|
||||
35
docs/cli/wake.md
Normal file
35
docs/cli/wake.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot wake` (enqueue a system event and optionally trigger an immediate heartbeat)"
|
||||
read_when:
|
||||
- You want to “poke” a running Gateway to process a system event
|
||||
- You use `wake` with cron jobs or remote nodes
|
||||
---
|
||||
|
||||
# `clawdbot wake`
|
||||
|
||||
Enqueue a system event on the Gateway and optionally trigger an immediate heartbeat.
|
||||
|
||||
This is a lightweight “poke” for automation flows where you don’t want to run a full command, but you do want the Gateway to react quickly.
|
||||
|
||||
Related:
|
||||
- Cron jobs: [Cron](/cli/cron)
|
||||
- Gateway heartbeat: [Heartbeat](/gateway/heartbeat)
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot wake --text "sync"
|
||||
clawdbot wake --text "sync" --mode now
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
- `--text <text>`: system event text.
|
||||
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires a running Gateway reachable by your current config (local or remote).
|
||||
- If you’re using sandboxing, `wake` still targets the Gateway; sandboxing does not block the command itself.
|
||||
|
||||
@@ -66,12 +66,12 @@ To inspect how much each injected file contributes (raw vs injected, truncation,
|
||||
|
||||
## Time handling
|
||||
|
||||
The system prompt includes a dedicated **Current Date & Time** section when the
|
||||
user timezone is known. To keep the prompt cache-stable, it now only includes
|
||||
the **time zone** (no dynamic clock or time format).
|
||||
The system prompt includes a dedicated **Current Date & Time** section when user
|
||||
time or timezone is known. It is explicit about:
|
||||
|
||||
Use `session_status` when the agent needs the current time; the status card
|
||||
includes a timestamp line.
|
||||
- The user’s **local time** (already converted).
|
||||
- The **time zone** used for the conversion.
|
||||
- The **time format** (12-hour / 24-hour).
|
||||
|
||||
Configure with:
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ read_when:
|
||||
|
||||
# Date & Time
|
||||
|
||||
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 (current time is available via `session_status`).
|
||||
Clawdbot defaults to **host-local time for transport timestamps** and **user-local time only in the system prompt**.
|
||||
Provider timestamps are preserved so tools keep their native semantics.
|
||||
|
||||
## Message envelopes (local by default)
|
||||
|
||||
@@ -63,16 +63,16 @@ You can override this behavior:
|
||||
|
||||
## System prompt: Current Date & Time
|
||||
|
||||
If the user timezone is known, the system prompt includes a dedicated
|
||||
**Current Date & Time** section with the **time zone only** (no clock/time format)
|
||||
to keep prompt caching stable:
|
||||
If the user timezone or local time is known, the system prompt includes a dedicated
|
||||
**Current Date & Time** section:
|
||||
|
||||
```
|
||||
Time zone: America/Chicago
|
||||
Thursday, January 15th, 2026 — 3:07 PM (America/Chicago)
|
||||
Time format: 12-hour
|
||||
```
|
||||
|
||||
When the agent needs the current time, use the `session_status` tool; the status
|
||||
card includes a timestamp line.
|
||||
If only the timezone is known, we still include the section and instruct the model
|
||||
to assume UTC for unknown time references.
|
||||
|
||||
## System event lines (local by default)
|
||||
|
||||
|
||||
@@ -349,14 +349,6 @@
|
||||
"source": "/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",
|
||||
"destination": "/web/dashboard"
|
||||
@@ -850,12 +842,12 @@
|
||||
"cli/memory",
|
||||
"cli/models",
|
||||
"cli/logs",
|
||||
"cli/system",
|
||||
"cli/nodes",
|
||||
"cli/approvals",
|
||||
"cli/gateway",
|
||||
"cli/tui",
|
||||
"cli/voicecall",
|
||||
"cli/wake",
|
||||
"cli/cron",
|
||||
"cli/dns",
|
||||
"cli/docs",
|
||||
@@ -992,7 +984,6 @@
|
||||
"automation/webhook",
|
||||
"automation/gmail-pubsub",
|
||||
"automation/cron-jobs",
|
||||
"automation/cron-vs-heartbeat",
|
||||
"automation/poll"
|
||||
]
|
||||
},
|
||||
@@ -1000,8 +991,6 @@
|
||||
"group": "Tools & Skills",
|
||||
"pages": [
|
||||
"tools",
|
||||
"tools/lobster",
|
||||
"tools/llm-task",
|
||||
"plugin",
|
||||
"plugins/voice-call",
|
||||
"plugins/zalouser",
|
||||
|
||||
@@ -1970,7 +1970,6 @@ Example (provider/model-specific allowlist):
|
||||
```
|
||||
|
||||
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||||
Matching is case-insensitive and supports `*` wildcards (`"*"` means all tools).
|
||||
This is applied even when the Docker sandbox is **off**.
|
||||
|
||||
Example (disable browser/canvas everywhere):
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
summary: "Heartbeat polling messages and notification rules"
|
||||
read_when:
|
||||
- Adjusting heartbeat cadence or messaging
|
||||
- Deciding between heartbeat and cron for scheduled tasks
|
||||
---
|
||||
# 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
|
||||
surface anything that needs attention without spamming you.
|
||||
|
||||
@@ -162,10 +159,6 @@ If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
|
||||
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
||||
safe to include every 30 minutes.
|
||||
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
|
||||
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
||||
|
||||
Example `HEARTBEAT.md`:
|
||||
@@ -199,7 +192,7 @@ Safety note: don’t put secrets (API keys, phone numbers, private tokens) into
|
||||
You can enqueue a system event and trigger an immediate heartbeat with:
|
||||
|
||||
```bash
|
||||
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
||||
clawdbot wake --text "Check for urgent follow-ups" --mode now
|
||||
```
|
||||
|
||||
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
||||
|
||||
@@ -43,14 +43,6 @@ Almost always a Node/npm PATH issue. Start here:
|
||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||
- [Control UI](/web/control-ui#insecure-http)
|
||||
|
||||
### `docs.clawd.bot` shows an SSL error (Comcast/Xfinity)
|
||||
|
||||
Some Comcast/Xfinity connections block `docs.clawd.bot` via Xfinity Advanced Security.
|
||||
Disable Advanced Security or add `docs.clawd.bot` to the allowlist, then retry.
|
||||
|
||||
- Xfinity Advanced Security help: https://www.xfinity.com/support/articles/using-xfinity-xfi-advanced-security
|
||||
- Quick sanity checks: try a mobile hotspot or VPN to confirm it’s ISP-level filtering
|
||||
|
||||
### Service says running, but RPC probe fails
|
||||
|
||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||
|
||||
@@ -184,7 +184,7 @@ services:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"gateway-daemon",
|
||||
"--bind",
|
||||
"${CLAWDBOT_GATEWAY_BIND}",
|
||||
"--port",
|
||||
|
||||
@@ -30,17 +30,17 @@ Notes:
|
||||
# From repo root; set release IDs so Sparkle feed is enabled.
|
||||
# APP_BUILD must be numeric + monotonic for Sparkle compare.
|
||||
BUNDLE_ID=com.clawdbot.mac \
|
||||
APP_VERSION=2026.1.23 \
|
||||
APP_VERSION=2026.1.21 \
|
||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-app.sh
|
||||
|
||||
# Zip for distribution (includes resource forks for Sparkle delta support)
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.23.zip
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.21.zip
|
||||
|
||||
# Optional: also build a styled DMG for humans (drag to /Applications)
|
||||
scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.23.dmg
|
||||
scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.21.dmg
|
||||
|
||||
# Recommended: build + notarize/staple zip + DMG
|
||||
# First, create a keychain profile once:
|
||||
@@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.23.dmg
|
||||
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
|
||||
NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \
|
||||
BUNDLE_ID=com.clawdbot.mac \
|
||||
APP_VERSION=2026.1.23 \
|
||||
APP_VERSION=2026.1.21 \
|
||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
|
||||
# Optional: ship dSYM alongside the release
|
||||
ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.23.dSYM.zip
|
||||
ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.21.dSYM.zip
|
||||
```
|
||||
|
||||
## Appcast entry
|
||||
Use the release note generator so Sparkle renders formatted HTML notes:
|
||||
```bash
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.23.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.21.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml
|
||||
```
|
||||
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
|
||||
Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing.
|
||||
|
||||
## Publish & verify
|
||||
- Upload `Clawdbot-2026.1.23.zip` (and `Clawdbot-2026.1.23.dSYM.zip`) to the GitHub release for tag `v2026.1.23`.
|
||||
- Upload `Clawdbot-2026.1.21.zip` (and `Clawdbot-2026.1.21.dSYM.zip`) to the GitHub release for tag `v2026.1.21`.
|
||||
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`.
|
||||
- Sanity checks:
|
||||
- `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200.
|
||||
|
||||
@@ -62,7 +62,6 @@ Plugins can register:
|
||||
- Background services
|
||||
- Optional config validation
|
||||
- **Skills** (by listing `skills` directories in the plugin manifest)
|
||||
- **Auto-reply commands** (execute without invoking the AI agent)
|
||||
|
||||
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
||||
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
||||
@@ -146,16 +145,6 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
Clawdbot can also merge **external channel catalogs** (for example, an MPM
|
||||
registry export). Drop a JSON file at one of:
|
||||
- `~/.clawdbot/mpm/plugins.json`
|
||||
- `~/.clawdbot/mpm/catalog.json`
|
||||
- `~/.clawdbot/plugins/catalog.json`
|
||||
|
||||
Or point `CLAWDBOT_PLUGIN_CATALOG_PATHS` (or `CLAWDBOT_MPM_CATALOG_PATHS`) at
|
||||
one or more JSON files (comma/semicolon/`PATH`-delimited). Each file should
|
||||
contain `{ "entries": [ { "name": "@scope/pkg", "clawdbot": { "channel": {...}, "install": {...} } } ] }`.
|
||||
|
||||
## Plugin IDs
|
||||
|
||||
Default plugin ids:
|
||||
@@ -495,65 +484,6 @@ export default function (api) {
|
||||
}
|
||||
```
|
||||
|
||||
### Register auto-reply commands
|
||||
|
||||
Plugins can register custom slash commands that execute **without invoking the
|
||||
AI agent**. This is useful for toggle commands, status checks, or quick actions
|
||||
that don't need LLM processing.
|
||||
|
||||
```ts
|
||||
export default function (api) {
|
||||
api.registerCommand({
|
||||
name: "mystatus",
|
||||
description: "Show plugin status",
|
||||
handler: (ctx) => ({
|
||||
text: `Plugin is running! Channel: ${ctx.channel}`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Command handler context:
|
||||
|
||||
- `senderId`: The sender's ID (if available)
|
||||
- `channel`: The channel where the command was sent
|
||||
- `isAuthorizedSender`: Whether the sender is an authorized user
|
||||
- `args`: Arguments passed after the command (if `acceptsArgs: true`)
|
||||
- `commandBody`: The full command text
|
||||
- `config`: The current Clawdbot config
|
||||
|
||||
Command options:
|
||||
|
||||
- `name`: Command name (without the leading `/`)
|
||||
- `description`: Help text shown in command lists
|
||||
- `acceptsArgs`: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlers
|
||||
- `requireAuth`: Whether to require authorized sender (default: true)
|
||||
- `handler`: Function that returns `{ text: string }` (can be async)
|
||||
|
||||
Example with authorization and arguments:
|
||||
|
||||
```ts
|
||||
api.registerCommand({
|
||||
name: "setmode",
|
||||
description: "Set plugin mode",
|
||||
acceptsArgs: true,
|
||||
requireAuth: true,
|
||||
handler: async (ctx) => {
|
||||
const mode = ctx.args?.trim() || "default";
|
||||
await saveMode(mode);
|
||||
return { text: `Mode set to: ${mode}` };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Plugin commands are processed **before** built-in commands and the AI agent
|
||||
- Commands are registered globally and work across all channels
|
||||
- Command names are case-insensitive (`/MyStatus` matches `/mystatus`)
|
||||
- Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
|
||||
- Reserved command names (like `help`, `status`, `reset`, etc.) cannot be overridden by plugins
|
||||
- Duplicate command registration across plugins will fail with a diagnostic error
|
||||
|
||||
### Register background services
|
||||
|
||||
```ts
|
||||
|
||||
@@ -112,23 +112,6 @@ Default heartbeat prompt:
|
||||
|
||||
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):**
|
||||
- **Emails** - Any urgent unread messages?
|
||||
- **Calendar** - Upcoming events in next 24-48h?
|
||||
|
||||
@@ -5,5 +5,4 @@ read_when:
|
||||
---
|
||||
# HEARTBEAT.md
|
||||
|
||||
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
||||
# Add tasks below when you want the agent to check something periodically.
|
||||
Keep this file empty unless you want a tiny checklist. Keep it small.
|
||||
|
||||
@@ -7,16 +7,11 @@ read_when:
|
||||
|
||||
*Fill this in during your first conversation. Make it yours.*
|
||||
|
||||
- **Name:**
|
||||
*(pick something you like)*
|
||||
- **Creature:**
|
||||
*(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||
- **Vibe:**
|
||||
*(how do you come across? sharp? warm? chaotic? calm?)*
|
||||
- **Emoji:**
|
||||
*(your signature — pick one that feels right)*
|
||||
- **Avatar:**
|
||||
*(workspace-relative path, http(s) URL, or data URI)*
|
||||
- **Name:** *(pick something you like)*
|
||||
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
||||
- **Emoji:** *(your signature — pick one that feels right)*
|
||||
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -182,8 +182,6 @@ By default, Clawdbot runs a heartbeat every 30 minutes with the prompt:
|
||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||
|
||||
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||
- If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
||||
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
||||
|
||||
|
||||
@@ -971,10 +971,6 @@ Heartbeats run every **30m** by default. Tune or disable them:
|
||||
}
|
||||
```
|
||||
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
|
||||
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
||||
|
||||
### Do I need to add a “bot account” to a WhatsApp group?
|
||||
|
||||
@@ -102,7 +102,6 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
- [Exec tool](/tools/exec)
|
||||
- [Elevated mode](/tools/elevated)
|
||||
- [Cron jobs](/automation/cron-jobs)
|
||||
- [Cron vs Heartbeat](/automation/cron-vs-heartbeat)
|
||||
- [Thinking + verbose](/tools/thinking)
|
||||
- [Models](/concepts/models)
|
||||
- [Sub-agents](/tools/subagents)
|
||||
|
||||
@@ -327,10 +327,16 @@ Full setup walkthrough (28m) by VelvetShark.
|
||||
|
||||
<Card title="OpenRouter Transcription" icon="microphone" href="https://clawdhub.com/obviyus/openrouter-transcribe">
|
||||
**@obviyus** • `transcription` `multilingual` `skill`
|
||||
|
||||
|
||||
Multi-lingual audio transcription via OpenRouter (Gemini, etc). Available on ClawdHub.
|
||||
</Card>
|
||||
|
||||
<Card title="Google Docs Editor" icon="file-word">
|
||||
**Community** • `docs` `editing` `skill`
|
||||
|
||||
Rich-text Google Docs editing skill. Built rapidly with Claude Code.
|
||||
</Card>
|
||||
|
||||
</CardGroup>
|
||||
|
||||
## 🏗️ Infrastructure & Deployment
|
||||
|
||||
@@ -166,19 +166,6 @@ Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting
|
||||
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
||||
tokens instead of committing them to config files.
|
||||
|
||||
### Node browser proxy (zero-config default)
|
||||
|
||||
If you run a **node host** on the machine that has your browser, Clawdbot can
|
||||
auto-route browser tool calls to that node without any custom `controlUrl`
|
||||
setup. This is the default path for remote gateways.
|
||||
|
||||
Notes:
|
||||
- The node host exposes its local browser control server via a **proxy command**.
|
||||
- Profiles come from the node’s own `browser.profiles` config (same as local).
|
||||
- Disable if you don’t want it:
|
||||
- On the node: `nodeHost.browserProxy.enabled=false`
|
||||
- On the gateway: `gateway.nodes.browser.mode="off"`
|
||||
|
||||
### Browserless (hosted remote CDP)
|
||||
|
||||
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
||||
|
||||
@@ -12,7 +12,6 @@ Exec approvals are the **companion app / node host guardrail** for letting a san
|
||||
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
||||
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
||||
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
||||
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used.
|
||||
|
||||
If the companion app UI is **not available**, any request that requires a prompt is
|
||||
resolved by the **ask fallback** (default: deny).
|
||||
|
||||
@@ -22,11 +22,6 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Matching is case-insensitive.
|
||||
- `*` wildcards are supported (`"*"` means all tools).
|
||||
- If `tools.allow` only references unknown or unloaded plugin tool names, Clawdbot logs a warning and ignores the allowlist so core tools stay available.
|
||||
|
||||
## Tool profiles (base allowlist)
|
||||
|
||||
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
||||
@@ -161,7 +156,6 @@ alongside tools (for example, the voice-call plugin).
|
||||
|
||||
Optional plugin tools:
|
||||
- [Lobster](/tools/lobster): typed workflow runtime with resumable approvals (requires the Lobster CLI on the gateway host).
|
||||
- [LLM Task](/tools/llm-task): JSON-only LLM step for structured workflow output (optional schema validation).
|
||||
|
||||
## Tool inventory
|
||||
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
summary: "JSON-only LLM tasks for workflows (optional plugin tool)"
|
||||
read_when:
|
||||
- You want a JSON-only LLM step inside workflows
|
||||
- You need schema-validated LLM output for automation
|
||||
---
|
||||
|
||||
# LLM Task
|
||||
|
||||
`llm-task` is an **optional plugin tool** that runs a JSON-only LLM task and
|
||||
returns structured output (optionally validated against JSON Schema).
|
||||
|
||||
This is ideal for workflow engines like Lobster: you can add a single LLM step
|
||||
without writing custom Clawdbot code for each workflow.
|
||||
|
||||
## Enable the plugin
|
||||
|
||||
1) Enable the plugin:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": { "enabled": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2) Allowlist the tool (it is registered with `optional: true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"tools": { "allow": ["llm-task"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Config (optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"defaultProvider": "openai-codex",
|
||||
"defaultModel": "gpt-5.2",
|
||||
"defaultAuthProfileId": "main",
|
||||
"allowedModels": ["openai-codex/gpt-5.2"],
|
||||
"maxTokens": 800,
|
||||
"timeoutMs": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`allowedModels` is an allowlist of `provider/model` strings. If set, any request
|
||||
outside the list is rejected.
|
||||
|
||||
## Tool parameters
|
||||
|
||||
- `prompt` (string, required)
|
||||
- `input` (any, optional)
|
||||
- `schema` (object, optional JSON Schema)
|
||||
- `provider` (string, optional)
|
||||
- `model` (string, optional)
|
||||
- `authProfileId` (string, optional)
|
||||
- `temperature` (number, optional)
|
||||
- `maxTokens` (number, optional)
|
||||
- `timeoutMs` (number, optional)
|
||||
|
||||
## Output
|
||||
|
||||
Returns `details.json` containing the parsed JSON (and validates against
|
||||
`schema` when provided).
|
||||
|
||||
## Example: Lobster workflow step
|
||||
|
||||
```lobster
|
||||
clawd.invoke --tool llm-task --action json --args-json '{
|
||||
"prompt": "Given the input email, return intent and draft.",
|
||||
"input": {
|
||||
"subject": "Hello",
|
||||
"body": "Can you help?"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intent": { "type": "string" },
|
||||
"draft": { "type": "string" }
|
||||
},
|
||||
"required": ["intent", "draft"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Safety notes
|
||||
|
||||
- The tool is **JSON-only** and instructs the model to output only JSON (no
|
||||
code fences, no commentary).
|
||||
- No tools are exposed to the model for this run.
|
||||
- Treat output as untrusted unless you validate with `schema`.
|
||||
- Put approvals before any side-effecting step (send, post, exec).
|
||||
@@ -65,52 +65,6 @@ gog.gmail.search --query 'newer_than:1d' \
|
||||
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
|
||||
```
|
||||
|
||||
## JSON-only LLM steps (llm-task)
|
||||
|
||||
For workflows that need a **structured LLM step**, enable the optional
|
||||
`llm-task` plugin tool and call it from Lobster. This keeps the workflow
|
||||
deterministic while still letting you classify/summarize/draft with a model.
|
||||
|
||||
Enable the tool:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": { "enabled": true }
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"tools": { "allow": ["llm-task"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use it in a pipeline:
|
||||
|
||||
```lobster
|
||||
clawd.invoke --tool llm-task --action json --args-json '{
|
||||
"prompt": "Given the input email, return intent and draft.",
|
||||
"input": { "subject": "Hello", "body": "Can you help?" },
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intent": { "type": "string" },
|
||||
"draft": { "type": "string" }
|
||||
},
|
||||
"required": ["intent", "draft"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
See [LLM Task](/tools/llm-task) for details and configuration options.
|
||||
|
||||
## Workflow files (.lobster)
|
||||
|
||||
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/bluebubbles",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot BlueBubbles channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
normalizeAccountId,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
@@ -63,7 +62,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
||||
resolveToolPolicy: resolveBlueBubblesGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -21,7 +21,6 @@ const bluebubblesActionSchema = z
|
||||
|
||||
const bluebubblesGroupConfigSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
});
|
||||
|
||||
const bluebubblesAccountSchema = z.object({
|
||||
|
||||
@@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "clawdbot/plugin-sdk";
|
||||
import type { ClawdbotConfig, PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
handleBlueBubblesWebhookRequest,
|
||||
@@ -129,7 +128,6 @@ function createMockRuntime(): PluginRuntime {
|
||||
session: {
|
||||
resolveStorePath: mockResolveStorePath as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
|
||||
readSessionUpdatedAt: mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
||||
recordInboundSession: vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
||||
recordSessionMetaFromInbound: vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
||||
updateLastRoute: vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
||||
},
|
||||
@@ -137,10 +135,6 @@ function createMockRuntime(): PluginRuntime {
|
||||
buildMentionRegexes: mockBuildMentionRegexes as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
|
||||
matchesMentionPatterns: mockMatchesMentionPatterns as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
||||
resolveRequireMention: mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
logAckFailure,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
resolveAckReaction,
|
||||
resolveControlCommandGate,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import { resolveAckReaction } from "clawdbot/plugin-sdk";
|
||||
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
|
||||
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
|
||||
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
||||
@@ -1352,25 +1346,23 @@ async function processMessage(
|
||||
})
|
||||
: false;
|
||||
const dmAuthorized = dmPolicy === "open" || ownerAllowedForCommands;
|
||||
const commandGate = resolveControlCommandGate({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands },
|
||||
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
|
||||
],
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCmd,
|
||||
});
|
||||
const commandAuthorized = isGroup ? commandGate.commandAuthorized : dmAuthorized;
|
||||
const commandAuthorized = isGroup
|
||||
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands },
|
||||
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
|
||||
],
|
||||
})
|
||||
: dmAuthorized;
|
||||
|
||||
// Block control commands from unauthorized senders in groups
|
||||
if (isGroup && commandGate.shouldBlock) {
|
||||
logInboundDrop({
|
||||
log: (msg) => logVerbose(core, runtime, msg),
|
||||
channel: "bluebubbles",
|
||||
reason: "control command (unauthorized)",
|
||||
target: message.senderId,
|
||||
});
|
||||
if (isGroup && hasControlCmd && !commandAuthorized) {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`bluebubbles: drop control command from unauthorized sender ${message.senderId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1529,20 +1521,19 @@ async function processMessage(
|
||||
core,
|
||||
runtime,
|
||||
});
|
||||
const shouldAckReaction = () =>
|
||||
Boolean(
|
||||
ackReactionValue &&
|
||||
core.channel.reactions.shouldAckReaction({
|
||||
scope: ackReactionScope,
|
||||
isDirect: !isGroup,
|
||||
isGroup,
|
||||
isMentionableGroup: isGroup,
|
||||
requireMention: Boolean(requireMention),
|
||||
canDetectMention,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
}),
|
||||
);
|
||||
const shouldAckReaction = () => {
|
||||
if (!ackReactionValue) return false;
|
||||
if (ackReactionScope === "all") return true;
|
||||
if (ackReactionScope === "direct") return !isGroup;
|
||||
if (ackReactionScope === "group-all") return isGroup;
|
||||
if (ackReactionScope === "group-mentions") {
|
||||
if (!isGroup) return false;
|
||||
if (!requireMention) return false;
|
||||
if (!canDetectMention) return false;
|
||||
return effectiveWasMentioned;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const ackMessageId = message.messageId?.trim() || "";
|
||||
const ackReactionPromise =
|
||||
shouldAckReaction() && ackMessageId && chatGuidForActions && ackReactionValue
|
||||
@@ -1758,27 +1749,29 @@ async function processMessage(
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
if (sentMessage && chatGuidForActions && ackMessageId) {
|
||||
core.channel.reactions.removeAckReactionAfterReply({
|
||||
removeAfterReply: removeAckAfterReply,
|
||||
ackReactionPromise,
|
||||
ackReactionValue: ackReactionValue ?? null,
|
||||
remove: () =>
|
||||
sendBlueBubblesReaction({
|
||||
chatGuid: chatGuidForActions,
|
||||
messageGuid: ackMessageId,
|
||||
emoji: ackReactionValue ?? "",
|
||||
remove: true,
|
||||
opts: { cfg: config, accountId: account.accountId },
|
||||
}),
|
||||
onError: (err) => {
|
||||
logAckFailure({
|
||||
log: (msg) => logVerbose(core, runtime, msg),
|
||||
channel: "bluebubbles",
|
||||
target: `${chatGuidForActions}/${ackMessageId}`,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
if (
|
||||
removeAckAfterReply &&
|
||||
sentMessage &&
|
||||
ackReactionPromise &&
|
||||
ackReactionValue &&
|
||||
chatGuidForActions &&
|
||||
ackMessageId
|
||||
) {
|
||||
void ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
sendBlueBubblesReaction({
|
||||
chatGuid: chatGuidForActions,
|
||||
messageGuid: ackMessageId,
|
||||
emoji: ackReactionValue,
|
||||
remove: true,
|
||||
opts: { cfg: config, accountId: account.accountId },
|
||||
}).catch((err) => {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`ack reaction removal failed chatGuid=${chatGuidForActions} msg=${ackMessageId}: ${String(err)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (chatGuidForActions && baseUrl && password && !sentMessage) {
|
||||
@@ -1787,13 +1780,7 @@ async function processMessage(
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
}).catch((err) => {
|
||||
logTypingFailure({
|
||||
log: (msg) => logVerbose(core, runtime, msg),
|
||||
channel: "bluebubbles",
|
||||
action: "stop",
|
||||
target: chatGuidForActions,
|
||||
error: err,
|
||||
});
|
||||
logVerbose(core, runtime, `typing stop (no reply) failed: ${String(err)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ export type GroupPolicy = "open" | "disabled" | "allowlist";
|
||||
export type BlueBubblesGroupConfig = {
|
||||
/** If true, only respond in this group when mentioned. */
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this group. */
|
||||
tools?: { allow?: string[]; deny?: string[] };
|
||||
};
|
||||
|
||||
export type BlueBubblesAccountConfig = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/copilot-proxy",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Copilot Proxy provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/diagnostics-otel",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot diagnostics OpenTelemetry exporter",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/discord",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Discord channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
resolveDiscordAccount,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordGroupRequireMention,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelPlugin,
|
||||
@@ -145,7 +144,6 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveDiscordGroupRequireMention,
|
||||
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
||||
},
|
||||
mentions: {
|
||||
stripPatterns: () => ["<@!?\\d+>"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/google-antigravity-auth",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Google Antigravity OAuth provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/google-gemini-cli-auth",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Gemini CLI OAuth provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/imessage",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot iMessage channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
resolveIMessageGroupRequireMention,
|
||||
resolveIMessageGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
type ResolvedIMessageAccount,
|
||||
@@ -107,7 +106,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
||||
},
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# LLM Task (plugin)
|
||||
|
||||
Adds an **optional** agent tool `llm-task` for running **JSON-only** LLM tasks
|
||||
(drafting, summarizing, classifying) with optional JSON Schema validation.
|
||||
|
||||
Designed to be called from workflow engines (for example, Lobster via
|
||||
`clawd.invoke --each`) without adding new Clawdbot code per workflow.
|
||||
|
||||
## Enable
|
||||
|
||||
1) Enable the plugin:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": { "enabled": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2) Allowlist the tool (it is registered with `optional: true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"tools": { "allow": ["llm-task"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Config (optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"llm-task": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"defaultProvider": "openai-codex",
|
||||
"defaultModel": "gpt-5.2",
|
||||
"defaultAuthProfileId": "main",
|
||||
"allowedModels": ["openai-codex/gpt-5.2"],
|
||||
"maxTokens": 800,
|
||||
"timeoutMs": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`allowedModels` is an allowlist of `provider/model` strings. If set, any request
|
||||
outside the list is rejected.
|
||||
|
||||
## Tool API
|
||||
|
||||
### Parameters
|
||||
|
||||
- `prompt` (string, required)
|
||||
- `input` (any, optional)
|
||||
- `schema` (object, optional JSON Schema)
|
||||
- `provider` (string, optional)
|
||||
- `model` (string, optional)
|
||||
- `authProfileId` (string, optional)
|
||||
- `temperature` (number, optional)
|
||||
- `maxTokens` (number, optional)
|
||||
- `timeoutMs` (number, optional)
|
||||
|
||||
### Output
|
||||
|
||||
Returns `details.json` containing the parsed JSON (and validates against
|
||||
`schema` when provided).
|
||||
|
||||
## Notes
|
||||
|
||||
- The tool is **JSON-only** and instructs the model to output only JSON
|
||||
(no code fences, no commentary).
|
||||
- No tools are exposed to the model for this run.
|
||||
- Side effects should be handled outside this tool (for example, approvals in
|
||||
Lobster) before calling tools that send messages/emails.
|
||||
|
||||
## Bundled extension note
|
||||
|
||||
This extension depends on Clawdbot internal modules (the embedded agent runner).
|
||||
It is intended to ship as a **bundled** Clawdbot extension (like `lobster`) and
|
||||
be enabled via `plugins.entries` + tool allowlists.
|
||||
|
||||
It is **not** currently designed to be copied into
|
||||
`~/.clawdbot/extensions` as a standalone plugin directory.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"id": "llm-task",
|
||||
"name": "LLM Task",
|
||||
"description": "Generic JSON-only LLM tool for structured tasks callable from workflows.",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"defaultProvider": { "type": "string" },
|
||||
"defaultModel": { "type": "string" },
|
||||
"defaultAuthProfileId": { "type": "string" },
|
||||
"allowedModels": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Allowlist of provider/model keys like openai-codex/gpt-5.2."
|
||||
},
|
||||
"maxTokens": { "type": "number" },
|
||||
"timeoutMs": { "type": "number" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
|
||||
|
||||
import { createLlmTaskTool } from "./src/llm-task-tool.js";
|
||||
|
||||
export default function register(api: ClawdbotPluginApi) {
|
||||
api.registerTool(createLlmTaskTool(api), { optional: true });
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "@clawdbot/llm-task",
|
||||
"version": "2026.1.23",
|
||||
"type": "module",
|
||||
"description": "Clawdbot JSON-only LLM task plugin",
|
||||
"clawdbot": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../../../src/agents/pi-embedded-runner.js", () => {
|
||||
return {
|
||||
runEmbeddedPiAgent: vi.fn(async () => ({
|
||||
meta: { startedAt: Date.now() },
|
||||
payloads: [{ text: "{}" }],
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
import { runEmbeddedPiAgent } from "../../../src/agents/pi-embedded-runner.js";
|
||||
import { createLlmTaskTool } from "./llm-task-tool.js";
|
||||
|
||||
function fakeApi(overrides: any = {}) {
|
||||
return {
|
||||
id: "llm-task",
|
||||
name: "llm-task",
|
||||
source: "test",
|
||||
config: { agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } } },
|
||||
pluginConfig: {},
|
||||
runtime: { version: "test" },
|
||||
logger: { debug() {}, info() {}, warn() {}, error() {} },
|
||||
registerTool() {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("llm-task tool (json-only)", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("returns parsed json", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const res = await tool.execute("id", { prompt: "return foo" });
|
||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("strips fenced json", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: "```json\n{\"ok\":true}\n```" }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const res = await tool.execute("id", { prompt: "return ok" });
|
||||
expect((res as any).details.json).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("validates schema", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: { foo: { type: "string" } },
|
||||
required: ["foo"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const res = await tool.execute("id", { prompt: "return foo", schema });
|
||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("throws on invalid json", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({ meta: {}, payloads: [{ text: "not-json" }] });
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
await expect(tool.execute("id", { prompt: "x" })).rejects.toThrow(/invalid json/i);
|
||||
});
|
||||
|
||||
it("throws on schema mismatch", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ foo: 1 }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
const schema = { type: "object", properties: { foo: { type: "string" } }, required: ["foo"] };
|
||||
await expect(tool.execute("id", { prompt: "x", schema })).rejects.toThrow(/match schema/i);
|
||||
});
|
||||
|
||||
it("passes provider/model overrides to embedded runner", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
await tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" });
|
||||
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
||||
expect(call.provider).toBe("anthropic");
|
||||
expect(call.model).toBe("claude-4-sonnet");
|
||||
});
|
||||
|
||||
it("enforces allowedModels", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }) as any);
|
||||
await expect(tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" })).rejects.toThrow(
|
||||
/not allowed/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("disables tools for embedded run", async () => {
|
||||
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi() as any);
|
||||
await tool.execute("id", { prompt: "x" });
|
||||
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
||||
expect(call.disableTools).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,218 +0,0 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import Ajv from "ajv";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// NOTE: This extension is intended to be bundled with Clawdbot.
|
||||
// When running from source (tests/dev), Clawdbot internals live under src/.
|
||||
// When running from a built install, internals live under dist/ (no src/ tree).
|
||||
// So we resolve internal imports dynamically with src-first, dist-fallback.
|
||||
|
||||
import type { ClawdbotPluginApi } from "../../../src/plugins/types.js";
|
||||
|
||||
type RunEmbeddedPiAgentFn = (params: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
|
||||
// Source checkout (tests/dev)
|
||||
try {
|
||||
const mod = await import("../../../src/agents/pi-embedded-runner.js");
|
||||
if (typeof (mod as any).runEmbeddedPiAgent === "function") return (mod as any).runEmbeddedPiAgent;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Bundled install (built)
|
||||
const mod = await import("../../../agents/pi-embedded-runner.js");
|
||||
if (typeof (mod as any).runEmbeddedPiAgent !== "function") {
|
||||
throw new Error("Internal error: runEmbeddedPiAgent not available");
|
||||
}
|
||||
return (mod as any).runEmbeddedPiAgent;
|
||||
}
|
||||
|
||||
function stripCodeFences(s: string): string {
|
||||
const trimmed = s.trim();
|
||||
const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
||||
if (m) return (m[1] ?? "").trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function collectText(payloads: Array<{ text?: string; isError?: boolean }> | undefined): string {
|
||||
const texts = (payloads ?? [])
|
||||
.filter((p) => !p.isError && typeof p.text === "string")
|
||||
.map((p) => p.text ?? "");
|
||||
return texts.join("\n").trim();
|
||||
}
|
||||
|
||||
function toModelKey(provider?: string, model?: string): string | undefined {
|
||||
const p = provider?.trim();
|
||||
const m = model?.trim();
|
||||
if (!p || !m) return undefined;
|
||||
return `${p}/${m}`;
|
||||
}
|
||||
|
||||
type PluginCfg = {
|
||||
defaultProvider?: string;
|
||||
defaultModel?: string;
|
||||
defaultAuthProfileId?: string;
|
||||
allowedModels?: string[];
|
||||
maxTokens?: number;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export function createLlmTaskTool(api: ClawdbotPluginApi) {
|
||||
return {
|
||||
name: "llm-task",
|
||||
description:
|
||||
"Run a generic JSON-only LLM task and return schema-validated JSON. Designed for orchestration from Lobster workflows via clawd.invoke.",
|
||||
parameters: Type.Object({
|
||||
prompt: Type.String({ description: "Task instruction for the LLM." }),
|
||||
input: Type.Optional(Type.Unknown({ description: "Optional input payload for the task." })),
|
||||
schema: Type.Optional(Type.Unknown({ description: "Optional JSON Schema to validate the returned JSON." })),
|
||||
provider: Type.Optional(Type.String({ description: "Provider override (e.g. openai-codex, anthropic)." })),
|
||||
model: Type.Optional(Type.String({ description: "Model id override." })),
|
||||
authProfileId: Type.Optional(Type.String({ description: "Auth profile override." })),
|
||||
temperature: Type.Optional(Type.Number({ description: "Best-effort temperature override." })),
|
||||
maxTokens: Type.Optional(Type.Number({ description: "Best-effort maxTokens override." })),
|
||||
timeoutMs: Type.Optional(Type.Number({ description: "Timeout for the LLM run." })),
|
||||
}),
|
||||
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
const prompt = String(params.prompt ?? "");
|
||||
if (!prompt.trim()) throw new Error("prompt required");
|
||||
|
||||
const pluginCfg = (api.pluginConfig ?? {}) as PluginCfg;
|
||||
|
||||
const primary = api.config?.agents?.defaults?.model?.primary;
|
||||
const primaryProvider = typeof primary === "string" ? primary.split("/")[0] : undefined;
|
||||
const primaryModel = typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined;
|
||||
|
||||
const provider =
|
||||
(typeof params.provider === "string" && params.provider.trim()) ||
|
||||
(typeof pluginCfg.defaultProvider === "string" && pluginCfg.defaultProvider.trim()) ||
|
||||
primaryProvider ||
|
||||
undefined;
|
||||
|
||||
const model =
|
||||
(typeof params.model === "string" && params.model.trim()) ||
|
||||
(typeof pluginCfg.defaultModel === "string" && pluginCfg.defaultModel.trim()) ||
|
||||
primaryModel ||
|
||||
undefined;
|
||||
|
||||
const authProfileId =
|
||||
(typeof (params as any).authProfileId === "string" && (params as any).authProfileId.trim()) ||
|
||||
(typeof pluginCfg.defaultAuthProfileId === "string" && pluginCfg.defaultAuthProfileId.trim()) ||
|
||||
undefined;
|
||||
|
||||
const modelKey = toModelKey(provider, model);
|
||||
if (!provider || !model || !modelKey) {
|
||||
throw new Error(
|
||||
`provider/model could not be resolved (provider=${String(provider ?? "")}, model=${String(model ?? "")})`,
|
||||
);
|
||||
}
|
||||
|
||||
const allowed = Array.isArray(pluginCfg.allowedModels) ? pluginCfg.allowedModels : undefined;
|
||||
if (allowed && allowed.length > 0 && !allowed.includes(modelKey)) {
|
||||
throw new Error(
|
||||
`Model not allowed by llm-task plugin config: ${modelKey}. Allowed models: ${allowed.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const timeoutMs =
|
||||
(typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : undefined) ||
|
||||
(typeof pluginCfg.timeoutMs === "number" && pluginCfg.timeoutMs > 0 ? pluginCfg.timeoutMs : undefined) ||
|
||||
30_000;
|
||||
|
||||
const streamParams = {
|
||||
temperature: typeof params.temperature === "number" ? params.temperature : undefined,
|
||||
maxTokens:
|
||||
typeof params.maxTokens === "number"
|
||||
? params.maxTokens
|
||||
: typeof pluginCfg.maxTokens === "number"
|
||||
? pluginCfg.maxTokens
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const input = (params as any).input as unknown;
|
||||
let inputJson: string;
|
||||
try {
|
||||
inputJson = JSON.stringify(input ?? null, null, 2);
|
||||
} catch {
|
||||
throw new Error("input must be JSON-serializable");
|
||||
}
|
||||
|
||||
const system = [
|
||||
"You are a JSON-only function.",
|
||||
"Return ONLY a valid JSON value.",
|
||||
"Do not wrap in markdown fences.",
|
||||
"Do not include commentary.",
|
||||
"Do not call tools.",
|
||||
].join(" ");
|
||||
|
||||
const fullPrompt = `${system}\n\nTASK:\n${prompt}\n\nINPUT_JSON:\n${inputJson}\n`;
|
||||
|
||||
let tmpDir: string | null = null;
|
||||
try {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-llm-task-"));
|
||||
const sessionId = `llm-task-${Date.now()}`;
|
||||
const sessionFile = path.join(tmpDir, "session.json");
|
||||
|
||||
const runEmbeddedPiAgent = await loadRunEmbeddedPiAgent();
|
||||
|
||||
const result = await runEmbeddedPiAgent({
|
||||
sessionId,
|
||||
sessionFile,
|
||||
workspaceDir: api.config?.agents?.defaults?.workspace ?? process.cwd(),
|
||||
config: api.config,
|
||||
prompt: fullPrompt,
|
||||
timeoutMs,
|
||||
runId: `llm-task-${Date.now()}`,
|
||||
provider,
|
||||
model,
|
||||
authProfileId,
|
||||
authProfileIdSource: authProfileId ? "user" : "auto",
|
||||
streamParams,
|
||||
disableTools: true,
|
||||
});
|
||||
|
||||
const text = collectText((result as any).payloads);
|
||||
if (!text) throw new Error("LLM returned empty output");
|
||||
|
||||
const raw = stripCodeFences(text);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new Error("LLM returned invalid JSON");
|
||||
}
|
||||
|
||||
const schema = (params as any).schema as unknown;
|
||||
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
||||
const validate = ajv.compile(schema as any);
|
||||
const ok = validate(parsed);
|
||||
if (!ok) {
|
||||
const msg =
|
||||
validate.errors?.map((e) => `${e.instancePath || "<root>"} ${e.message || "invalid"}`).join("; ") ??
|
||||
"invalid";
|
||||
throw new Error(`LLM JSON did not match schema: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
||||
details: { json: parsed, provider, model },
|
||||
};
|
||||
} finally {
|
||||
if (tmpDir) {
|
||||
try {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/lobster",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.23
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.22
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/matrix",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Matrix channel plugin",
|
||||
"clawdbot": {
|
||||
@@ -25,12 +25,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||
"clawdbot": "workspace:*",
|
||||
"markdown-it": "14.1.0",
|
||||
"matrix-bot-sdk": "0.8.0",
|
||||
"music-metadata": "^11.10.6",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clawdbot": "workspace:*"
|
||||
"music-metadata": "^11.10.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
import { matrixMessageActions } from "./actions.js";
|
||||
import { MatrixConfigSchema } from "./config-schema.js";
|
||||
import { resolveMatrixGroupRequireMention, resolveMatrixGroupToolPolicy } from "./group-mentions.js";
|
||||
import { resolveMatrixGroupRequireMention } from "./group-mentions.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
import {
|
||||
listMatrixAccountIds,
|
||||
@@ -167,7 +167,6 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveMatrixGroupRequireMention,
|
||||
resolveToolPolicy: resolveMatrixGroupToolPolicy,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -26,7 +26,6 @@ const matrixRoomSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
autoReply: z.boolean().optional(),
|
||||
users: z.array(allowFromEntry).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
||||
import type { ChannelGroupContext } from "clawdbot/plugin-sdk";
|
||||
|
||||
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
@@ -32,30 +32,3 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveMatrixGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const rawGroupId = params.groupId?.trim() ?? "";
|
||||
let roomId = rawGroupId;
|
||||
const lower = roomId.toLowerCase();
|
||||
if (lower.startsWith("matrix:")) {
|
||||
roomId = roomId.slice("matrix:".length).trim();
|
||||
}
|
||||
if (roomId.toLowerCase().startsWith("channel:")) {
|
||||
roomId = roomId.slice("channel:".length).trim();
|
||||
}
|
||||
if (roomId.toLowerCase().startsWith("room:")) {
|
||||
roomId = roomId.slice("room:".length).trim();
|
||||
}
|
||||
const groupChannel = params.groupChannel?.trim() ?? "";
|
||||
const aliases = groupChannel ? [groupChannel] : [];
|
||||
const cfg = params.cfg as CoreConfig;
|
||||
const resolved = resolveMatrixRoomConfig({
|
||||
rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
|
||||
roomId,
|
||||
aliases,
|
||||
name: groupChannel || undefined,
|
||||
}).config;
|
||||
return resolved?.tools;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { LocationMessageEventContent, MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import {
|
||||
createReplyPrefixContext,
|
||||
createTypingCallbacks,
|
||||
formatAllowlistMatchMeta,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
resolveControlCommandGate,
|
||||
type RuntimeEnv,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
||||
@@ -381,25 +376,21 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
userName: senderName,
|
||||
})
|
||||
: false;
|
||||
const hasControlCommandInMessage = core.channel.text.hasControlCommand(bodyText, cfg);
|
||||
const commandGate = resolveControlCommandGate({
|
||||
const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
||||
{ configured: roomUsers.length > 0, allowed: senderAllowedForRoomUsers },
|
||||
{ configured: groupAllowConfigured, allowed: senderAllowedForGroup },
|
||||
],
|
||||
allowTextCommands,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
});
|
||||
const commandAuthorized = commandGate.commandAuthorized;
|
||||
if (isRoom && commandGate.shouldBlock) {
|
||||
logInboundDrop({
|
||||
log: logVerboseMessage,
|
||||
channel: "matrix",
|
||||
reason: "control command (unauthorized)",
|
||||
target: senderId,
|
||||
});
|
||||
if (
|
||||
isRoom &&
|
||||
allowTextCommands &&
|
||||
core.channel.text.hasControlCommand(bodyText, cfg) &&
|
||||
!commandAuthorized
|
||||
) {
|
||||
logVerboseMessage(`matrix: drop control command from unauthorized sender ${senderId}`);
|
||||
return;
|
||||
}
|
||||
const shouldRequireMention = isRoom
|
||||
@@ -418,8 +409,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
!wasMentioned &&
|
||||
!hasExplicitMention &&
|
||||
commandAuthorized &&
|
||||
hasControlCommandInMessage;
|
||||
const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention;
|
||||
core.channel.text.hasControlCommand(bodyText);
|
||||
if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) {
|
||||
logger.info({ roomId, reason: "no-mention" }, "skipping room message");
|
||||
return;
|
||||
@@ -496,45 +486,47 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
OriginatingTo: `room:${roomId}`,
|
||||
});
|
||||
|
||||
await core.channel.session.recordInboundSession({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
updateLastRoute: isDirectMessage
|
||||
? {
|
||||
sessionKey: route.mainSessionKey,
|
||||
channel: "matrix",
|
||||
to: `room:${roomId}`,
|
||||
accountId: route.accountId,
|
||||
}
|
||||
: undefined,
|
||||
onRecordError: (err) => {
|
||||
void core.channel.session
|
||||
.recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.warn(
|
||||
{ error: String(err), storePath, sessionKey: ctxPayload.SessionKey ?? route.sessionKey },
|
||||
"failed updating session meta",
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (isDirectMessage) {
|
||||
await core.channel.session.updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: route.mainSessionKey,
|
||||
channel: "matrix",
|
||||
to: `room:${roomId}`,
|
||||
accountId: route.accountId,
|
||||
ctx: ctxPayload,
|
||||
});
|
||||
}
|
||||
|
||||
const preview = bodyText.slice(0, 200).replace(/\n/g, "\\n");
|
||||
logVerboseMessage(`matrix inbound: room=${roomId} from=${senderId} preview="${preview}"`);
|
||||
|
||||
const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
|
||||
const ackScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
const shouldAckReaction = () =>
|
||||
Boolean(
|
||||
ackReaction &&
|
||||
core.channel.reactions.shouldAckReaction({
|
||||
scope: ackScope,
|
||||
isDirect: isDirectMessage,
|
||||
isGroup: isRoom,
|
||||
isMentionableGroup: isRoom,
|
||||
requireMention: Boolean(shouldRequireMention),
|
||||
canDetectMention,
|
||||
effectiveWasMentioned: wasMentioned || shouldBypassMention,
|
||||
shouldBypassMention,
|
||||
}),
|
||||
);
|
||||
const shouldAckReaction = () => {
|
||||
if (!ackReaction) return false;
|
||||
if (ackScope === "all") return true;
|
||||
if (ackScope === "direct") return isDirectMessage;
|
||||
if (ackScope === "group-all") return isRoom;
|
||||
if (ackScope === "group-mentions") {
|
||||
if (!isRoom) return false;
|
||||
if (!shouldRequireMention) return false;
|
||||
return wasMentioned || shouldBypassMention;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (shouldAckReaction() && messageId) {
|
||||
reactMatrixMessage(roomId, messageId, ackReaction, client).catch((err) => {
|
||||
logVerboseMessage(`matrix react failed for room ${roomId}: ${String(err)}`);
|
||||
@@ -561,33 +553,10 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
channel: "matrix",
|
||||
accountId: route.accountId,
|
||||
});
|
||||
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
|
||||
const typingCallbacks = createTypingCallbacks({
|
||||
start: () => sendTypingMatrix(roomId, true, undefined, client),
|
||||
stop: () => sendTypingMatrix(roomId, false, undefined, client),
|
||||
onStartError: (err) => {
|
||||
logTypingFailure({
|
||||
log: logVerboseMessage,
|
||||
channel: "matrix",
|
||||
action: "start",
|
||||
target: roomId,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
onStopError: (err) => {
|
||||
logTypingFailure({
|
||||
log: logVerboseMessage,
|
||||
channel: "matrix",
|
||||
action: "stop",
|
||||
target: roomId,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
core.channel.reply.createReplyDispatcherWithTyping({
|
||||
responsePrefix: prefixContext.responsePrefix,
|
||||
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
||||
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
.responsePrefix,
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload) => {
|
||||
await deliverMatrixReplies({
|
||||
@@ -606,8 +575,10 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(`matrix ${info.kind} reply failed: ${String(err)}`);
|
||||
},
|
||||
onReplyStart: typingCallbacks.onReplyStart,
|
||||
onIdle: typingCallbacks.onIdle,
|
||||
onReplyStart: () =>
|
||||
sendTypingMatrix(roomId, true, undefined, client).catch(() => {}),
|
||||
onIdle: () =>
|
||||
sendTypingMatrix(roomId, false, undefined, client).catch(() => {}),
|
||||
});
|
||||
|
||||
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
||||
@@ -617,7 +588,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
replyOptions: {
|
||||
...replyOptions,
|
||||
skillFilter: roomConfig?.skills,
|
||||
onModelSelected: prefixContext.onModelSelected,
|
||||
},
|
||||
});
|
||||
markDispatchIdle();
|
||||
|
||||
@@ -18,8 +18,6 @@ export type MatrixRoomConfig = {
|
||||
allow?: boolean;
|
||||
/** Require mentioning the bot to trigger replies. */
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this room. */
|
||||
tools?: { allow?: string[]; deny?: string[] };
|
||||
/** If true, reply without mention requirements. */
|
||||
autoReply?: boolean;
|
||||
/** Optional allowlist for room senders (user IDs or localparts). */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/mattermost",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Mattermost channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -7,15 +7,10 @@ import type {
|
||||
RuntimeEnv,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
createReplyPrefixContext,
|
||||
createTypingCallbacks,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntriesIfEnabled,
|
||||
clearHistoryEntries,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
resolveControlCommandGate,
|
||||
recordPendingHistoryEntry,
|
||||
resolveChannelMediaMaxBytes,
|
||||
type HistoryEntry,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
@@ -35,9 +30,12 @@ import {
|
||||
} from "./client.js";
|
||||
import {
|
||||
createDedupeCache,
|
||||
extractShortModelName,
|
||||
formatInboundFromLabel,
|
||||
rawDataToString,
|
||||
resolveIdentityName,
|
||||
resolveThreadSessionKeys,
|
||||
type ResponsePrefixContext,
|
||||
} from "./monitor-helpers.js";
|
||||
import { sendMessageMattermost } from "./send.js";
|
||||
|
||||
@@ -309,7 +307,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
};
|
||||
|
||||
const sendTypingIndicator = async (channelId: string, parentId?: string) => {
|
||||
await sendMattermostTyping(client, { channelId, parentId });
|
||||
try {
|
||||
await sendMattermostTyping(client, { channelId, parentId });
|
||||
} catch (err) {
|
||||
logger.debug?.(`mattermost typing cue failed for channel ${channelId}: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveChannelInfo = async (channelId: string): Promise<MattermostChannel | null> => {
|
||||
@@ -401,8 +403,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
cfg,
|
||||
surface: "mattermost",
|
||||
});
|
||||
const hasControlCommand = core.channel.text.hasControlCommand(rawText, cfg);
|
||||
const isControlCommand = allowTextCommands && hasControlCommand;
|
||||
const isControlCommand = allowTextCommands && core.channel.text.hasControlCommand(rawText, cfg);
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const senderAllowedForCommands = isSenderAllowed({
|
||||
senderId,
|
||||
@@ -414,20 +415,19 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
senderName,
|
||||
allowFrom: effectiveGroupAllowFrom,
|
||||
});
|
||||
const commandGate = resolveControlCommandGate({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
||||
{
|
||||
configured: effectiveGroupAllowFrom.length > 0,
|
||||
allowed: groupAllowedForCommands,
|
||||
},
|
||||
],
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
});
|
||||
const commandAuthorized =
|
||||
kind === "dm" ? dmPolicy === "open" || senderAllowedForCommands : commandGate.commandAuthorized;
|
||||
kind === "dm"
|
||||
? dmPolicy === "open" || senderAllowedForCommands
|
||||
: core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
||||
{
|
||||
configured: effectiveGroupAllowFrom.length > 0,
|
||||
allowed: groupAllowedForCommands,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (kind === "dm") {
|
||||
if (dmPolicy === "disabled") {
|
||||
@@ -488,13 +488,10 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
}
|
||||
}
|
||||
|
||||
if (kind !== "dm" && commandGate.shouldBlock) {
|
||||
logInboundDrop({
|
||||
log: logVerboseMessage,
|
||||
channel: "mattermost",
|
||||
reason: "control command (unauthorized)",
|
||||
target: senderId,
|
||||
});
|
||||
if (kind !== "dm" && isControlCommand && !commandAuthorized) {
|
||||
logVerboseMessage(
|
||||
`mattermost: drop control command from unauthorized sender ${senderId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -537,19 +534,19 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
: "");
|
||||
const pendingSender = senderName;
|
||||
const recordPendingHistory = () => {
|
||||
if (!historyKey || historyLimit <= 0) return;
|
||||
const trimmed = pendingBody.trim();
|
||||
recordPendingHistoryEntryIfEnabled({
|
||||
if (!trimmed) return;
|
||||
recordPendingHistoryEntry({
|
||||
historyMap: channelHistories,
|
||||
historyKey,
|
||||
limit: historyLimit,
|
||||
historyKey: historyKey ?? "",
|
||||
entry: historyKey && trimmed
|
||||
? {
|
||||
sender: pendingSender,
|
||||
body: trimmed,
|
||||
timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
|
||||
messageId: post.id ?? undefined,
|
||||
}
|
||||
: null,
|
||||
entry: {
|
||||
sender: pendingSender,
|
||||
body: trimmed,
|
||||
timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
|
||||
messageId: post.id ?? undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -626,7 +623,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
sender: { name: senderName, id: senderId },
|
||||
});
|
||||
let combinedBody = body;
|
||||
if (historyKey) {
|
||||
if (historyKey && historyLimit > 0) {
|
||||
combinedBody = buildPendingHistoryContextFromMap({
|
||||
historyMap: channelHistories,
|
||||
historyKey,
|
||||
@@ -716,23 +713,15 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
accountId: account.accountId,
|
||||
});
|
||||
|
||||
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
|
||||
let prefixContext: ResponsePrefixContext = {
|
||||
identityName: resolveIdentityName(cfg, route.agentId),
|
||||
};
|
||||
|
||||
const typingCallbacks = createTypingCallbacks({
|
||||
start: () => sendTypingIndicator(channelId, threadRootId),
|
||||
onStartError: (err) => {
|
||||
logTypingFailure({
|
||||
log: (message) => logger.debug?.(message),
|
||||
channel: "mattermost",
|
||||
target: channelId,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
core.channel.reply.createReplyDispatcherWithTyping({
|
||||
responsePrefix: prefixContext.responsePrefix,
|
||||
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
||||
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
.responsePrefix,
|
||||
responsePrefixContextProvider: () => prefixContext,
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload: ReplyPayload) => {
|
||||
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
@@ -763,7 +752,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(`mattermost ${info.kind} reply failed: ${String(err)}`);
|
||||
},
|
||||
onReplyStart: typingCallbacks.onReplyStart,
|
||||
onReplyStart: () => sendTypingIndicator(channelId, threadRootId),
|
||||
});
|
||||
|
||||
await core.channel.reply.dispatchReplyFromConfig({
|
||||
@@ -774,12 +763,17 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
...replyOptions,
|
||||
disableBlockStreaming:
|
||||
typeof account.blockStreaming === "boolean" ? !account.blockStreaming : undefined,
|
||||
onModelSelected: prefixContext.onModelSelected,
|
||||
onModelSelected: (ctx) => {
|
||||
prefixContext.provider = ctx.provider;
|
||||
prefixContext.model = extractShortModelName(ctx.model);
|
||||
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
|
||||
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
|
||||
},
|
||||
},
|
||||
});
|
||||
markDispatchIdle();
|
||||
if (historyKey) {
|
||||
clearHistoryEntriesIfEnabled({ historyMap: channelHistories, historyKey, limit: historyLimit });
|
||||
if (historyKey && historyLimit > 0) {
|
||||
clearHistoryEntries({ historyMap: channelHistories, historyKey });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/memory-core",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot core memory search plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/memory-lancedb",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.23
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.22
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/msteams",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Microsoft Teams channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -68,10 +68,10 @@ function scopeCandidatesForUrl(url: string): string[] {
|
||||
host.endsWith("1drv.ms") ||
|
||||
host.includes("sharepoint");
|
||||
return looksLikeGraph
|
||||
? ["https://graph.microsoft.com", "https://api.botframework.com"]
|
||||
: ["https://api.botframework.com", "https://graph.microsoft.com"];
|
||||
? ["https://graph.microsoft.com/.default", "https://api.botframework.com/.default"]
|
||||
: ["https://api.botframework.com/.default", "https://graph.microsoft.com/.default"];
|
||||
} catch {
|
||||
return ["https://api.botframework.com", "https://graph.microsoft.com"];
|
||||
return ["https://api.botframework.com/.default", "https://graph.microsoft.com/.default"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ export async function downloadMSTeamsGraphMedia(params: {
|
||||
const messageUrl = params.messageUrl;
|
||||
let accessToken: string;
|
||||
try {
|
||||
accessToken = await params.tokenProvider.getAccessToken("https://graph.microsoft.com");
|
||||
accessToken = await params.tokenProvider.getAccessToken("https://graph.microsoft.com/.default");
|
||||
} catch {
|
||||
return { media: [], messageUrl, tokenError: true };
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
||||
import { msteamsOutbound } from "./outbound.js";
|
||||
import { probeMSTeams } from "./probe.js";
|
||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||
import {
|
||||
normalizeMSTeamsMessagingTarget,
|
||||
normalizeMSTeamsUserInput,
|
||||
@@ -78,9 +77,6 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
hasRepliedRef,
|
||||
}),
|
||||
},
|
||||
groups: {
|
||||
resolveToolPolicy: resolveMSTeamsGroupToolPolicy,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.msteams"] },
|
||||
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
||||
config: {
|
||||
|
||||
@@ -64,7 +64,7 @@ async function resolveGraphToken(cfg: unknown): Promise<string> {
|
||||
if (!creds) throw new Error("MS Teams credentials missing");
|
||||
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
||||
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
|
||||
const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
|
||||
const token = await tokenProvider.getAccessToken("https://graph.microsoft.com/.default");
|
||||
const accessToken = readAccessToken(token);
|
||||
if (!accessToken) throw new Error("MS Teams graph token unavailable");
|
||||
return accessToken;
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
|
||||
|
||||
const GRAPH_ROOT = "https://graph.microsoft.com/v1.0";
|
||||
const GRAPH_BETA = "https://graph.microsoft.com/beta";
|
||||
const GRAPH_SCOPE = "https://graph.microsoft.com";
|
||||
const GRAPH_SCOPE = "https://graph.microsoft.com/.default";
|
||||
|
||||
export interface OneDriveUploadResult {
|
||||
id: string;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntriesIfEnabled,
|
||||
clearHistoryEntries,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
logInboundDrop,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
resolveControlCommandGate,
|
||||
recordPendingHistoryEntry,
|
||||
resolveMentionGating,
|
||||
formatAllowlistMatchMeta,
|
||||
type HistoryEntry,
|
||||
@@ -253,24 +251,15 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
senderId,
|
||||
senderName,
|
||||
});
|
||||
const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg);
|
||||
const commandGate = resolveControlCommandGate({
|
||||
const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveDmAllowFrom.length > 0, allowed: ownerAllowedForCommands },
|
||||
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
|
||||
],
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
});
|
||||
const commandAuthorized = commandGate.commandAuthorized;
|
||||
if (commandGate.shouldBlock) {
|
||||
logInboundDrop({
|
||||
log: logVerboseMessage,
|
||||
channel: "msteams",
|
||||
reason: "control command (unauthorized)",
|
||||
target: senderId,
|
||||
});
|
||||
if (core.channel.text.hasControlCommand(text, cfg) && !commandAuthorized) {
|
||||
logVerboseMessage(`msteams: drop control command from unauthorized sender ${senderId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,17 +371,19 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
requireMention,
|
||||
mentioned,
|
||||
});
|
||||
recordPendingHistoryEntryIfEnabled({
|
||||
historyMap: conversationHistories,
|
||||
historyKey: conversationId,
|
||||
limit: historyLimit,
|
||||
entry: {
|
||||
sender: senderName,
|
||||
body: rawBody,
|
||||
timestamp: timestamp?.getTime(),
|
||||
messageId: activity.id ?? undefined,
|
||||
},
|
||||
});
|
||||
if (historyLimit > 0) {
|
||||
recordPendingHistoryEntry({
|
||||
historyMap: conversationHistories,
|
||||
historyKey: conversationId,
|
||||
limit: historyLimit,
|
||||
entry: {
|
||||
sender: senderName,
|
||||
body: rawBody,
|
||||
timestamp: timestamp?.getTime(),
|
||||
messageId: activity.id ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -435,7 +426,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
let combinedBody = body;
|
||||
const isRoomish = !isDirectMessage;
|
||||
const historyKey = isRoomish ? conversationId : undefined;
|
||||
if (isRoomish && historyKey) {
|
||||
if (isRoomish && historyKey && historyLimit > 0) {
|
||||
combinedBody = buildPendingHistoryContextFromMap({
|
||||
historyMap: conversationHistories,
|
||||
historyKey,
|
||||
@@ -476,13 +467,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
...mediaPayload,
|
||||
});
|
||||
|
||||
await core.channel.session.recordInboundSession({
|
||||
storePath,
|
||||
void core.channel.session.recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
onRecordError: (err) => {
|
||||
logVerboseMessage(`msteams: failed updating session meta: ${String(err)}`);
|
||||
},
|
||||
}).catch((err) => {
|
||||
logVerboseMessage(`msteams: failed updating session meta: ${String(err)}`);
|
||||
});
|
||||
|
||||
logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
|
||||
@@ -522,11 +512,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
|
||||
const didSendReply = counts.final + counts.tool + counts.block > 0;
|
||||
if (!queuedFinal) {
|
||||
if (isRoomish && historyKey) {
|
||||
clearHistoryEntriesIfEnabled({
|
||||
if (isRoomish && historyKey && historyLimit > 0) {
|
||||
clearHistoryEntries({
|
||||
historyMap: conversationHistories,
|
||||
historyKey,
|
||||
limit: historyLimit,
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -535,12 +524,8 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
logVerboseMessage(
|
||||
`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`,
|
||||
);
|
||||
if (isRoomish && historyKey) {
|
||||
clearHistoryEntriesIfEnabled({
|
||||
historyMap: conversationHistories,
|
||||
historyKey,
|
||||
limit: historyLimit,
|
||||
});
|
||||
if (isRoomish && historyKey && historyLimit > 0) {
|
||||
clearHistoryEntries({ historyMap: conversationHistories, historyKey });
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("dispatch failed", { error: String(err) });
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type {
|
||||
AllowlistMatch,
|
||||
ChannelGroupContext,
|
||||
GroupPolicy,
|
||||
GroupToolPolicyConfig,
|
||||
MSTeamsChannelConfig,
|
||||
MSTeamsConfig,
|
||||
MSTeamsReplyStyle,
|
||||
@@ -88,50 +86,6 @@ export function resolveMSTeamsRouteConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMSTeamsGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const cfg = params.cfg.channels?.msteams;
|
||||
if (!cfg) return undefined;
|
||||
const groupId = params.groupId?.trim();
|
||||
const groupChannel = params.groupChannel?.trim();
|
||||
const groupSpace = params.groupSpace?.trim();
|
||||
|
||||
const resolved = resolveMSTeamsRouteConfig({
|
||||
cfg,
|
||||
teamId: groupSpace,
|
||||
teamName: groupSpace,
|
||||
conversationId: groupId,
|
||||
channelName: groupChannel,
|
||||
});
|
||||
|
||||
if (resolved.channelConfig) {
|
||||
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools;
|
||||
}
|
||||
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
|
||||
|
||||
if (!groupId) return undefined;
|
||||
|
||||
const channelCandidates = buildChannelKeyCandidates(
|
||||
groupId,
|
||||
groupChannel,
|
||||
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
||||
);
|
||||
for (const teamConfig of Object.values(cfg.teams ?? {})) {
|
||||
const match = resolveChannelEntryMatchWithFallback({
|
||||
entries: teamConfig?.channels ?? {},
|
||||
keys: channelCandidates,
|
||||
wildcardKey: "*",
|
||||
normalizeKey: normalizeChannelSlug,
|
||||
});
|
||||
if (match.entry) {
|
||||
return match.entry.tools ?? teamConfig?.tools;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type MSTeamsReplyPolicy = {
|
||||
requireMention: boolean;
|
||||
replyStyle: MSTeamsReplyStyle;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
createReplyPrefixContext,
|
||||
createTypingCallbacks,
|
||||
logTypingFailure,
|
||||
resolveChannelMediaMaxBytes,
|
||||
type ClawdbotConfig,
|
||||
type MSTeamsReplyStyle,
|
||||
@@ -42,33 +39,23 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
}) {
|
||||
const core = getMSTeamsRuntime();
|
||||
const sendTypingIndicator = async () => {
|
||||
await params.context.sendActivities([{ type: "typing" }]);
|
||||
try {
|
||||
await params.context.sendActivities([{ type: "typing" }]);
|
||||
} catch {
|
||||
// Typing indicator is best-effort.
|
||||
}
|
||||
};
|
||||
const typingCallbacks = createTypingCallbacks({
|
||||
start: sendTypingIndicator,
|
||||
onStartError: (err) => {
|
||||
logTypingFailure({
|
||||
log: (message) => params.log.debug(message),
|
||||
channel: "msteams",
|
||||
action: "start",
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
});
|
||||
const prefixContext = createReplyPrefixContext({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
core.channel.reply.createReplyDispatcherWithTyping({
|
||||
responsePrefix: prefixContext.responsePrefix,
|
||||
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
|
||||
deliver: async (payload) => {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: params.cfg,
|
||||
channel: "msteams",
|
||||
return core.channel.reply.createReplyDispatcherWithTyping({
|
||||
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(
|
||||
params.cfg,
|
||||
params.agentId,
|
||||
).responsePrefix,
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
|
||||
deliver: async (payload) => {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: params.cfg,
|
||||
channel: "msteams",
|
||||
});
|
||||
const messages = renderReplyPayloadsToMessages([payload], {
|
||||
textChunkLimit: params.textLimit,
|
||||
@@ -100,27 +87,21 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
mediaMaxBytes,
|
||||
});
|
||||
if (ids.length > 0) params.onSentMessageIds?.(ids);
|
||||
},
|
||||
onError: (err, info) => {
|
||||
const errMsg = formatUnknownError(err);
|
||||
const classification = classifyMSTeamsSendError(err);
|
||||
const hint = formatMSTeamsSendErrorHint(classification);
|
||||
params.runtime.error?.(
|
||||
`msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`,
|
||||
);
|
||||
params.log.error("reply failed", {
|
||||
kind: info.kind,
|
||||
error: errMsg,
|
||||
classification,
|
||||
hint,
|
||||
});
|
||||
},
|
||||
onReplyStart: typingCallbacks.onReplyStart,
|
||||
});
|
||||
|
||||
return {
|
||||
dispatcher,
|
||||
replyOptions: { ...replyOptions, onModelSelected: prefixContext.onModelSelected },
|
||||
markDispatchIdle,
|
||||
};
|
||||
},
|
||||
onError: (err, info) => {
|
||||
const errMsg = formatUnknownError(err);
|
||||
const classification = classifyMSTeamsSendError(err);
|
||||
const hint = formatMSTeamsSendErrorHint(classification);
|
||||
params.runtime.error?.(
|
||||
`msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`,
|
||||
);
|
||||
params.log.error("reply failed", {
|
||||
kind: info.kind,
|
||||
error: errMsg,
|
||||
classification,
|
||||
hint,
|
||||
});
|
||||
},
|
||||
onReplyStart: sendTypingIndicator,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ async function resolveGraphToken(cfg: unknown): Promise<string> {
|
||||
if (!creds) throw new Error("MS Teams credentials missing");
|
||||
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
||||
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
|
||||
const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
|
||||
const token = await tokenProvider.getAccessToken("https://graph.microsoft.com/.default");
|
||||
const accessToken = readAccessToken(token);
|
||||
if (!accessToken) throw new Error("MS Teams graph token unavailable");
|
||||
return accessToken;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/nextcloud-talk",
|
||||
"version": "2026.1.23",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Nextcloud Talk channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -24,7 +24,6 @@ import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
|
||||
import { getNextcloudTalkRuntime } from "./runtime.js";
|
||||
import { sendMessageNextcloudTalk } from "./send.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
||||
|
||||
const meta = {
|
||||
id: "nextcloud-talk",
|
||||
@@ -160,7 +159,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
|
||||
return true;
|
||||
},
|
||||
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
ToolPolicySchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
@@ -12,7 +11,6 @@ import { z } from "zod";
|
||||
export const NextcloudTalkRoomSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.string()).optional(),
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
logInboundDrop,
|
||||
resolveControlCommandGate,
|
||||
type ClawdbotConfig,
|
||||
type RuntimeEnv,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
|
||||
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
||||
import {
|
||||
@@ -123,11 +118,7 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
senderId,
|
||||
senderName,
|
||||
}).allowed;
|
||||
const hasControlCommand = core.channel.text.hasControlCommand(
|
||||
rawBody,
|
||||
config as ClawdbotConfig,
|
||||
);
|
||||
const commandGate = resolveControlCommandGate({
|
||||
const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{
|
||||
@@ -136,10 +127,7 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
allowed: senderAllowedForCommands,
|
||||
},
|
||||
],
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
});
|
||||
const commandAuthorized = commandGate.commandAuthorized;
|
||||
|
||||
if (isGroup) {
|
||||
const groupAllow = resolveNextcloudTalkGroupAllow({
|
||||
@@ -200,13 +188,15 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (isGroup && commandGate.shouldBlock) {
|
||||
logInboundDrop({
|
||||
log: (message) => runtime.log?.(message),
|
||||
channel: CHANNEL_ID,
|
||||
reason: "control command (unauthorized)",
|
||||
target: senderId,
|
||||
});
|
||||
if (
|
||||
isGroup &&
|
||||
allowTextCommands &&
|
||||
core.channel.text.hasControlCommand(rawBody, config as ClawdbotConfig) &&
|
||||
commandAuthorized !== true
|
||||
) {
|
||||
runtime.log?.(
|
||||
`nextcloud-talk: drop control command from unauthorized sender ${senderId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,6 +212,10 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
wildcardConfig: roomMatch.wildcardConfig,
|
||||
})
|
||||
: false;
|
||||
const hasControlCommand = core.channel.text.hasControlCommand(
|
||||
rawBody,
|
||||
config as ClawdbotConfig,
|
||||
);
|
||||
const mentionGate = resolveNextcloudTalkMentionGate({
|
||||
isGroup,
|
||||
requireMention: shouldRequireMention,
|
||||
@@ -293,14 +287,15 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
CommandAuthorized: commandAuthorized,
|
||||
});
|
||||
|
||||
await core.channel.session.recordInboundSession({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
onRecordError: (err) => {
|
||||
void core.channel.session
|
||||
.recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
})
|
||||
.catch((err) => {
|
||||
runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AllowlistMatch, ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
||||
import type { AllowlistMatch, GroupPolicy } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
buildChannelKeyCandidates,
|
||||
normalizeChannelSlug,
|
||||
@@ -86,21 +86,6 @@ export function resolveNextcloudTalkRoomMatch(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveNextcloudTalkGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const cfg = params.cfg as { channels?: { "nextcloud-talk"?: { rooms?: Record<string, NextcloudTalkRoomConfig> } } };
|
||||
const roomToken = params.groupId?.trim();
|
||||
if (!roomToken) return undefined;
|
||||
const roomName = params.groupChannel?.trim() || undefined;
|
||||
const match = resolveNextcloudTalkRoomMatch({
|
||||
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
|
||||
roomToken,
|
||||
roomName,
|
||||
});
|
||||
return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
|
||||
}
|
||||
|
||||
export function resolveNextcloudTalkRequireMention(params: {
|
||||
roomConfig?: NextcloudTalkRoomConfig;
|
||||
wildcardConfig?: NextcloudTalkRoomConfig;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user