Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07cc816969 | ||
|
|
b0d0914ca0 | ||
|
|
b33334620a | ||
|
|
ebc4fe57d7 | ||
|
|
5909cf36b9 | ||
|
|
e3a97b96d9 | ||
|
|
4942f3af0f | ||
|
|
ad44e32910 | ||
|
|
b0b77aae88 | ||
|
|
007f8c9222 | ||
|
|
dbd5a76cb0 | ||
|
|
630b256aee | ||
|
|
e170bc397d | ||
|
|
d2f033aee5 | ||
|
|
8f12b47b6d | ||
|
|
e8b382e8ed |
No files matched your search
@@ -74,9 +74,9 @@ jobs:
|
||||
- runtime: node
|
||||
task: protocol
|
||||
command: pnpm protocol:check
|
||||
- runtime: node
|
||||
task: format
|
||||
command: pnpm format
|
||||
- runtime: bun
|
||||
task: lint
|
||||
command: bunx biome check src
|
||||
- runtime: bun
|
||||
task: test
|
||||
command: bunx vitest run
|
||||
|
||||
@@ -29,5 +29,4 @@ jobs:
|
||||
CLAWDBOT_INSTALL_CLI_URL: https://clawd.bot/install-cli.sh
|
||||
CLAWDBOT_NO_ONBOARD: "1"
|
||||
CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1"
|
||||
CLAWDBOT_INSTALL_SMOKE_PREVIOUS: "2026.1.11-4"
|
||||
run: pnpm test:install:smoke
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"indentWidth": 2,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/oxlintrc",
|
||||
"extends": ["recommended"]
|
||||
}
|
||||
@@ -25,12 +25,12 @@
|
||||
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
|
||||
- Node remains supported for running built output (`dist/*`) and production installs.
|
||||
- Type-check/build: `pnpm build` (tsc)
|
||||
- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt)
|
||||
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
|
||||
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
|
||||
- Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits.
|
||||
- Formatting/linting via Biome; run `pnpm lint` before commits.
|
||||
- Add brief code comments for tricky or non-obvious logic.
|
||||
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
|
||||
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
|
||||
@@ -76,7 +76,6 @@
|
||||
- Vocabulary: "makeup" = "mac app".
|
||||
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
|
||||
- Never update the Carbon dependency.
|
||||
- Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`).
|
||||
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars.
|
||||
- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes.
|
||||
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Clawdbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
|
||||
|
||||
+26
-88
@@ -1,105 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.15
|
||||
|
||||
### Changes
|
||||
- Security: add `clawdbot security audit` (`--deep`) and surface it in `status --all` and `doctor`.
|
||||
- Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require `--accept-risk` for `--non-interactive`.
|
||||
- Docs: expand gateway security hardening guidance and incident response checklist.
|
||||
|
||||
### Fixes
|
||||
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
|
||||
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.
|
||||
|
||||
## 2026.1.14
|
||||
|
||||
### Changes
|
||||
- Usage: add MiniMax coding plan usage tracking.
|
||||
- Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR.
|
||||
- Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915)
|
||||
- Telegram: add message delete action in the message tool. (#903) — thanks @sleontenko.
|
||||
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
|
||||
|
||||
### Fixes
|
||||
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
|
||||
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
|
||||
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
|
||||
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
|
||||
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.
|
||||
- Telegram: split long captions into media + follow-up text messages. (#907) - thanks @jalehman.
|
||||
- Telegram: migrate group config when supergroups change chat IDs. (#906) — thanks @sleontenko.
|
||||
- Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose.
|
||||
- Slack: drop Socket Mode events with mismatched `api_app_id`/`team_id`. (#889) — thanks @roshanasingh4.
|
||||
- Discord: isolate autoThread thread context. (#856) — thanks @davidguttman.
|
||||
- WhatsApp: fix context isolation using wrong ID (was bot's number, now conversation ID). (#911) — thanks @tristanmanchester.
|
||||
- WhatsApp: normalize user JIDs with device suffix for allowlist checks in groups. (#838) — thanks @peschee.
|
||||
|
||||
## 2026.1.13
|
||||
|
||||
### Fixes
|
||||
- Postinstall: treat already-applied pnpm patches as no-ops to avoid npm/bun install failures.
|
||||
- Packaging: pin `@mariozechner/pi-ai` to 0.45.7 and refresh patched dependency to match npm resolution.
|
||||
|
||||
## 2026.1.12-2
|
||||
|
||||
### Fixes
|
||||
- Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`).
|
||||
- Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4.
|
||||
|
||||
## 2026.1.12-1
|
||||
|
||||
### Fixes
|
||||
- Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`).
|
||||
|
||||
## 2026.1.12
|
||||
## 2025.1.12 (Unreleased)
|
||||
|
||||
### Highlights
|
||||
- **BREAKING:** rename chat “providers” (Slack/Telegram/WhatsApp/…) to **channels** across CLI/RPC/config; legacy config keys auto-migrate on load (and are written back as `channels.*`).
|
||||
- Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback.
|
||||
- Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI).
|
||||
- Models: add Synthetic provider plus Moonshot Kimi K2 0905 + turbo/thinking variants (with docs). (#811) — thanks @siraht; (#818) — thanks @mickahouan.
|
||||
- Models: add Synthetic provider plus Moonshot Kimi K2 0905 + turbo/thinking variants (with docs).
|
||||
- Cron: one-shot schedules accept ISO timestamps (UTC) with optional delete-after-run; cron jobs can target a specific agent (CLI + macOS/Control UI).
|
||||
- Agents: add compaction mode config with optional safeguard summarization and per-agent model fallbacks. (#700) — thanks @thewilloftheshadow; (#583) — thanks @mitschabaude-bot.
|
||||
- Agents: add compaction mode config with optional safeguard summarization and per-agent model fallbacks.
|
||||
|
||||
### New & Improved
|
||||
- Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI. (#819) — thanks @mukhtharcm.
|
||||
- Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI.
|
||||
- Memory: new `clawdbot memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default.
|
||||
- Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config.
|
||||
- Tools/Sandbox: add tool profiles + group shorthands; support tool-policy groups in `tools.sandbox.tools`; drop legacy `memory` shorthand; allow Docker bind mounts via `docker.binds`. (#790) — thanks @akonyer.
|
||||
- Tools: add browser `scrollintoview` action; allow Claude/Gemini tool param aliases; allow thinking `xhigh` for GPT-5.2/Codex with safe downgrades. (#793) — thanks @hsrvc; (#444) — thanks @grp06.
|
||||
- Gateway/CLI: add Tailscale binary discovery, custom bind mode, and probe auth retry; add `clawdbot dashboard` auto-open flow; default native slash commands to `"auto"` with per-provider overrides. (#740) — thanks @jeffersonwarrior.
|
||||
- Auth/Onboarding: add Chutes OAuth (PKCE + refresh + onboarding choice); normalize API key inputs; default TUI onboarding to `deliver: false`. (#726) — thanks @FrieSei; (#791) — thanks @roshanasingh4.
|
||||
- Providers: add `discord.allowBots`; trim legacy MiniMax M2 from default catalogs; route MiniMax vision to the Coding Plan VLM endpoint (also accepts `@/path/to/file.png` inputs). (#802) — thanks @zknicker.
|
||||
- Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer. (#823) — thanks @roshanasingh4; (#786) — thanks @meaningfool.
|
||||
- Tools/Sandbox: add tool profiles + group shorthands; support tool-policy groups in `tools.sandbox.tools`; drop legacy `memory` shorthand; allow Docker bind mounts via `docker.binds`.
|
||||
- Tools: add browser `scrollintoview` action; allow Claude/Gemini tool param aliases; allow thinking `xhigh` for GPT-5.2/Codex with safe downgrades.
|
||||
- Gateway/CLI: add Tailscale binary discovery, custom bind mode, and probe auth retry; add `clawdbot dashboard` auto-open flow; default native slash commands to `"auto"` with per-provider overrides.
|
||||
- Auth/Onboarding: add Chutes OAuth (PKCE + refresh + onboarding choice); normalize API key inputs; default TUI onboarding to `deliver: false`.
|
||||
- Providers: add `discord.allowBots`; trim legacy MiniMax M2 from default catalogs; route MiniMax vision to the Coding Plan VLM endpoint (also accepts `@/path/to/file.png` inputs).
|
||||
- Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer.
|
||||
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
||||
|
||||
### Installer
|
||||
- Install: run `clawdbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||
|
||||
### Fixes
|
||||
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
|
||||
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. (#822) — thanks @sebslight; (#705) — thanks @TAGOOZ.
|
||||
- Auth: drop invalid auth profiles from ordering so environment keys can still be used for providers like MiniMax.
|
||||
- Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors. (#795) — thanks @thewilloftheshadow; (#783) — thanks @ananth-vardhan-cn; (#793) — thanks @hsrvc; (#805) — thanks @marcmarg.
|
||||
- Agents: auto-recover from compaction context overflow by resetting the session and retrying; propagate overflow details from embedded runs so callers can recover.
|
||||
- MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing. (#809) — thanks @latitudeki5223.
|
||||
- Onboarding/Auth: honor `CLAWDBOT_AGENT_DIR` / `PI_CODING_AGENT_DIR` when writing auth profiles (MiniMax). (#829) — thanks @roshanasingh4.
|
||||
- Anthropic: handle `overloaded_error` with a friendly message and failover classification. (#832) — thanks @danielz1z.
|
||||
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid incorrect role errors. (#804) — thanks @ThomsenDrake.
|
||||
- Messaging: enforce context isolation for message tool sends; keep typing indicators alive during tool execution. (#793) — thanks @hsrvc; (#450, #447) — thanks @thewilloftheshadow.
|
||||
- Auto-reply: `/status` allowlist behavior, reasoning-tag enforcement on fallback, and system-event enqueueing for elevated/reasoning toggles. (#810) — thanks @mcinteerj.
|
||||
- System events: include local timestamps when events are injected into prompts. (#245) — thanks @thewilloftheshadow.
|
||||
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles.
|
||||
- Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors.
|
||||
- MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing.
|
||||
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid incorrect role errors.
|
||||
- Messaging: enforce context isolation for message tool sends; keep typing indicators alive during tool execution.
|
||||
- Auto-reply: `/status` allowlist behavior, reasoning-tag enforcement on fallback, and system-event enqueueing for elevated/reasoning toggles.
|
||||
- Auto-reply: explain how to enable `/bash` when it’s disabled; add security notes + FAQ. (#722) — thanks @vrknetha.
|
||||
- Auto-reply: resolve ambiguous `/model` matches; fix streaming block reply media handling; keep >300 char heartbeat replies instead of dropping.
|
||||
- Discord/Slack: centralize reply-thread planning; fix autoThread routing + add per-channel autoThread; avoid duplicate listeners; keep reasoning italics intact; allow clearing channel parents via message tool. (#800, #807) — thanks @davidguttman; (#744) — thanks @thewilloftheshadow.
|
||||
- Telegram: preserve forum topic thread ids, persist polling offsets, respect account bindings in webhook mode, and show typing indicator in General topics. (#727, #739) — thanks @thewilloftheshadow; (#821) — thanks @gumadeiras; (#779) — thanks @azade-c.
|
||||
- Slack: accept slash commands with or without leading `/` for custom command configs. (#798) — thanks @thewilloftheshadow.
|
||||
- Cron: persist disabled jobs correctly; accept `jobId` aliases for update/run/remove params. (#205, #252) — thanks @thewilloftheshadow.
|
||||
- Gateway/CLI: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides; `agents.list` respects explicit config; reduce noisy loopback WS logs during tests; run `clawdbot doctor --non-interactive` during updates. (#781) — thanks @ronyrus.
|
||||
- Onboarding/Control UI: refuse invalid configs (run doctor first); quote Windows browser URLs for OAuth; keep chat scroll position unless the user is near the bottom. (#764) — thanks @mukhtharcm; (#794) — thanks @roshanasingh4; (#217) — thanks @thewilloftheshadow.
|
||||
- Tools/UI: harden tool input schemas for strict providers; drop null-only union variants for Gemini schema cleanup; treat `maxChars: 0` as unlimited; keep TUI last streamed response instead of "(no output)". (#782) — thanks @AbhisekBasu1; (#796) — thanks @gabriel-trigo; (#747) — thanks @thewilloftheshadow.
|
||||
- Connections UI: polish multi-account account cards. (#816) — thanks @steipete.
|
||||
- Discord/Slack: centralize reply-thread planning; fix autoThread routing + add per-channel autoThread; avoid duplicate listeners; keep reasoning italics intact; allow clearing channel parents via message tool.
|
||||
- Telegram: preserve forum topic thread ids, persist polling offsets, respect account bindings in webhook mode, and show typing indicator in General topics.
|
||||
- Slack: accept slash commands with or without leading `/` for custom command configs.
|
||||
- Cron: persist disabled jobs correctly; accept `jobId` aliases for update/run/remove params.
|
||||
- Gateway/CLI: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides; `agents.list` respects explicit config; reduce noisy loopback WS logs during tests; run `clawdbot doctor --non-interactive` during updates.
|
||||
- Onboarding/Control UI: refuse invalid configs (run doctor first); quote Windows browser URLs for OAuth; keep chat scroll position unless the user is near the bottom.
|
||||
- Tools/UI: harden tool input schemas for strict providers; drop null-only union variants for Gemini schema cleanup; treat `maxChars: 0` as unlimited; keep TUI last streamed response instead of "(no output)".
|
||||
- Connections UI: polish multi-account account cards.
|
||||
|
||||
### Maintenance
|
||||
- Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai.
|
||||
@@ -345,7 +283,7 @@
|
||||
- Dependencies: Pi 0.40.0 bump (#543) — thanks @mcinteerj.
|
||||
- Build: Docker build cache layer (#605) — thanks @zknicker.
|
||||
|
||||
- Auth: enable OAuth token refresh for Claude Code CLI credentials (`anthropic:claude-cli`) with bidirectional sync back to Claude Code storage (file on Linux/Windows, Keychain on macOS). This allows long-running agents to operate autonomously without manual re-authentication (#654 — thanks @radek-paclt).
|
||||
- Auth: enable OAuth token refresh for Claude CLI credentials (`anthropic:claude-cli`) with bidirectional sync back to Claude Code storage (file on Linux/Windows, Keychain on macOS). This allows long-running agents to operate autonomously without manual re-authentication (#654 — thanks @radek-paclt).
|
||||
|
||||
## 2026.1.8
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Welcome to the lobster tank! 🦞
|
||||
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
|
||||
|
||||
- **Shadow** - Discord + Slack subsystem
|
||||
- GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shad0wed](https://x.com/4shad0wed)
|
||||
- GitHub: [@4shadowed](https://github.com/4shadowed) · X: [@4shad0wed](https://x.com/4shad0wed)
|
||||
|
||||
- **Jos** - Telegram, API, Nix mode
|
||||
- GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes)
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
</p>
|
||||
|
||||
**Clawdbot** is a *personal AI assistant* you run on your own devices.
|
||||
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat), can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
|
||||
It answers you on the providers you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat), can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
|
||||
|
||||
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
|
||||
|
||||
[Website](https://clawdbot.com) · [Docs](https://docs.clawd.bot) · [Getting Started](https://docs.clawd.bot/start/getting-started) · [Updating](https://docs.clawd.bot/install/updating) · [Showcase](https://docs.clawd.bot/start/showcase) · [FAQ](https://docs.clawd.bot/start/faq) · [Wizard](https://docs.clawd.bot/start/wizard) · [Nix](https://github.com/clawdbot/nix-clawdbot) · [Docker](https://docs.clawd.bot/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
|
||||
Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
Works with npm, pnpm, or bun.
|
||||
New install? Start here: [Getting started](https://docs.clawd.bot/start/getting-started)
|
||||
|
||||
@@ -54,7 +54,7 @@ The wizard installs the Gateway daemon (launchd/systemd user service) so it stay
|
||||
|
||||
Runtime: **Node ≥22**.
|
||||
|
||||
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.clawd.bot/start/getting-started)
|
||||
Full beginner guide (auth, pairing, providers): [Getting started](https://docs.clawd.bot/start/getting-started)
|
||||
|
||||
```bash
|
||||
clawdbot onboard --install-daemon
|
||||
@@ -97,17 +97,17 @@ Clawdbot connects to real messaging surfaces. Treat inbound DMs as **untrusted i
|
||||
Full security guide: [Security](https://docs.clawd.bot/gateway/security)
|
||||
|
||||
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Slack:
|
||||
- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
|
||||
- Approve with: `clawdbot pairing approve <channel> <code>` (then the sender is added to a local allowlist store).
|
||||
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`).
|
||||
- **DM pairing** (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
|
||||
- Approve with: `clawdbot pairing approve <provider> <code>` (then the sender is added to a local allowlist store).
|
||||
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the provider allowlist (`allowFrom` / `discord.dm.allowFrom` / `slack.dm.allowFrom`).
|
||||
|
||||
Run `clawdbot doctor` to surface risky/misconfigured DM policies.
|
||||
|
||||
## Highlights
|
||||
|
||||
- **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, channels, tools, and events.
|
||||
- **[Multi-channel inbox](https://docs.clawd.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat, macOS, iOS/Android.
|
||||
- **[Multi-agent routing](https://docs.clawd.bot/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
|
||||
- **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, providers, tools, and events.
|
||||
- **[Multi-provider inbox](https://docs.clawd.bot/providers)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat, macOS, iOS/Android.
|
||||
- **[Multi-agent routing](https://docs.clawd.bot/gateway/configuration)** — route inbound providers/accounts/peers to isolated agents (workspaces + per-agent sessions).
|
||||
- **[Voice Wake](https://docs.clawd.bot/nodes/voicewake) + [Talk Mode](https://docs.clawd.bot/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
|
||||
- **[Live Canvas](https://docs.clawd.bot/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui).
|
||||
- **[First-class tools](https://docs.clawd.bot/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
|
||||
@@ -127,9 +127,9 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
|
||||
- [Session model](https://docs.clawd.bot/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.clawd.bot/concepts/groups).
|
||||
- [Media pipeline](https://docs.clawd.bot/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawd.bot/nodes/audio).
|
||||
|
||||
### Channels
|
||||
- [Channels](https://docs.clawd.bot/channels): [WhatsApp](https://docs.clawd.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/channels/telegram) (grammY), [Slack](https://docs.clawd.bot/channels/slack) (Bolt), [Discord](https://docs.clawd.bot/channels/discord) (discord.js), [Signal](https://docs.clawd.bot/channels/signal) (signal-cli), [iMessage](https://docs.clawd.bot/channels/imessage) (imsg), [Microsoft Teams](https://docs.clawd.bot/channels/msteams) (Bot Framework), [WebChat](https://docs.clawd.bot/web/webchat).
|
||||
- [Group routing](https://docs.clawd.bot/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.clawd.bot/channels).
|
||||
### Providers
|
||||
- [Providers](https://docs.clawd.bot/providers): [WhatsApp](https://docs.clawd.bot/providers/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/providers/telegram) (grammY), [Slack](https://docs.clawd.bot/providers/slack) (Bolt), [Discord](https://docs.clawd.bot/providers/discord) (discord.js), [Signal](https://docs.clawd.bot/providers/signal) (signal-cli), [iMessage](https://docs.clawd.bot/providers/imessage) (imsg), [Microsoft Teams](https://docs.clawd.bot/providers/msteams) (Bot Framework), [WebChat](https://docs.clawd.bot/web/webchat).
|
||||
- [Group routing](https://docs.clawd.bot/concepts/group-messages): mention gating, reply tags, per-provider chunking and routing. Provider rules: [Providers](https://docs.clawd.bot/providers).
|
||||
|
||||
### Apps + nodes
|
||||
- [macOS app](https://docs.clawd.bot/platforms/macos): menu bar control plane, [Voice Wake](https://docs.clawd.bot/nodes/voicewake)/PTT, [Talk Mode](https://docs.clawd.bot/nodes/talk) overlay, [WebChat](https://docs.clawd.bot/web/webchat), debug tools, [remote gateway](https://docs.clawd.bot/gateway/remote) control.
|
||||
@@ -145,10 +145,10 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
|
||||
- [Skills platform](https://docs.clawd.bot/tools/skills): bundled, managed, and workspace skills with install gating + UI.
|
||||
|
||||
### Runtime + safety
|
||||
- [Channel routing](https://docs.clawd.bot/concepts/channel-routing), [retry policy](https://docs.clawd.bot/concepts/retry), and [streaming/chunking](https://docs.clawd.bot/concepts/streaming).
|
||||
- [Provider routing](https://docs.clawd.bot/concepts/provider-routing), [retry policy](https://docs.clawd.bot/concepts/retry), and [streaming/chunking](https://docs.clawd.bot/concepts/streaming).
|
||||
- [Presence](https://docs.clawd.bot/concepts/presence), [typing indicators](https://docs.clawd.bot/concepts/typing-indicators), and [usage tracking](https://docs.clawd.bot/concepts/usage-tracking).
|
||||
- [Models](https://docs.clawd.bot/concepts/models), [model failover](https://docs.clawd.bot/concepts/model-failover), and [session pruning](https://docs.clawd.bot/concepts/session-pruning).
|
||||
- [Security](https://docs.clawd.bot/gateway/security) and [troubleshooting](https://docs.clawd.bot/channels/troubleshooting).
|
||||
- [Security](https://docs.clawd.bot/gateway/security) and [troubleshooting](https://docs.clawd.bot/providers/troubleshooting).
|
||||
|
||||
### Ops + packaging
|
||||
- [Control UI](https://docs.clawd.bot/web) + [WebChat](https://docs.clawd.bot/web/webchat) served directly from the Gateway.
|
||||
@@ -204,7 +204,7 @@ Details: [Tailscale guide](https://docs.clawd.bot/gateway/tailscale) · [Web sur
|
||||
|
||||
It’s perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute device‑local actions when needed.
|
||||
|
||||
- **Gateway host** runs the exec tool and channel connections by default.
|
||||
- **Gateway host** runs the exec tool and provider connections by default.
|
||||
- **Device nodes** run device‑local actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
|
||||
In short: exec runs where the Gateway lives; device actions run where the device lives.
|
||||
|
||||
@@ -315,56 +315,52 @@ Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
|
||||
|
||||
Details: [Security guide](https://docs.clawd.bot/gateway/security) · [Docker + sandboxing](https://docs.clawd.bot/install/docker) · [Sandbox config](https://docs.clawd.bot/gateway/configuration)
|
||||
|
||||
### [WhatsApp](https://docs.clawd.bot/channels/whatsapp)
|
||||
### [WhatsApp](https://docs.clawd.bot/providers/whatsapp)
|
||||
|
||||
- Link the device: `pnpm clawdbot channels login` (stores creds in `~/.clawdbot/credentials`).
|
||||
- Allowlist who can talk to the assistant via `channels.whatsapp.allowFrom`.
|
||||
- If `channels.whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
|
||||
- Link the device: `pnpm clawdbot providers login` (stores creds in `~/.clawdbot/credentials`).
|
||||
- Allowlist who can talk to the assistant via `whatsapp.allowFrom`.
|
||||
- If `whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
|
||||
|
||||
### [Telegram](https://docs.clawd.bot/channels/telegram)
|
||||
### [Telegram](https://docs.clawd.bot/providers/telegram)
|
||||
|
||||
- Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` (env wins).
|
||||
- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` as needed.
|
||||
- Set `TELEGRAM_BOT_TOKEN` or `telegram.botToken` (env wins).
|
||||
- Optional: set `telegram.groups` (with `telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `telegram.allowFrom` or `telegram.webhookUrl` as needed.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123456:ABCDEF"
|
||||
}
|
||||
telegram: {
|
||||
botToken: "123456:ABCDEF"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### [Slack](https://docs.clawd.bot/channels/slack)
|
||||
### [Slack](https://docs.clawd.bot/providers/slack)
|
||||
|
||||
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `channels.slack.botToken` + `channels.slack.appToken`).
|
||||
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `slack.botToken` + `slack.appToken`).
|
||||
|
||||
### [Discord](https://docs.clawd.bot/channels/discord)
|
||||
### [Discord](https://docs.clawd.bot/providers/discord)
|
||||
|
||||
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins).
|
||||
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.dm.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
|
||||
- Set `DISCORD_BOT_TOKEN` or `discord.token` (env wins).
|
||||
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
token: "1234abcd"
|
||||
}
|
||||
discord: {
|
||||
token: "1234abcd"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### [Signal](https://docs.clawd.bot/channels/signal)
|
||||
### [Signal](https://docs.clawd.bot/providers/signal)
|
||||
|
||||
- Requires `signal-cli` and a `channels.signal` config section.
|
||||
- Requires `signal-cli` and a `signal` config section.
|
||||
|
||||
### [iMessage](https://docs.clawd.bot/channels/imessage)
|
||||
### [iMessage](https://docs.clawd.bot/providers/imessage)
|
||||
|
||||
- macOS only; Messages must be signed in.
|
||||
- If `channels.imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
|
||||
- If `imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
|
||||
|
||||
### [Microsoft Teams](https://docs.clawd.bot/channels/msteams)
|
||||
### [Microsoft Teams](https://docs.clawd.bot/providers/msteams)
|
||||
|
||||
- Configure a Teams app + Bot Framework, then add a `msteams` config section.
|
||||
- Allowlist who can talk via `msteams.allowFrom`; group access via `msteams.groupAllowFrom` or `msteams.groupPolicy: "open"`.
|
||||
@@ -399,7 +395,7 @@ Use these when you’re past the onboarding flow and want the deeper reference.
|
||||
- [Set up Gmail Pub/Sub triggers.](https://docs.clawd.bot/automation/gmail-pubsub)
|
||||
- [Learn the macOS menu bar companion details.](https://docs.clawd.bot/platforms/mac/menu-bar)
|
||||
- [Platform guides: Windows (WSL2)](https://docs.clawd.bot/platforms/windows), [Linux](https://docs.clawd.bot/platforms/linux), [macOS](https://docs.clawd.bot/platforms/macos), [iOS](https://docs.clawd.bot/platforms/ios), [Android](https://docs.clawd.bot/platforms/android)
|
||||
- [Debug common failures with the troubleshooting guide.](https://docs.clawd.bot/channels/troubleshooting)
|
||||
- [Debug common failures with the troubleshooting guide.](https://docs.clawd.bot/providers/troubleshooting)
|
||||
- [Review security guidance before exposing anything.](https://docs.clawd.bot/gateway/security)
|
||||
|
||||
## Advanced docs (discovery + control)
|
||||
@@ -472,21 +468,20 @@ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="Sebastian Barrios" title="Sebastian Barrios"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></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/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/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/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=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/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/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/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></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=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/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/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/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=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/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/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/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/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/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/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/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/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/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/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/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a>
|
||||
<a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></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=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></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/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/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/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/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/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/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/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/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/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/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/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="Sebastian Barrios" title="Sebastian Barrios"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></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/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/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=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></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/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/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=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/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/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/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/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/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/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/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/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/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/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/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/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=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||
</p>
|
||||
+16
-16
@@ -2,22 +2,6 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>Clawdbot</title>
|
||||
<item>
|
||||
<title>2026.1.12-2</title>
|
||||
<pubDate>Tue, 13 Jan 2026 10:05:25 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||
<sparkle:version>5534</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.12-2</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>Clawdbot 2026.1.12-2</h2>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Packaging: include <code>dist/memory/**</code> in the npm tarball (fixes <code>ERR_MODULE_NOT_FOUND</code> for <code>dist/memory/index.js</code>).</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.12-2/Clawdbot-2026.1.12-2.zip" length="19854203" type="application/octet-stream" sparkle:edSignature="CVpUofNS+pl6Smk/K0Q8q35saRuuFx90s4sePABORFvGcAF1biajC8zpiImKuXpqD0ENb+VTwDJ1ul1Oxh3wDA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.11-3</title>
|
||||
<pubDate>Mon, 12 Jan 2026 10:40:23 +0000</pubDate>
|
||||
@@ -52,5 +36,21 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.11-2/Clawdbot-2026.1.11-2.zip" length="19860732" type="application/octet-stream" sparkle:edSignature="0UG+d9v3Qf5F9vs/KozUB404WpHjFBQRVoRuhwtzF8kpU7jJmmGlQzh1c61E+LMN4fHcljpxIwHHrvvIfRyrCw=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.11-1</title>
|
||||
<pubDate>Mon, 12 Jan 2026 09:53:46 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||
<sparkle:version>5207</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.11-1</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>Clawdbot 2026.1.11-1</h2>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Installer: include <code>patches/</code> in the npm package so postinstall patching works for npm/bun installs.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.11-1/Clawdbot-2026.1.11-1.zip" length="19860761" type="application/octet-stream" sparkle:edSignature="CXKzzha/s6cGBeF0TMz+cV8/pfqoAL9ZyNVacYRLnnHEwA1cMbOWRftpGRhYe4HknVQYYBgNQqZK2lBxpOZgBg=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -187,7 +187,7 @@ actor BridgeServer {
|
||||
thinking: "low",
|
||||
deliver: false,
|
||||
to: nil,
|
||||
channel: .last))
|
||||
provider: .last))
|
||||
|
||||
case "agent.request":
|
||||
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else {
|
||||
@@ -205,7 +205,7 @@ actor BridgeServer {
|
||||
?? "node-\(nodeId)"
|
||||
let thinking = link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
||||
let to = link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
||||
let channel = GatewayAgentChannel(raw: link.channel)
|
||||
let provider = GatewayAgentProvider(raw: link.channel)
|
||||
|
||||
_ = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
|
||||
message: message,
|
||||
@@ -213,7 +213,7 @@ actor BridgeServer {
|
||||
thinking: thinking,
|
||||
deliver: link.deliver,
|
||||
to: to,
|
||||
channel: channel))
|
||||
provider: provider))
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -86,7 +86,7 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
thinking: "low",
|
||||
deliver: false,
|
||||
to: nil,
|
||||
channel: .last,
|
||||
provider: .last,
|
||||
idempotencyKey: actionId))
|
||||
|
||||
await MainActor.run {
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func channelHeaderActions(_ channel: ConnectionChannel) -> some View {
|
||||
func providerHeaderActions(_ provider: ConnectionProvider) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
if channel == .whatsapp {
|
||||
if provider == .whatsapp {
|
||||
Button("Logout") {
|
||||
Task { await self.store.logoutWhatsApp() }
|
||||
}
|
||||
@@ -21,7 +21,7 @@ extension ConnectionsSettings {
|
||||
.disabled(self.store.whatsappBusy)
|
||||
}
|
||||
|
||||
if channel == .telegram {
|
||||
if provider == .telegram {
|
||||
Button("Logout") {
|
||||
Task { await self.store.logoutTelegram() }
|
||||
}
|
||||
+62
-62
@@ -1,15 +1,15 @@
|
||||
import SwiftUI
|
||||
|
||||
extension ConnectionsSettings {
|
||||
private func channelStatus<T: Decodable>(
|
||||
private func providerStatus<T: Decodable>(
|
||||
_ id: String,
|
||||
as type: T.Type) -> T?
|
||||
{
|
||||
self.store.snapshot?.decodeChannel(id, as: type)
|
||||
self.store.snapshot?.decodeProvider(id, as: type)
|
||||
}
|
||||
|
||||
var whatsAppTint: Color {
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return .secondary }
|
||||
if !status.configured { return .secondary }
|
||||
if !status.linked { return .red }
|
||||
@@ -20,7 +20,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var telegramTint: Color {
|
||||
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
||||
else { return .secondary }
|
||||
if !status.configured { return .secondary }
|
||||
if status.lastError != nil { return .orange }
|
||||
@@ -30,7 +30,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var discordTint: Color {
|
||||
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
||||
else { return .secondary }
|
||||
if !status.configured { return .secondary }
|
||||
if status.lastError != nil { return .orange }
|
||||
@@ -40,7 +40,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var signalTint: Color {
|
||||
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
||||
else { return .secondary }
|
||||
if !status.configured { return .secondary }
|
||||
if status.lastError != nil { return .orange }
|
||||
@@ -50,7 +50,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var imessageTint: Color {
|
||||
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
||||
else { return .secondary }
|
||||
if !status.configured { return .secondary }
|
||||
if status.lastError != nil { return .orange }
|
||||
@@ -60,7 +60,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var whatsAppSummary: String {
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return "Checking…" }
|
||||
if !status.linked { return "Not linked" }
|
||||
if status.connected { return "Connected" }
|
||||
@@ -69,7 +69,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var telegramSummary: String {
|
||||
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
||||
else { return "Checking…" }
|
||||
if !status.configured { return "Not configured" }
|
||||
if status.running { return "Running" }
|
||||
@@ -77,7 +77,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var discordSummary: String {
|
||||
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
||||
else { return "Checking…" }
|
||||
if !status.configured { return "Not configured" }
|
||||
if status.running { return "Running" }
|
||||
@@ -85,7 +85,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var signalSummary: String {
|
||||
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
||||
else { return "Checking…" }
|
||||
if !status.configured { return "Not configured" }
|
||||
if status.running { return "Running" }
|
||||
@@ -93,7 +93,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var imessageSummary: String {
|
||||
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
||||
else { return "Checking…" }
|
||||
if !status.configured { return "Not configured" }
|
||||
if status.running { return "Running" }
|
||||
@@ -101,7 +101,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var whatsAppDetails: String? {
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return nil }
|
||||
var lines: [String] = []
|
||||
if let e164 = status.`self`?.e164 ?? status.`self`?.jid {
|
||||
@@ -132,7 +132,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var telegramDetails: String? {
|
||||
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
||||
else { return nil }
|
||||
var lines: [String] = []
|
||||
if let source = status.tokenSource {
|
||||
@@ -164,7 +164,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var discordDetails: String? {
|
||||
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
||||
else { return nil }
|
||||
var lines: [String] = []
|
||||
if let source = status.tokenSource {
|
||||
@@ -193,7 +193,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var signalDetails: String? {
|
||||
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
||||
else { return nil }
|
||||
var lines: [String] = []
|
||||
lines.append("Base URL: \(status.baseUrl)")
|
||||
@@ -220,7 +220,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var imessageDetails: String? {
|
||||
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
||||
else { return nil }
|
||||
var lines: [String] = []
|
||||
if let cliPath = status.cliPath, !cliPath.isEmpty {
|
||||
@@ -243,68 +243,68 @@ extension ConnectionsSettings {
|
||||
}
|
||||
|
||||
var isTelegramTokenLocked: Bool {
|
||||
self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)?.tokenSource == "env"
|
||||
self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)?.tokenSource == "env"
|
||||
}
|
||||
|
||||
var isDiscordTokenLocked: Bool {
|
||||
self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)?.tokenSource == "env"
|
||||
self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)?.tokenSource == "env"
|
||||
}
|
||||
|
||||
var orderedChannels: [ConnectionChannel] {
|
||||
ConnectionChannel.allCases.sorted { lhs, rhs in
|
||||
let lhsEnabled = self.channelEnabled(lhs)
|
||||
let rhsEnabled = self.channelEnabled(rhs)
|
||||
var orderedProviders: [ConnectionProvider] {
|
||||
ConnectionProvider.allCases.sorted { lhs, rhs in
|
||||
let lhsEnabled = self.providerEnabled(lhs)
|
||||
let rhsEnabled = self.providerEnabled(rhs)
|
||||
if lhsEnabled != rhsEnabled { return lhsEnabled && !rhsEnabled }
|
||||
return lhs.sortOrder < rhs.sortOrder
|
||||
}
|
||||
}
|
||||
|
||||
var enabledChannels: [ConnectionChannel] {
|
||||
self.orderedChannels.filter { self.channelEnabled($0) }
|
||||
var enabledProviders: [ConnectionProvider] {
|
||||
self.orderedProviders.filter { self.providerEnabled($0) }
|
||||
}
|
||||
|
||||
var availableChannels: [ConnectionChannel] {
|
||||
self.orderedChannels.filter { !self.channelEnabled($0) }
|
||||
var availableProviders: [ConnectionProvider] {
|
||||
self.orderedProviders.filter { !self.providerEnabled($0) }
|
||||
}
|
||||
|
||||
func ensureSelection() {
|
||||
guard let selected = self.selectedChannel else {
|
||||
self.selectedChannel = self.orderedChannels.first
|
||||
guard let selected = self.selectedProvider else {
|
||||
self.selectedProvider = self.orderedProviders.first
|
||||
return
|
||||
}
|
||||
if !self.orderedChannels.contains(selected) {
|
||||
self.selectedChannel = self.orderedChannels.first
|
||||
if !self.orderedProviders.contains(selected) {
|
||||
self.selectedProvider = self.orderedProviders.first
|
||||
}
|
||||
}
|
||||
|
||||
func channelEnabled(_ channel: ConnectionChannel) -> Bool {
|
||||
switch channel {
|
||||
func providerEnabled(_ provider: ConnectionProvider) -> Bool {
|
||||
switch provider {
|
||||
case .whatsapp:
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return false }
|
||||
return status.configured || status.linked || status.running
|
||||
case .telegram:
|
||||
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
||||
else { return false }
|
||||
return status.configured || status.running
|
||||
case .discord:
|
||||
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
||||
else { return false }
|
||||
return status.configured || status.running
|
||||
case .signal:
|
||||
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
||||
else { return false }
|
||||
return status.configured || status.running
|
||||
case .imessage:
|
||||
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
||||
else { return false }
|
||||
return status.configured || status.running
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func channelSection(_ channel: ConnectionChannel) -> some View {
|
||||
switch channel {
|
||||
func providerSection(_ provider: ConnectionProvider) -> some View {
|
||||
switch provider {
|
||||
case .whatsapp:
|
||||
self.whatsAppSection
|
||||
case .telegram:
|
||||
@@ -318,8 +318,8 @@ extension ConnectionsSettings {
|
||||
}
|
||||
}
|
||||
|
||||
func channelTint(_ channel: ConnectionChannel) -> Color {
|
||||
switch channel {
|
||||
func providerTint(_ provider: ConnectionProvider) -> Color {
|
||||
switch provider {
|
||||
case .whatsapp:
|
||||
self.whatsAppTint
|
||||
case .telegram:
|
||||
@@ -333,8 +333,8 @@ extension ConnectionsSettings {
|
||||
}
|
||||
}
|
||||
|
||||
func channelSummary(_ channel: ConnectionChannel) -> String {
|
||||
switch channel {
|
||||
func providerSummary(_ provider: ConnectionProvider) -> String {
|
||||
switch provider {
|
||||
case .whatsapp:
|
||||
self.whatsAppSummary
|
||||
case .telegram:
|
||||
@@ -348,8 +348,8 @@ extension ConnectionsSettings {
|
||||
}
|
||||
}
|
||||
|
||||
func channelDetails(_ channel: ConnectionChannel) -> String? {
|
||||
switch channel {
|
||||
func providerDetails(_ provider: ConnectionProvider) -> String? {
|
||||
switch provider {
|
||||
case .whatsapp:
|
||||
self.whatsAppDetails
|
||||
case .telegram:
|
||||
@@ -363,55 +363,55 @@ extension ConnectionsSettings {
|
||||
}
|
||||
}
|
||||
|
||||
func channelLastCheckText(_ channel: ConnectionChannel) -> String {
|
||||
guard let date = self.channelLastCheck(channel) else { return "never" }
|
||||
func providerLastCheckText(_ provider: ConnectionProvider) -> String {
|
||||
guard let date = self.providerLastCheck(provider) else { return "never" }
|
||||
return relativeAge(from: date)
|
||||
}
|
||||
|
||||
func channelLastCheck(_ channel: ConnectionChannel) -> Date? {
|
||||
switch channel {
|
||||
func providerLastCheck(_ provider: ConnectionProvider) -> Date? {
|
||||
switch provider {
|
||||
case .whatsapp:
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return nil }
|
||||
return self.date(fromMs: status.lastEventAt ?? status.lastMessageAt ?? status.lastConnectedAt)
|
||||
case .telegram:
|
||||
return self
|
||||
.date(fromMs: self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)?
|
||||
.date(fromMs: self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)?
|
||||
.lastProbeAt)
|
||||
case .discord:
|
||||
return self
|
||||
.date(fromMs: self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)?
|
||||
.date(fromMs: self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)?
|
||||
.lastProbeAt)
|
||||
case .signal:
|
||||
return self
|
||||
.date(fromMs: self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)?.lastProbeAt)
|
||||
.date(fromMs: self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)?.lastProbeAt)
|
||||
case .imessage:
|
||||
return self
|
||||
.date(fromMs: self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)?
|
||||
.date(fromMs: self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)?
|
||||
.lastProbeAt)
|
||||
}
|
||||
}
|
||||
|
||||
func channelHasError(_ channel: ConnectionChannel) -> Bool {
|
||||
switch channel {
|
||||
func providerHasError(_ provider: ConnectionProvider) -> Bool {
|
||||
switch provider {
|
||||
case .whatsapp:
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return false }
|
||||
return status.lastError?.isEmpty == false || status.lastDisconnect?.loggedOut == true
|
||||
case .telegram:
|
||||
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
||||
else { return false }
|
||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||
case .discord:
|
||||
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
||||
else { return false }
|
||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||
case .signal:
|
||||
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
||||
else { return false }
|
||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||
case .imessage:
|
||||
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
||||
else { return false }
|
||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||
}
|
||||
@@ -11,7 +11,7 @@ extension ConnectionsSettings {
|
||||
self.store.start()
|
||||
self.ensureSelection()
|
||||
}
|
||||
.onChange(of: self.orderedChannels) { _, _ in
|
||||
.onChange(of: self.orderedProviders) { _, _ in
|
||||
self.ensureSelection()
|
||||
}
|
||||
.onDisappear { self.store.stop() }
|
||||
@@ -20,17 +20,17 @@ extension ConnectionsSettings {
|
||||
private var sidebar: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
if !self.enabledChannels.isEmpty {
|
||||
if !self.enabledProviders.isEmpty {
|
||||
self.sidebarSectionHeader("Configured")
|
||||
ForEach(self.enabledChannels) { channel in
|
||||
self.sidebarRow(channel)
|
||||
ForEach(self.enabledProviders) { provider in
|
||||
self.sidebarRow(provider)
|
||||
}
|
||||
}
|
||||
|
||||
if !self.availableChannels.isEmpty {
|
||||
if !self.availableProviders.isEmpty {
|
||||
self.sidebarSectionHeader("Available")
|
||||
ForEach(self.availableChannels) { channel in
|
||||
self.sidebarRow(channel)
|
||||
ForEach(self.availableProviders) { provider in
|
||||
self.sidebarRow(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ extension ConnectionsSettings {
|
||||
|
||||
private var detail: some View {
|
||||
Group {
|
||||
if let channel = self.selectedChannel {
|
||||
self.channelDetail(channel)
|
||||
if let provider = self.selectedProvider {
|
||||
self.providerDetail(provider)
|
||||
} else {
|
||||
self.emptyDetail
|
||||
}
|
||||
@@ -59,7 +59,7 @@ extension ConnectionsSettings {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Connections")
|
||||
.font(.title3.weight(.semibold))
|
||||
Text("Select a channel to view status and settings.")
|
||||
Text("Select a provider to view status and settings.")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -67,12 +67,12 @@ extension ConnectionsSettings {
|
||||
.padding(.vertical, 18)
|
||||
}
|
||||
|
||||
private func channelDetail(_ channel: ConnectionChannel) -> some View {
|
||||
private func providerDetail(_ provider: ConnectionProvider) -> some View {
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
self.detailHeader(for: channel)
|
||||
self.detailHeader(for: provider)
|
||||
Divider()
|
||||
self.channelSection(channel)
|
||||
self.providerSection(provider)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -81,18 +81,18 @@ extension ConnectionsSettings {
|
||||
}
|
||||
}
|
||||
|
||||
private func sidebarRow(_ channel: ConnectionChannel) -> some View {
|
||||
let isSelected = self.selectedChannel == channel
|
||||
private func sidebarRow(_ provider: ConnectionProvider) -> some View {
|
||||
let isSelected = self.selectedProvider == provider
|
||||
return Button {
|
||||
self.selectedChannel = channel
|
||||
self.selectedProvider = provider
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(self.channelTint(channel))
|
||||
.fill(self.providerTint(provider))
|
||||
.frame(width: 8, height: 8)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(channel.title)
|
||||
Text(self.channelSummary(channel))
|
||||
Text(provider.title)
|
||||
Text(self.providerSummary(provider))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -119,23 +119,23 @@ extension ConnectionsSettings {
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
private func detailHeader(for channel: ConnectionChannel) -> some View {
|
||||
private func detailHeader(for provider: ConnectionProvider) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Label(channel.detailTitle, systemImage: channel.systemImage)
|
||||
Label(provider.detailTitle, systemImage: provider.systemImage)
|
||||
.font(.title3.weight(.semibold))
|
||||
self.statusBadge(
|
||||
self.channelSummary(channel),
|
||||
color: self.channelTint(channel))
|
||||
self.providerSummary(provider),
|
||||
color: self.providerTint(provider))
|
||||
Spacer()
|
||||
self.channelHeaderActions(channel)
|
||||
self.providerHeaderActions(provider)
|
||||
}
|
||||
|
||||
HStack(spacing: 10) {
|
||||
Text("Last check \(self.channelLastCheckText(channel))")
|
||||
Text("Last check \(self.providerLastCheckText(provider))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
if self.channelHasError(channel) {
|
||||
if self.providerHasError(provider) {
|
||||
Text("Error")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.padding(.horizontal, 6)
|
||||
@@ -146,7 +146,7 @@ extension ConnectionsSettings {
|
||||
}
|
||||
}
|
||||
|
||||
if let details = self.channelDetails(channel) {
|
||||
if let details = self.providerDetails(provider) {
|
||||
Text(details)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -2,7 +2,7 @@ import AppKit
|
||||
import SwiftUI
|
||||
|
||||
struct ConnectionsSettings: View {
|
||||
enum ConnectionChannel: String, CaseIterable, Identifiable, Hashable {
|
||||
enum ConnectionProvider: String, CaseIterable, Identifiable, Hashable {
|
||||
case whatsapp
|
||||
case telegram
|
||||
case discord
|
||||
@@ -53,7 +53,7 @@ struct ConnectionsSettings: View {
|
||||
}
|
||||
|
||||
@Bindable var store: ConnectionsStore
|
||||
@State var selectedChannel: ConnectionChannel?
|
||||
@State var selectedProvider: ConnectionProvider?
|
||||
@State var showTelegramToken = false
|
||||
@State var showDiscordToken = false
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ extension ConnectionsStore {
|
||||
"probe": AnyCodable(probe),
|
||||
"timeoutMs": AnyCodable(8000),
|
||||
]
|
||||
let snap: ChannelsStatusSnapshot = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .channelsStatus,
|
||||
let snap: ProvidersStatusSnapshot = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .providersStatus,
|
||||
params: params,
|
||||
timeoutMs: 12000)
|
||||
self.snapshot = snap
|
||||
@@ -101,10 +101,10 @@ extension ConnectionsStore {
|
||||
defer { self.whatsappBusy = false }
|
||||
do {
|
||||
let params: [String: AnyCodable] = [
|
||||
"channel": AnyCodable("whatsapp"),
|
||||
"provider": AnyCodable("whatsapp"),
|
||||
]
|
||||
let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .channelsLogout,
|
||||
let result: ProviderLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .providersLogout,
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
self.whatsappLoginMessage = result.cleared
|
||||
@@ -123,10 +123,10 @@ extension ConnectionsStore {
|
||||
defer { self.telegramBusy = false }
|
||||
do {
|
||||
let params: [String: AnyCodable] = [
|
||||
"channel": AnyCodable("telegram"),
|
||||
"provider": AnyCodable("telegram"),
|
||||
]
|
||||
let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .channelsLogout,
|
||||
let result: ProviderLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .providersLogout,
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
if result.envToken == true {
|
||||
@@ -154,8 +154,8 @@ private struct WhatsAppLoginWaitResult: Codable {
|
||||
let message: String
|
||||
}
|
||||
|
||||
private struct ChannelLogoutResult: Codable {
|
||||
let channel: String?
|
||||
private struct ProviderLogoutResult: Codable {
|
||||
let provider: String?
|
||||
let accountId: String?
|
||||
let cleared: Bool
|
||||
let envToken: Bool?
|
||||
|
||||
@@ -2,7 +2,7 @@ import ClawdbotProtocol
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
struct ChannelsStatusSnapshot: Codable {
|
||||
struct ProvidersStatusSnapshot: Codable {
|
||||
struct WhatsAppSelf: Codable {
|
||||
let e164: String?
|
||||
let jid: String?
|
||||
@@ -121,7 +121,7 @@ struct ChannelsStatusSnapshot: Codable {
|
||||
let lastProbeAt: Double?
|
||||
}
|
||||
|
||||
struct ChannelAccountSnapshot: Codable {
|
||||
struct ProviderAccountSnapshot: Codable {
|
||||
let accountId: String
|
||||
let name: String?
|
||||
let enabled: Bool?
|
||||
@@ -154,14 +154,14 @@ struct ChannelsStatusSnapshot: Codable {
|
||||
}
|
||||
|
||||
let ts: Double
|
||||
let channelOrder: [String]
|
||||
let channelLabels: [String: String]
|
||||
let channels: [String: AnyCodable]
|
||||
let channelAccounts: [String: [ChannelAccountSnapshot]]
|
||||
let channelDefaultAccountId: [String: String]
|
||||
let providerOrder: [String]
|
||||
let providerLabels: [String: String]
|
||||
let providers: [String: AnyCodable]
|
||||
let providerAccounts: [String: [ProviderAccountSnapshot]]
|
||||
let providerDefaultAccountId: [String: String]
|
||||
|
||||
func decodeChannel<T: Decodable>(_ id: String, as type: T.Type) -> T? {
|
||||
guard let value = self.channels[id] else { return nil }
|
||||
func decodeProvider<T: Decodable>(_ id: String, as type: T.Type) -> T? {
|
||||
guard let value = self.providers[id] else { return nil }
|
||||
do {
|
||||
let data = try JSONEncoder().encode(value)
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
@@ -230,7 +230,7 @@ struct DiscordGuildForm: Identifiable {
|
||||
final class ConnectionsStore {
|
||||
static let shared = ConnectionsStore()
|
||||
|
||||
var snapshot: ChannelsStatusSnapshot?
|
||||
var snapshot: ProvidersStatusSnapshot?
|
||||
var lastError: String?
|
||||
var lastSuccess: Date?
|
||||
var isRefreshing = false
|
||||
|
||||
@@ -36,13 +36,13 @@ extension CronJobEditor {
|
||||
case let .systemEvent(text):
|
||||
self.payloadKind = .systemEvent
|
||||
self.systemEventText = text
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver):
|
||||
self.payloadKind = .agentTurn
|
||||
self.agentMessage = message
|
||||
self.thinking = thinking ?? ""
|
||||
self.timeoutSeconds = timeoutSeconds.map(String.init) ?? ""
|
||||
self.deliver = deliver ?? false
|
||||
self.channel = GatewayAgentChannel(raw: channel)
|
||||
self.provider = GatewayAgentProvider(raw: provider)
|
||||
self.to = to ?? ""
|
||||
self.bestEffortDeliver = bestEffortDeliver ?? false
|
||||
}
|
||||
@@ -204,7 +204,7 @@ extension CronJobEditor {
|
||||
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
|
||||
payload["deliver"] = self.deliver
|
||||
if self.deliver {
|
||||
payload["channel"] = self.channel.rawValue
|
||||
payload["provider"] = self.provider.rawValue
|
||||
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !to.isEmpty { payload["to"] = to }
|
||||
payload["bestEffortDeliver"] = self.bestEffortDeliver
|
||||
|
||||
@@ -14,7 +14,7 @@ extension CronJobEditor {
|
||||
self.payloadKind = .agentTurn
|
||||
self.agentMessage = "Run diagnostic"
|
||||
self.deliver = true
|
||||
self.channel = .last
|
||||
self.provider = .last
|
||||
self.to = "+15551230000"
|
||||
self.thinking = "low"
|
||||
self.timeoutSeconds = "90"
|
||||
|
||||
@@ -18,7 +18,7 @@ struct CronJobEditor: View {
|
||||
static let scheduleKindNote =
|
||||
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
|
||||
static let isolatedPayloadNote =
|
||||
"Isolated jobs always run an agent turn. The result can be delivered to a channel, "
|
||||
"Isolated jobs always run an agent turn. The result can be delivered to a provider, "
|
||||
+ "and a short summary is posted back to your main chat."
|
||||
static let mainPayloadNote =
|
||||
"System events are injected into the current main session. Agent turns require an isolated session target."
|
||||
@@ -45,7 +45,7 @@ struct CronJobEditor: View {
|
||||
@State var systemEventText: String = ""
|
||||
@State var agentMessage: String = ""
|
||||
@State var deliver: Bool = false
|
||||
@State var channel: GatewayAgentChannel = .last
|
||||
@State var provider: GatewayAgentProvider = .last
|
||||
@State var to: String = ""
|
||||
@State var thinking: String = ""
|
||||
@State var timeoutSeconds: String = ""
|
||||
@@ -323,7 +323,7 @@ struct CronJobEditor: View {
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Deliver")
|
||||
Toggle("Deliver result to a channel", isOn: self.$deliver)
|
||||
Toggle("Deliver result to a provider", isOn: self.$deliver)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
@@ -331,15 +331,15 @@ struct CronJobEditor: View {
|
||||
if self.deliver {
|
||||
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
|
||||
GridRow {
|
||||
self.gridLabel("Channel")
|
||||
Picker("", selection: self.$channel) {
|
||||
Text("last").tag(GatewayAgentChannel.last)
|
||||
Text("whatsapp").tag(GatewayAgentChannel.whatsapp)
|
||||
Text("telegram").tag(GatewayAgentChannel.telegram)
|
||||
Text("discord").tag(GatewayAgentChannel.discord)
|
||||
Text("slack").tag(GatewayAgentChannel.slack)
|
||||
Text("signal").tag(GatewayAgentChannel.signal)
|
||||
Text("imessage").tag(GatewayAgentChannel.imessage)
|
||||
self.gridLabel("Provider")
|
||||
Picker("", selection: self.$provider) {
|
||||
Text("last").tag(GatewayAgentProvider.last)
|
||||
Text("whatsapp").tag(GatewayAgentProvider.whatsapp)
|
||||
Text("telegram").tag(GatewayAgentProvider.telegram)
|
||||
Text("discord").tag(GatewayAgentProvider.discord)
|
||||
Text("slack").tag(GatewayAgentProvider.slack)
|
||||
Text("signal").tag(GatewayAgentProvider.signal)
|
||||
Text("imessage").tag(GatewayAgentProvider.imessage)
|
||||
}
|
||||
.labelsHidden()
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
@@ -74,12 +74,12 @@ enum CronPayload: Codable, Equatable {
|
||||
thinking: String?,
|
||||
timeoutSeconds: Int?,
|
||||
deliver: Bool?,
|
||||
channel: String?,
|
||||
provider: String?,
|
||||
to: String?,
|
||||
bestEffortDeliver: Bool?)
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case kind, text, message, thinking, timeoutSeconds, deliver, channel, provider, to, bestEffortDeliver
|
||||
case kind, text, message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver
|
||||
}
|
||||
|
||||
var kind: String {
|
||||
@@ -101,8 +101,7 @@ enum CronPayload: Codable, Equatable {
|
||||
thinking: container.decodeIfPresent(String.self, forKey: .thinking),
|
||||
timeoutSeconds: container.decodeIfPresent(Int.self, forKey: .timeoutSeconds),
|
||||
deliver: container.decodeIfPresent(Bool.self, forKey: .deliver),
|
||||
channel: container.decodeIfPresent(String.self, forKey: .channel)
|
||||
?? container.decodeIfPresent(String.self, forKey: .provider),
|
||||
provider: container.decodeIfPresent(String.self, forKey: .provider),
|
||||
to: container.decodeIfPresent(String.self, forKey: .to),
|
||||
bestEffortDeliver: container.decodeIfPresent(Bool.self, forKey: .bestEffortDeliver))
|
||||
default:
|
||||
@@ -119,12 +118,12 @@ enum CronPayload: Codable, Equatable {
|
||||
switch self {
|
||||
case let .systemEvent(text):
|
||||
try container.encode(text, forKey: .text)
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver):
|
||||
try container.encode(message, forKey: .message)
|
||||
try container.encodeIfPresent(thinking, forKey: .thinking)
|
||||
try container.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds)
|
||||
try container.encodeIfPresent(deliver, forKey: .deliver)
|
||||
try container.encodeIfPresent(channel, forKey: .channel)
|
||||
try container.encodeIfPresent(provider, forKey: .provider)
|
||||
try container.encodeIfPresent(to, forKey: .to)
|
||||
try container.encodeIfPresent(bestEffortDeliver, forKey: .bestEffortDeliver)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct CronSettings_Previews: PreviewProvider {
|
||||
thinking: "low",
|
||||
timeoutSeconds: 600,
|
||||
deliver: true,
|
||||
channel: "last",
|
||||
provider: "last",
|
||||
to: nil,
|
||||
bestEffortDeliver: true),
|
||||
isolation: CronIsolation(postToMainPrefix: "Cron"),
|
||||
@@ -76,7 +76,7 @@ extension CronSettings {
|
||||
thinking: "low",
|
||||
timeoutSeconds: 120,
|
||||
deliver: true,
|
||||
channel: "whatsapp",
|
||||
provider: "whatsapp",
|
||||
to: "+15551234567",
|
||||
bestEffortDeliver: true),
|
||||
isolation: CronIsolation(postToMainPrefix: "[cron] "),
|
||||
|
||||
@@ -59,7 +59,7 @@ final class DeepLinkHandler {
|
||||
}
|
||||
|
||||
do {
|
||||
let channel = GatewayAgentChannel(raw: link.channel)
|
||||
let provider = GatewayAgentProvider(raw: link.channel)
|
||||
let explicitSessionKey = link.sessionKey?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.nonEmpty
|
||||
@@ -72,9 +72,9 @@ final class DeepLinkHandler {
|
||||
message: messagePreview,
|
||||
sessionKey: resolvedSessionKey,
|
||||
thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
||||
deliver: channel.shouldDeliver(link.deliver),
|
||||
deliver: provider.shouldDeliver(link.deliver),
|
||||
to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
||||
channel: channel,
|
||||
provider: provider,
|
||||
timeoutSeconds: link.timeoutSeconds,
|
||||
idempotencyKey: UUID().uuidString)
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
enum GatewayAgentChannel: String, CaseIterable, Sendable {
|
||||
case last
|
||||
case webchat
|
||||
case whatsapp
|
||||
case telegram
|
||||
|
||||
init(raw: String?) {
|
||||
let trimmed = raw?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased() ?? ""
|
||||
self = GatewayAgentChannel(rawValue: trimmed) ?? .last
|
||||
}
|
||||
|
||||
func shouldDeliver(_ isLast: Bool) -> Bool {
|
||||
switch self {
|
||||
case .webchat:
|
||||
false
|
||||
case .last:
|
||||
isLast
|
||||
case .whatsapp, .telegram:
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import OSLog
|
||||
|
||||
private let gatewayConnectionLogger = Logger(subsystem: "com.clawdbot", category: "gateway.connection")
|
||||
|
||||
enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
|
||||
enum GatewayAgentProvider: String, Codable, CaseIterable, Sendable {
|
||||
case last
|
||||
case whatsapp
|
||||
case telegram
|
||||
@@ -18,7 +18,7 @@ enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
|
||||
|
||||
init(raw: String?) {
|
||||
let normalized = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
self = GatewayAgentChannel(rawValue: normalized) ?? .last
|
||||
self = GatewayAgentProvider(rawValue: normalized) ?? .last
|
||||
}
|
||||
|
||||
var isDeliverable: Bool { self != .webchat }
|
||||
@@ -32,7 +32,7 @@ struct GatewayAgentInvocation: Sendable {
|
||||
var thinking: String?
|
||||
var deliver: Bool = false
|
||||
var to: String?
|
||||
var channel: GatewayAgentChannel = .last
|
||||
var provider: GatewayAgentProvider = .last
|
||||
var timeoutSeconds: Int?
|
||||
var idempotencyKey: String = UUID().uuidString
|
||||
}
|
||||
@@ -52,7 +52,7 @@ actor GatewayConnection {
|
||||
case setHeartbeats = "set-heartbeats"
|
||||
case systemEvent = "system-event"
|
||||
case health
|
||||
case channelsStatus = "channels.status"
|
||||
case providersStatus = "providers.status"
|
||||
case configGet = "config.get"
|
||||
case configSet = "config.set"
|
||||
case wizardStart = "wizard.start"
|
||||
@@ -62,7 +62,7 @@ actor GatewayConnection {
|
||||
case talkMode = "talk.mode"
|
||||
case webLoginStart = "web.login.start"
|
||||
case webLoginWait = "web.login.wait"
|
||||
case channelsLogout = "channels.logout"
|
||||
case providersLogout = "providers.logout"
|
||||
case modelsList = "models.list"
|
||||
case chatHistory = "chat.history"
|
||||
case chatSend = "chat.send"
|
||||
@@ -368,7 +368,7 @@ extension GatewayConnection {
|
||||
"thinking": AnyCodable(invocation.thinking ?? "default"),
|
||||
"deliver": AnyCodable(invocation.deliver),
|
||||
"to": AnyCodable(invocation.to ?? ""),
|
||||
"channel": AnyCodable(invocation.channel.rawValue),
|
||||
"provider": AnyCodable(invocation.provider.rawValue),
|
||||
"idempotencyKey": AnyCodable(invocation.idempotencyKey),
|
||||
]
|
||||
if let timeout = invocation.timeoutSeconds {
|
||||
@@ -389,7 +389,7 @@ extension GatewayConnection {
|
||||
sessionKey: String,
|
||||
deliver: Bool,
|
||||
to: String?,
|
||||
channel: GatewayAgentChannel = .last,
|
||||
provider: GatewayAgentProvider = .last,
|
||||
timeoutSeconds: Int? = nil,
|
||||
idempotencyKey: String = UUID().uuidString) async -> (ok: Bool, error: String?)
|
||||
{
|
||||
@@ -399,7 +399,7 @@ extension GatewayConnection {
|
||||
thinking: thinking,
|
||||
deliver: deliver,
|
||||
to: to,
|
||||
channel: channel,
|
||||
provider: provider,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
idempotencyKey: idempotencyKey))
|
||||
}
|
||||
|
||||
@@ -388,43 +388,6 @@ actor GatewayEndpointStore {
|
||||
}
|
||||
}
|
||||
|
||||
extension GatewayEndpointStore {
|
||||
static func dashboardURL(for config: GatewayConnection.Config) throws -> URL {
|
||||
guard var components = URLComponents(url: config.url, resolvingAgainstBaseURL: false) else {
|
||||
throw NSError(domain: "Dashboard", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Invalid gateway URL",
|
||||
])
|
||||
}
|
||||
switch components.scheme?.lowercased() {
|
||||
case "ws":
|
||||
components.scheme = "http"
|
||||
case "wss":
|
||||
components.scheme = "https"
|
||||
default:
|
||||
components.scheme = "http"
|
||||
}
|
||||
components.path = "/"
|
||||
var queryItems: [URLQueryItem] = []
|
||||
if let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!token.isEmpty
|
||||
{
|
||||
queryItems.append(URLQueryItem(name: "token", value: token))
|
||||
}
|
||||
if let password = config.password?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!password.isEmpty
|
||||
{
|
||||
queryItems.append(URLQueryItem(name: "password", value: password))
|
||||
}
|
||||
components.queryItems = queryItems.isEmpty ? nil : queryItems
|
||||
guard let url = components.url else {
|
||||
throw NSError(domain: "Dashboard", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to build dashboard URL",
|
||||
])
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension GatewayEndpointStore {
|
||||
static func _testResolveGatewayPassword(
|
||||
|
||||
@@ -211,19 +211,19 @@ final class GatewayProcessManager {
|
||||
private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String {
|
||||
let instanceText = instance ?? "pid unknown"
|
||||
if let snap {
|
||||
let linkId = snap.channelOrder?.first(where: {
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
let linkId = snap.providerOrder?.first(where: {
|
||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
||||
return false
|
||||
}) ?? snap.channels.keys.first(where: {
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
}) ?? snap.providers.keys.first(where: {
|
||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
||||
return false
|
||||
})
|
||||
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
|
||||
let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age"
|
||||
let linked = linkId.flatMap { snap.providers[$0]?.linked } ?? false
|
||||
let authAge = linkId.flatMap { snap.providers[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age"
|
||||
let label =
|
||||
linkId.flatMap { snap.channelLabels?[$0] } ??
|
||||
linkId.flatMap { snap.providerLabels?[$0] } ??
|
||||
linkId?.capitalized ??
|
||||
"channel"
|
||||
"provider"
|
||||
let linkText = linked ? "linked" : "not linked"
|
||||
return "port \(port), \(label) \(linkText), auth \(authAge), \(instanceText)"
|
||||
}
|
||||
|
||||
@@ -496,18 +496,18 @@ struct GeneralSettings: View {
|
||||
}
|
||||
|
||||
if let snap = snapshot {
|
||||
let linkId = snap.channelOrder?.first(where: {
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
let linkId = snap.providerOrder?.first(where: {
|
||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
||||
return false
|
||||
}) ?? snap.channels.keys.first(where: {
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
}) ?? snap.providers.keys.first(where: {
|
||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
||||
return false
|
||||
})
|
||||
let linkLabel =
|
||||
linkId.flatMap { snap.channelLabels?[$0] } ??
|
||||
linkId.flatMap { snap.providerLabels?[$0] } ??
|
||||
linkId?.capitalized ??
|
||||
"Link channel"
|
||||
let linkAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }
|
||||
"Link provider"
|
||||
let linkAge = linkId.flatMap { snap.providers[$0]?.authAgeMs }
|
||||
Text("\(linkLabel) auth age: \(healthAgeString(linkAge))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -4,7 +4,7 @@ import Observation
|
||||
import SwiftUI
|
||||
|
||||
struct HealthSnapshot: Codable, Sendable {
|
||||
struct ChannelSummary: Codable, Sendable {
|
||||
struct ProviderSummary: Codable, Sendable {
|
||||
struct Probe: Codable, Sendable {
|
||||
struct Bot: Codable, Sendable {
|
||||
let username: String?
|
||||
@@ -44,9 +44,9 @@ struct HealthSnapshot: Codable, Sendable {
|
||||
let ok: Bool?
|
||||
let ts: Double
|
||||
let durationMs: Double
|
||||
let channels: [String: ChannelSummary]
|
||||
let channelOrder: [String]?
|
||||
let channelLabels: [String: String]?
|
||||
let providers: [String: ProviderSummary]
|
||||
let providerOrder: [String]?
|
||||
let providerLabels: [String: String]?
|
||||
let heartbeatSeconds: Int?
|
||||
let sessions: Sessions
|
||||
}
|
||||
@@ -144,13 +144,13 @@ final class HealthStore {
|
||||
}
|
||||
}
|
||||
|
||||
private static func isChannelHealthy(_ summary: HealthSnapshot.ChannelSummary) -> Bool {
|
||||
private static func isProviderHealthy(_ summary: HealthSnapshot.ProviderSummary) -> Bool {
|
||||
guard summary.configured == true else { return false }
|
||||
// If probe is missing, treat it as "configured but unknown health" (not a hard fail).
|
||||
return summary.probe?.ok ?? true
|
||||
}
|
||||
|
||||
private static func describeProbeFailure(_ probe: HealthSnapshot.ChannelSummary.Probe) -> String {
|
||||
private static func describeProbeFailure(_ probe: HealthSnapshot.ProviderSummary.Probe) -> String {
|
||||
let elapsed = probe.elapsedMs.map { "\(Int($0))ms" }
|
||||
if let error = probe.error, error.lowercased().contains("timeout") || probe.status == nil {
|
||||
if let elapsed { return "Health check timed out (\(elapsed))" }
|
||||
@@ -162,28 +162,28 @@ final class HealthStore {
|
||||
return "\(reason) (\(code))"
|
||||
}
|
||||
|
||||
private func resolveLinkChannel(
|
||||
_ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ChannelSummary)?
|
||||
private func resolveLinkProvider(
|
||||
_ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ProviderSummary)?
|
||||
{
|
||||
let order = snap.channelOrder ?? Array(snap.channels.keys)
|
||||
let order = snap.providerOrder ?? Array(snap.providers.keys)
|
||||
for id in order {
|
||||
if let summary = snap.channels[id], summary.linked != nil {
|
||||
if let summary = snap.providers[id], summary.linked != nil {
|
||||
return (id: id, summary: summary)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func resolveFallbackChannel(
|
||||
private func resolveFallbackProvider(
|
||||
_ snap: HealthSnapshot,
|
||||
excluding id: String?) -> (id: String, summary: HealthSnapshot.ChannelSummary)?
|
||||
excluding id: String?) -> (id: String, summary: HealthSnapshot.ProviderSummary)?
|
||||
{
|
||||
let order = snap.channelOrder ?? Array(snap.channels.keys)
|
||||
for channelId in order {
|
||||
if channelId == id { continue }
|
||||
guard let summary = snap.channels[channelId] else { continue }
|
||||
if Self.isChannelHealthy(summary) {
|
||||
return (id: channelId, summary: summary)
|
||||
let order = snap.providerOrder ?? Array(snap.providers.keys)
|
||||
for providerId in order {
|
||||
if providerId == id { continue }
|
||||
guard let summary = snap.providers[providerId] else { continue }
|
||||
if Self.isProviderHealthy(summary) {
|
||||
return (id: providerId, summary: summary)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -194,13 +194,13 @@ final class HealthStore {
|
||||
return .degraded(error)
|
||||
}
|
||||
guard let snap = self.snapshot else { return .unknown }
|
||||
guard let link = self.resolveLinkChannel(snap) else { return .unknown }
|
||||
guard let link = self.resolveLinkProvider(snap) else { return .unknown }
|
||||
if link.summary.linked != true {
|
||||
// Linking is optional if any other channel is healthy; don't paint the whole app red.
|
||||
let fallback = self.resolveFallbackChannel(snap, excluding: link.id)
|
||||
// Linking is optional if any other provider is healthy; don't paint the whole app red.
|
||||
let fallback = self.resolveFallbackProvider(snap, excluding: link.id)
|
||||
return fallback != nil ? .degraded("Not linked") : .linkingNeeded
|
||||
}
|
||||
// A channel can be "linked" but still unhealthy (failed probe / cannot connect).
|
||||
// A provider can be "linked" but still unhealthy (failed probe / cannot connect).
|
||||
if let probe = link.summary.probe, probe.ok == false {
|
||||
return .degraded(Self.describeProbeFailure(probe))
|
||||
}
|
||||
@@ -211,10 +211,10 @@ final class HealthStore {
|
||||
if self.isRefreshing { return "Health check running…" }
|
||||
if let error = self.lastError { return "Health check failed: \(error)" }
|
||||
guard let snap = self.snapshot else { return "Health check pending" }
|
||||
guard let link = self.resolveLinkChannel(snap) else { return "Health check pending" }
|
||||
guard let link = self.resolveLinkProvider(snap) else { return "Health check pending" }
|
||||
if link.summary.linked != true {
|
||||
if let fallback = self.resolveFallbackChannel(snap, excluding: link.id) {
|
||||
let fallbackLabel = snap.channelLabels?[fallback.id] ?? fallback.id.capitalized
|
||||
if let fallback = self.resolveFallbackProvider(snap, excluding: link.id) {
|
||||
let fallbackLabel = snap.providerLabels?[fallback.id] ?? fallback.id.capitalized
|
||||
let fallbackState = (fallback.summary.probe?.ok ?? true) ? "ok" : "degraded"
|
||||
return "\(fallbackLabel) \(fallbackState) · Not linked — run clawdbot login"
|
||||
}
|
||||
@@ -247,10 +247,10 @@ final class HealthStore {
|
||||
}
|
||||
|
||||
func describeFailure(from snap: HealthSnapshot, fallback: String?) -> String {
|
||||
if let link = self.resolveLinkChannel(snap), link.summary.linked != true {
|
||||
if let link = self.resolveLinkProvider(snap), link.summary.linked != true {
|
||||
return "Not linked — run clawdbot login"
|
||||
}
|
||||
if let link = self.resolveLinkChannel(snap), let probe = link.summary.probe, probe.ok == false {
|
||||
if let link = self.resolveLinkProvider(snap), let probe = link.summary.probe, probe.ok == false {
|
||||
return Self.describeProbeFailure(probe)
|
||||
}
|
||||
if let fallback, !fallback.isEmpty {
|
||||
|
||||
@@ -242,18 +242,18 @@ final class InstancesStore {
|
||||
do {
|
||||
let data = try await ControlChannel.shared.health(timeout: 8)
|
||||
guard let snap = decodeHealthSnapshot(from: data) else { return }
|
||||
let linkId = snap.channelOrder?.first(where: {
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
let linkId = snap.providerOrder?.first(where: {
|
||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
||||
return false
|
||||
}) ?? snap.channels.keys.first(where: {
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
}) ?? snap.providers.keys.first(where: {
|
||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
||||
return false
|
||||
})
|
||||
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
|
||||
let linked = linkId.flatMap { snap.providers[$0]?.linked } ?? false
|
||||
let linkLabel =
|
||||
linkId.flatMap { snap.channelLabels?[$0] } ??
|
||||
linkId.flatMap { snap.providerLabels?[$0] } ??
|
||||
linkId?.capitalized ??
|
||||
"channel"
|
||||
"provider"
|
||||
let entry = InstanceInfo(
|
||||
id: "health-\(snap.ts)",
|
||||
host: "gateway (health)",
|
||||
|
||||
@@ -313,7 +313,27 @@ struct MenuContent: View {
|
||||
private func openDashboard() async {
|
||||
do {
|
||||
let config = try await GatewayEndpointStore.shared.requireConfig()
|
||||
let url = try GatewayEndpointStore.dashboardURL(for: config)
|
||||
let wsURL = config.url
|
||||
guard var components = URLComponents(url: wsURL, resolvingAgainstBaseURL: false) else {
|
||||
throw NSError(domain: "Dashboard", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Invalid gateway URL",
|
||||
])
|
||||
}
|
||||
switch components.scheme?.lowercased() {
|
||||
case "ws":
|
||||
components.scheme = "http"
|
||||
case "wss":
|
||||
components.scheme = "https"
|
||||
default:
|
||||
components.scheme = "http"
|
||||
}
|
||||
components.path = "/"
|
||||
components.query = nil
|
||||
guard let url = components.url else {
|
||||
throw NSError(domain: "Dashboard", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to build dashboard URL",
|
||||
])
|
||||
}
|
||||
NSWorkspace.shared.open(url)
|
||||
} catch {
|
||||
let alert = NSAlert()
|
||||
|
||||
@@ -694,7 +694,7 @@ extension OnboardingView {
|
||||
systemImage: "bubble.left.and.bubble.right")
|
||||
self.featureActionRow(
|
||||
title: "Connect WhatsApp or Telegram",
|
||||
subtitle: "Open Settings → Connections to link channels and monitor status.",
|
||||
subtitle: "Open Settings → Connections to link providers and monitor status.",
|
||||
systemImage: "link")
|
||||
{
|
||||
self.openSettings(tab: .connections)
|
||||
|
||||
@@ -37,7 +37,7 @@ enum VoiceWakeForwarder {
|
||||
var thinking: String = "low"
|
||||
var deliver: Bool = true
|
||||
var to: String?
|
||||
var channel: GatewayAgentChannel = .last
|
||||
var provider: GatewayAgentProvider = .last
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -46,14 +46,14 @@ enum VoiceWakeForwarder {
|
||||
options: ForwardOptions = ForwardOptions()) async -> Result<Void, VoiceWakeForwardError>
|
||||
{
|
||||
let payload = Self.prefixedTranscript(transcript)
|
||||
let deliver = options.channel.shouldDeliver(options.deliver)
|
||||
let deliver = options.provider.shouldDeliver(options.deliver)
|
||||
let result = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
|
||||
message: payload,
|
||||
sessionKey: options.sessionKey,
|
||||
thinking: options.thinking,
|
||||
deliver: deliver,
|
||||
to: options.to,
|
||||
channel: options.channel))
|
||||
provider: options.provider))
|
||||
|
||||
if result.ok {
|
||||
self.logger.info("voice wake forward ok")
|
||||
|
||||
@@ -341,7 +341,7 @@ public struct SendParams: Codable, Sendable {
|
||||
public let message: String
|
||||
public let mediaurl: String?
|
||||
public let gifplayback: Bool?
|
||||
public let channel: String?
|
||||
public let provider: String?
|
||||
public let accountid: String?
|
||||
public let idempotencykey: String
|
||||
|
||||
@@ -350,7 +350,7 @@ public struct SendParams: Codable, Sendable {
|
||||
message: String,
|
||||
mediaurl: String?,
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
provider: String?,
|
||||
accountid: String?,
|
||||
idempotencykey: String
|
||||
) {
|
||||
@@ -358,7 +358,7 @@ public struct SendParams: Codable, Sendable {
|
||||
self.message = message
|
||||
self.mediaurl = mediaurl
|
||||
self.gifplayback = gifplayback
|
||||
self.channel = channel
|
||||
self.provider = provider
|
||||
self.accountid = accountid
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
@@ -367,7 +367,7 @@ public struct SendParams: Codable, Sendable {
|
||||
case message
|
||||
case mediaurl = "mediaUrl"
|
||||
case gifplayback = "gifPlayback"
|
||||
case channel
|
||||
case provider
|
||||
case accountid = "accountId"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
@@ -379,7 +379,7 @@ public struct PollParams: Codable, Sendable {
|
||||
public let options: [String]
|
||||
public let maxselections: Int?
|
||||
public let durationhours: Int?
|
||||
public let channel: String?
|
||||
public let provider: String?
|
||||
public let accountid: String?
|
||||
public let idempotencykey: String
|
||||
|
||||
@@ -389,7 +389,7 @@ public struct PollParams: Codable, Sendable {
|
||||
options: [String],
|
||||
maxselections: Int?,
|
||||
durationhours: Int?,
|
||||
channel: String?,
|
||||
provider: String?,
|
||||
accountid: String?,
|
||||
idempotencykey: String
|
||||
) {
|
||||
@@ -398,7 +398,7 @@ public struct PollParams: Codable, Sendable {
|
||||
self.options = options
|
||||
self.maxselections = maxselections
|
||||
self.durationhours = durationhours
|
||||
self.channel = channel
|
||||
self.provider = provider
|
||||
self.accountid = accountid
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
@@ -408,7 +408,7 @@ public struct PollParams: Codable, Sendable {
|
||||
case options
|
||||
case maxselections = "maxSelections"
|
||||
case durationhours = "durationHours"
|
||||
case channel
|
||||
case provider
|
||||
case accountid = "accountId"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
@@ -422,7 +422,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let thinking: String?
|
||||
public let deliver: Bool?
|
||||
public let attachments: [AnyCodable]?
|
||||
public let channel: String?
|
||||
public let provider: String?
|
||||
public let timeout: Int?
|
||||
public let lane: String?
|
||||
public let extrasystemprompt: String?
|
||||
@@ -438,7 +438,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
thinking: String?,
|
||||
deliver: Bool?,
|
||||
attachments: [AnyCodable]?,
|
||||
channel: String?,
|
||||
provider: String?,
|
||||
timeout: Int?,
|
||||
lane: String?,
|
||||
extrasystemprompt: String?,
|
||||
@@ -453,7 +453,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.thinking = thinking
|
||||
self.deliver = deliver
|
||||
self.attachments = attachments
|
||||
self.channel = channel
|
||||
self.provider = provider
|
||||
self.timeout = timeout
|
||||
self.lane = lane
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
@@ -469,7 +469,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
case thinking
|
||||
case deliver
|
||||
case attachments
|
||||
case channel
|
||||
case provider
|
||||
case timeout
|
||||
case lane
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
@@ -1102,7 +1102,7 @@ public struct TalkModeParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChannelsStatusParams: Codable, Sendable {
|
||||
public struct ProvidersStatusParams: Codable, Sendable {
|
||||
public let probe: Bool?
|
||||
public let timeoutms: Int?
|
||||
|
||||
@@ -1119,52 +1119,52 @@ public struct ChannelsStatusParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChannelsStatusResult: Codable, Sendable {
|
||||
public struct ProvidersStatusResult: Codable, Sendable {
|
||||
public let ts: Int
|
||||
public let channelorder: [String]
|
||||
public let channellabels: [String: AnyCodable]
|
||||
public let channels: [String: AnyCodable]
|
||||
public let channelaccounts: [String: AnyCodable]
|
||||
public let channeldefaultaccountid: [String: AnyCodable]
|
||||
public let providerorder: [String]
|
||||
public let providerlabels: [String: AnyCodable]
|
||||
public let providers: [String: AnyCodable]
|
||||
public let provideraccounts: [String: AnyCodable]
|
||||
public let providerdefaultaccountid: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
ts: Int,
|
||||
channelorder: [String],
|
||||
channellabels: [String: AnyCodable],
|
||||
channels: [String: AnyCodable],
|
||||
channelaccounts: [String: AnyCodable],
|
||||
channeldefaultaccountid: [String: AnyCodable]
|
||||
providerorder: [String],
|
||||
providerlabels: [String: AnyCodable],
|
||||
providers: [String: AnyCodable],
|
||||
provideraccounts: [String: AnyCodable],
|
||||
providerdefaultaccountid: [String: AnyCodable]
|
||||
) {
|
||||
self.ts = ts
|
||||
self.channelorder = channelorder
|
||||
self.channellabels = channellabels
|
||||
self.channels = channels
|
||||
self.channelaccounts = channelaccounts
|
||||
self.channeldefaultaccountid = channeldefaultaccountid
|
||||
self.providerorder = providerorder
|
||||
self.providerlabels = providerlabels
|
||||
self.providers = providers
|
||||
self.provideraccounts = provideraccounts
|
||||
self.providerdefaultaccountid = providerdefaultaccountid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ts
|
||||
case channelorder = "channelOrder"
|
||||
case channellabels = "channelLabels"
|
||||
case channels
|
||||
case channelaccounts = "channelAccounts"
|
||||
case channeldefaultaccountid = "channelDefaultAccountId"
|
||||
case providerorder = "providerOrder"
|
||||
case providerlabels = "providerLabels"
|
||||
case providers
|
||||
case provideraccounts = "providerAccounts"
|
||||
case providerdefaultaccountid = "providerDefaultAccountId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChannelsLogoutParams: Codable, Sendable {
|
||||
public let channel: String
|
||||
public struct ProvidersLogoutParams: Codable, Sendable {
|
||||
public let provider: String
|
||||
public let accountid: String?
|
||||
|
||||
public init(
|
||||
channel: String,
|
||||
provider: String,
|
||||
accountid: String?
|
||||
) {
|
||||
self.channel = channel
|
||||
self.provider = provider
|
||||
self.accountid = accountid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case channel
|
||||
case provider
|
||||
case accountid = "accountId"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@ import Testing
|
||||
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct ConnectionsSettingsSmokeTests {
|
||||
@Test func connectionsSettingsBuildsBodyWithSnapshot() {
|
||||
let store = ConnectionsStore(isPreview: true)
|
||||
store.snapshot = ChannelsStatusSnapshot(
|
||||
ts: 1_700_000_000_000,
|
||||
channelOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
||||
channelLabels: [
|
||||
"whatsapp": "WhatsApp",
|
||||
"telegram": "Telegram",
|
||||
"signal": "Signal",
|
||||
"imessage": "iMessage",
|
||||
],
|
||||
channels: [
|
||||
"whatsapp": AnyCodable([
|
||||
"configured": true,
|
||||
"linked": true,
|
||||
struct ConnectionsSettingsSmokeTests {
|
||||
@Test func connectionsSettingsBuildsBodyWithSnapshot() {
|
||||
let store = ConnectionsStore(isPreview: true)
|
||||
store.snapshot = ProvidersStatusSnapshot(
|
||||
ts: 1_700_000_000_000,
|
||||
providerOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
||||
providerLabels: [
|
||||
"whatsapp": "WhatsApp",
|
||||
"telegram": "Telegram",
|
||||
"signal": "Signal",
|
||||
"imessage": "iMessage",
|
||||
],
|
||||
providers: [
|
||||
"whatsapp": AnyCodable([
|
||||
"configured": true,
|
||||
"linked": true,
|
||||
"authAgeMs": 86_400_000,
|
||||
"self": ["e164": "+15551234567"],
|
||||
"running": true,
|
||||
@@ -70,13 +70,13 @@ import Testing
|
||||
"lastError": "not configured",
|
||||
"probe": ["ok": false, "error": "imsg not found (imsg)"],
|
||||
"lastProbeAt": 1_700_000_050_000,
|
||||
]),
|
||||
],
|
||||
channelAccounts: [:],
|
||||
channelDefaultAccountId: [
|
||||
"whatsapp": "default",
|
||||
"telegram": "default",
|
||||
"signal": "default",
|
||||
]),
|
||||
],
|
||||
providerAccounts: [:],
|
||||
providerDefaultAccountId: [
|
||||
"whatsapp": "default",
|
||||
"telegram": "default",
|
||||
"signal": "default",
|
||||
"imessage": "default",
|
||||
])
|
||||
|
||||
@@ -93,23 +93,23 @@ import Testing
|
||||
|
||||
let view = ConnectionsSettings(store: store)
|
||||
_ = view.body
|
||||
}
|
||||
}
|
||||
|
||||
@Test func connectionsSettingsBuildsBodyWithoutSnapshot() {
|
||||
let store = ConnectionsStore(isPreview: true)
|
||||
store.snapshot = ChannelsStatusSnapshot(
|
||||
ts: 1_700_000_000_000,
|
||||
channelOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
||||
channelLabels: [
|
||||
"whatsapp": "WhatsApp",
|
||||
"telegram": "Telegram",
|
||||
"signal": "Signal",
|
||||
"imessage": "iMessage",
|
||||
],
|
||||
channels: [
|
||||
"whatsapp": AnyCodable([
|
||||
"configured": false,
|
||||
"linked": false,
|
||||
@Test func connectionsSettingsBuildsBodyWithoutSnapshot() {
|
||||
let store = ConnectionsStore(isPreview: true)
|
||||
store.snapshot = ProvidersStatusSnapshot(
|
||||
ts: 1_700_000_000_000,
|
||||
providerOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
||||
providerLabels: [
|
||||
"whatsapp": "WhatsApp",
|
||||
"telegram": "Telegram",
|
||||
"signal": "Signal",
|
||||
"imessage": "iMessage",
|
||||
],
|
||||
providers: [
|
||||
"whatsapp": AnyCodable([
|
||||
"configured": false,
|
||||
"linked": false,
|
||||
"running": false,
|
||||
"connected": false,
|
||||
"reconnectAttempts": 0,
|
||||
@@ -146,13 +146,13 @@ import Testing
|
||||
"cliPath": "imsg",
|
||||
"probe": ["ok": false, "error": "imsg not found (imsg)"],
|
||||
"lastProbeAt": 1_700_000_200_000,
|
||||
]),
|
||||
],
|
||||
channelAccounts: [:],
|
||||
channelDefaultAccountId: [
|
||||
"whatsapp": "default",
|
||||
"telegram": "default",
|
||||
"signal": "default",
|
||||
]),
|
||||
],
|
||||
providerAccounts: [:],
|
||||
providerDefaultAccountId: [
|
||||
"whatsapp": "default",
|
||||
"telegram": "default",
|
||||
"signal": "default",
|
||||
"imessage": "default",
|
||||
])
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Clawdbot
|
||||
|
||||
@Suite struct HealthDecodeTests {
|
||||
private let sampleJSON: String = // minimal but complete payload
|
||||
"""
|
||||
{"ts":1733622000,"durationMs":420,"channels":{"whatsapp":{"linked":true,"authAgeMs":120000},"telegram":{"configured":true,"probe":{"ok":true,"elapsedMs":800}}},"channelOrder":["whatsapp","telegram"],"heartbeatSeconds":60,"sessions":{"path":"/tmp/sessions.json","count":1,"recent":[{"key":"abc","updatedAt":1733621900,"age":120000}]}}
|
||||
@Suite struct HealthDecodeTests {
|
||||
private let sampleJSON: String = // minimal but complete payload
|
||||
"""
|
||||
{"ts":1733622000,"durationMs":420,"providers":{"whatsapp":{"linked":true,"authAgeMs":120000},"telegram":{"configured":true,"probe":{"ok":true,"elapsedMs":800}}},"providerOrder":["whatsapp","telegram"],"heartbeatSeconds":60,"sessions":{"path":"/tmp/sessions.json","count":1,"recent":[{"key":"abc","updatedAt":1733621900,"age":120000}]}}
|
||||
"""
|
||||
|
||||
@Test func decodesCleanJSON() async throws {
|
||||
let data = Data(sampleJSON.utf8)
|
||||
let snap = decodeHealthSnapshot(from: data)
|
||||
|
||||
#expect(snap?.channels["whatsapp"]?.linked == true)
|
||||
#expect(snap?.providers["whatsapp"]?.linked == true)
|
||||
#expect(snap?.sessions.count == 1)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import Testing
|
||||
let noisy = "debug: something logged\n" + self.sampleJSON + "\ntrailer"
|
||||
let snap = decodeHealthSnapshot(from: Data(noisy.utf8))
|
||||
|
||||
#expect(snap?.channels["telegram"]?.probe?.elapsedMs == 800)
|
||||
#expect(snap?.providers["telegram"]?.probe?.elapsedMs == 800)
|
||||
}
|
||||
|
||||
@Test func failsWithoutBraces() async throws {
|
||||
|
||||
@@ -3,12 +3,12 @@ import Testing
|
||||
@testable import Clawdbot
|
||||
|
||||
@Suite struct HealthStoreStateTests {
|
||||
@Test @MainActor func linkedChannelProbeFailureDegradesState() async throws {
|
||||
@Test @MainActor func linkedProviderProbeFailureDegradesState() async throws {
|
||||
let snap = HealthSnapshot(
|
||||
ok: true,
|
||||
ts: 0,
|
||||
durationMs: 1,
|
||||
channels: [
|
||||
providers: [
|
||||
"whatsapp": .init(
|
||||
configured: true,
|
||||
linked: true,
|
||||
@@ -22,8 +22,8 @@ import Testing
|
||||
webhook: nil),
|
||||
lastProbeAt: 0),
|
||||
],
|
||||
channelOrder: ["whatsapp"],
|
||||
channelLabels: ["whatsapp": "WhatsApp"],
|
||||
providerOrder: ["whatsapp"],
|
||||
providerLabels: ["whatsapp": "WhatsApp"],
|
||||
heartbeatSeconds: 60,
|
||||
sessions: .init(path: "/tmp/sessions.json", count: 0, recent: []))
|
||||
|
||||
@@ -34,7 +34,7 @@ import Testing
|
||||
case let .degraded(message):
|
||||
#expect(!message.isEmpty)
|
||||
default:
|
||||
Issue.record("Expected degraded state when probe fails for linked channel")
|
||||
Issue.record("Expected degraded state when probe fails for linked provider")
|
||||
}
|
||||
|
||||
#expect(store.summaryLine.contains("probe degraded"))
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/biome.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentWidth": 2,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"files": {
|
||||
"includes": ["src/**/*.ts", "test/**/*.ts"]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ nav:
|
||||
- title: "Android App"
|
||||
url: "/platforms/android/"
|
||||
- title: "Telegram"
|
||||
url: "/channels/telegram/"
|
||||
url: "/providers/telegram/"
|
||||
- title: "Security"
|
||||
url: "/gateway/security/"
|
||||
- title: "Troubleshooting"
|
||||
|
||||
@@ -80,7 +80,7 @@ Key behaviors:
|
||||
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
||||
- A summary is posted to the main session (prefix `Cron`, configurable).
|
||||
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
|
||||
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
|
||||
- If `payload.deliver: true`, output is delivered to a provider; otherwise it stays internal.
|
||||
|
||||
Use isolated jobs for noisy, frequent, or "background chores" that shouldn't spam
|
||||
your main chat history.
|
||||
@@ -94,9 +94,9 @@ Common `agentTurn` fields:
|
||||
- `message`: required text prompt.
|
||||
- `model` / `thinking`: optional overrides (see below).
|
||||
- `timeoutSeconds`: optional timeout override.
|
||||
- `deliver`: `true` to send output to a channel target.
|
||||
- `channel`: `last` or a specific channel.
|
||||
- `to`: channel-specific target (phone/chat/channel id).
|
||||
- `deliver`: `true` to send output to a provider target.
|
||||
- `provider`: `last` or a specific provider.
|
||||
- `to`: provider-specific target (phone/chat/channel id).
|
||||
- `bestEffortDeliver`: avoid failing the job if delivery fails.
|
||||
|
||||
Isolation options (only for `session=isolated`):
|
||||
@@ -116,12 +116,12 @@ Resolution priority:
|
||||
2. Hook-specific defaults (e.g., `hooks.gmail.model`)
|
||||
3. Agent config default
|
||||
|
||||
### Delivery (channel + target)
|
||||
Isolated jobs can deliver output to a channel. The job payload can specify:
|
||||
- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage` / `last`
|
||||
- `to`: channel-specific recipient target
|
||||
### Delivery (provider + target)
|
||||
Isolated jobs can deliver output to a provider. The job payload can specify:
|
||||
- `provider`: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage` / `last`
|
||||
- `to`: provider-specific recipient target
|
||||
|
||||
If `channel` or `to` is omitted, cron can fall back to the main session’s “last route”
|
||||
If `provider` or `to` is omitted, cron can fall back to the main session’s “last route”
|
||||
(the last place the agent replied).
|
||||
|
||||
Target format reminders:
|
||||
@@ -192,7 +192,7 @@ clawdbot cron add \
|
||||
--session isolated \
|
||||
--message "Summarize inbox + calendar for today." \
|
||||
--deliver \
|
||||
--channel whatsapp \
|
||||
--provider whatsapp \
|
||||
--to "+15551234567"
|
||||
```
|
||||
|
||||
@@ -205,7 +205,7 @@ clawdbot cron add \
|
||||
--session isolated \
|
||||
--message "Summarize today; send to the nightly topic." \
|
||||
--deliver \
|
||||
--channel telegram \
|
||||
--provider telegram \
|
||||
--to "-1001234567890:topic:123"
|
||||
```
|
||||
|
||||
@@ -220,7 +220,7 @@ clawdbot cron add \
|
||||
--model "opus" \
|
||||
--thinking high \
|
||||
--deliver \
|
||||
--channel whatsapp \
|
||||
--provider whatsapp \
|
||||
--to "+15551234567"
|
||||
|
||||
Agent selection (multi-agent setups):
|
||||
|
||||
@@ -32,7 +32,7 @@ Example hook config (enable Gmail preset mapping):
|
||||
```
|
||||
|
||||
To deliver the Gmail summary to a chat surface, override the preset with a mapping
|
||||
that sets `deliver` + optional `channel`/`to`:
|
||||
that sets `deliver` + optional `provider`/`to`:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -51,7 +51,7 @@ that sets `deliver` + optional `channel`/`to`:
|
||||
"New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
|
||||
model: "openai/gpt-5.2-mini",
|
||||
deliver: true,
|
||||
channel: "last"
|
||||
provider: "last"
|
||||
// to: "+15551234567"
|
||||
}
|
||||
]
|
||||
@@ -59,7 +59,7 @@ that sets `deliver` + optional `channel`/`to`:
|
||||
}
|
||||
```
|
||||
|
||||
If you want a fixed channel, set `channel` + `to`. Otherwise `channel: "last"`
|
||||
If you want a fixed channel, set `provider` + `to`. Otherwise `provider: "last"`
|
||||
uses the last delivery route (falls back to WhatsApp).
|
||||
|
||||
To force a cheaper model for Gmail runs, set `model` in the mapping
|
||||
|
||||
@@ -7,8 +7,8 @@ read_when:
|
||||
# Polls
|
||||
|
||||
|
||||
## Supported channels
|
||||
- WhatsApp (web channel)
|
||||
## Supported providers
|
||||
- WhatsApp (web provider)
|
||||
- Discord
|
||||
- MS Teams (Adaptive Cards)
|
||||
|
||||
@@ -22,18 +22,18 @@ clawdbot message poll --to 123456789@g.us \
|
||||
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
|
||||
|
||||
# Discord
|
||||
clawdbot message poll --channel discord --to channel:123456789 \
|
||||
clawdbot message poll --provider discord --to channel:123456789 \
|
||||
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
|
||||
clawdbot message poll --channel discord --to channel:123456789 \
|
||||
clawdbot message poll --provider discord --to channel:123456789 \
|
||||
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
|
||||
|
||||
# MS Teams
|
||||
clawdbot message poll --channel msteams --to conversation:19:abc@thread.tacv2 \
|
||||
clawdbot message poll --provider msteams --to conversation:19:abc@thread.tacv2 \
|
||||
--poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--channel`: `whatsapp` (default), `discord`, or `msteams`
|
||||
- `--provider`: `whatsapp` (default), `discord`, or `msteams`
|
||||
- `--poll-multi`: allow selecting multiple options
|
||||
- `--poll-duration-hours`: Discord-only (defaults to 24 when omitted)
|
||||
|
||||
@@ -47,16 +47,16 @@ Params:
|
||||
- `options` (string[], required)
|
||||
- `maxSelections` (number, optional)
|
||||
- `durationHours` (number, optional)
|
||||
- `channel` (string, optional, default: `whatsapp`)
|
||||
- `provider` (string, optional, default: `whatsapp`)
|
||||
- `idempotencyKey` (string, required)
|
||||
|
||||
## Channel differences
|
||||
## Provider differences
|
||||
- WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
|
||||
- Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
|
||||
- MS Teams: Adaptive Card polls (Clawdbot-managed). No native poll API; `durationHours` is ignored.
|
||||
|
||||
## Agent tool (Message)
|
||||
Use the `message` tool with `poll` action (`to`, `pollQuestion`, `pollOption`, optional `pollMulti`, `pollDurationHours`, `channel`).
|
||||
Use the `message` tool with `poll` action (`to`, `pollQuestion`, `pollOption`, optional `pollMulti`, `pollDurationHours`, `provider`).
|
||||
|
||||
Note: Discord has no “pick exactly N” mode; `pollMulti` maps to multi-select.
|
||||
Teams polls are rendered as Adaptive Cards and require the gateway to stay online
|
||||
|
||||
@@ -58,7 +58,7 @@ Payload:
|
||||
"sessionKey": "hook:email:msg-123",
|
||||
"wakeMode": "now",
|
||||
"deliver": true,
|
||||
"channel": "last",
|
||||
"provider": "last",
|
||||
"to": "+15551234567",
|
||||
"model": "openai/gpt-5.2-mini",
|
||||
"thinking": "low",
|
||||
@@ -70,9 +70,9 @@ Payload:
|
||||
- `name` optional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.
|
||||
- `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context.
|
||||
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
|
||||
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
|
||||
- `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`.
|
||||
- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack, conversation ID for MS Teams). Defaults to the last recipient in the main session.
|
||||
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging provider. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
|
||||
- `provider` optional (string): The messaging service for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`.
|
||||
- `to` optional (string): The recipient identifier for the provider (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack, conversation ID for MS Teams). Defaults to the last recipient in the main session.
|
||||
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
|
||||
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
|
||||
- `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.
|
||||
@@ -94,8 +94,8 @@ Mapping options (summary):
|
||||
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
|
||||
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
|
||||
- TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime.
|
||||
- Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface
|
||||
(`channel` defaults to `last` and falls back to WhatsApp).
|
||||
- Set `deliver: true` + `provider`/`to` on mappings to route replies to a chat surface
|
||||
(`provider` defaults to `last` and falls back to WhatsApp).
|
||||
- `clawdbot hooks gmail setup` writes `hooks.gmail` config for `clawdbot hooks gmail run`.
|
||||
See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow.
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ status: experimental
|
||||
|
||||
Broadcast Groups enable multiple agents to process and respond to the same message simultaneously. This allows you to create specialized agent teams that work together in a single WhatsApp group or DM — all using one phone number.
|
||||
|
||||
Current scope: **WhatsApp only** (web channel).
|
||||
Current scope: **WhatsApp only** (web provider).
|
||||
|
||||
Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when Clawdbot would normally reply (for example: on mention, depending on your group settings).
|
||||
Broadcast groups are evaluated after provider allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when Clawdbot would normally reply (for example: on mention, depending on your group settings).
|
||||
|
||||
## Use Cases
|
||||
|
||||
@@ -152,7 +152,7 @@ Agents process in order (one waits for previous to finish):
|
||||
4. **If not in broadcast list**:
|
||||
- Normal routing applies (first matching binding)
|
||||
|
||||
Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change *which agents run* when a message is eligible for processing.
|
||||
Note: broadcast groups do not bypass provider allowlists or group activation rules (mentions/commands/etc). They only change *which agents run* when a message is eligible for processing.
|
||||
|
||||
### Session Isolation
|
||||
|
||||
@@ -272,7 +272,7 @@ Broadcast groups work alongside existing routing:
|
||||
```json
|
||||
{
|
||||
"bindings": [
|
||||
{ "match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } }, "agentId": "alfred" }
|
||||
{ "match": { "provider": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } }, "agentId": "alfred" }
|
||||
],
|
||||
"broadcast": {
|
||||
"GROUP_B": ["agent1", "agent2"]
|
||||
@@ -403,5 +403,5 @@ Planned features:
|
||||
## See Also
|
||||
|
||||
- [Multi-Agent Configuration](/multi-agent-sandbox-tools)
|
||||
- [Routing Configuration](/concepts/channel-routing)
|
||||
- [Routing Configuration](/concepts/provider-routing)
|
||||
- [Session Management](/concepts/sessions)
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
summary: "Messaging platforms Clawdbot can connect to"
|
||||
read_when:
|
||||
- You want to choose a chat channel for Clawdbot
|
||||
- You need a quick overview of supported messaging platforms
|
||||
---
|
||||
# Chat Channels
|
||||
|
||||
Clawdbot can talk to you on any chat app you already use. Each channel connects via the Gateway.
|
||||
Text is supported everywhere; media and reactions vary by channel.
|
||||
|
||||
## Supported channels
|
||||
|
||||
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
|
||||
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
|
||||
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
|
||||
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
||||
- [Signal](/channels/signal) — signal-cli; privacy-focused.
|
||||
- [iMessage](/channels/imessage) — macOS only; native integration.
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support.
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
|
||||
## Notes
|
||||
|
||||
- Channels can run simultaneously; configure multiple and Clawdbot will route per chat.
|
||||
- Group behavior varies by channel; see [Groups](/concepts/groups).
|
||||
- DM pairing and allowlists are enforced for safety; see [Security](/gateway/security).
|
||||
- Telegram internals: [grammY notes](/channels/grammy).
|
||||
- Troubleshooting: [Channel troubleshooting](/channels/troubleshooting).
|
||||
- Model providers are documented separately; see [Model Providers](/providers/models).
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
summary: "Signal support via signal-cli (JSON-RPC + SSE), setup, and number model"
|
||||
read_when:
|
||||
- Setting up Signal support
|
||||
- Debugging Signal send/receive
|
||||
---
|
||||
# Signal (signal-cli)
|
||||
|
||||
|
||||
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
|
||||
|
||||
## Quick setup (beginner)
|
||||
1) Use a **separate Signal number** for the bot (recommended).
|
||||
2) Install `signal-cli` (Java required).
|
||||
3) Link the bot device and start the daemon:
|
||||
- `signal-cli link -n "Clawdbot"`
|
||||
4) Configure Clawdbot and start the gateway.
|
||||
|
||||
Minimal config:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
signal: {
|
||||
enabled: true,
|
||||
account: "+15551234567",
|
||||
cliPath: "signal-cli",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15557654321"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What it is
|
||||
- Signal channel via `signal-cli` (not embedded libsignal).
|
||||
- Deterministic routing: replies always go back to Signal.
|
||||
- DMs share the agent's main session; groups are isolated (`agent:<agentId>:signal:group:<groupId>`).
|
||||
|
||||
## Config writes
|
||||
By default, Signal is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
|
||||
|
||||
Disable with:
|
||||
```json5
|
||||
{
|
||||
channels: { signal: { configWrites: false } }
|
||||
}
|
||||
```
|
||||
|
||||
## The number model (important)
|
||||
- The gateway connects to a **Signal device** (the `signal-cli` account).
|
||||
- If you run the bot on **your personal Signal account**, it will ignore your own messages (loop protection).
|
||||
- For "I text the bot and it replies," use a **separate bot number**.
|
||||
|
||||
## Setup (fast path)
|
||||
1) Install `signal-cli` (Java required).
|
||||
2) Link a bot account:
|
||||
- `signal-cli link -n "Clawdbot"` then scan the QR in Signal.
|
||||
3) Configure Signal and start the gateway.
|
||||
|
||||
Example:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
signal: {
|
||||
enabled: true,
|
||||
account: "+15551234567",
|
||||
cliPath: "signal-cli",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15557654321"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
||||
|
||||
## Access control (DMs + groups)
|
||||
DMs:
|
||||
- Default: `channels.signal.dmPolicy = "pairing"`.
|
||||
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
|
||||
- Approve via:
|
||||
- `clawdbot pairing list signal`
|
||||
- `clawdbot pairing approve signal <CODE>`
|
||||
- Pairing is the default token exchange for Signal DMs. Details: [Pairing](/start/pairing)
|
||||
- UUID-only senders (from `sourceUuid`) are stored as `uuid:<id>` in `channels.signal.allowFrom`.
|
||||
|
||||
Groups:
|
||||
- `channels.signal.groupPolicy = open | allowlist | disabled`.
|
||||
- `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
|
||||
|
||||
## How it works (behavior)
|
||||
- `signal-cli` runs as a daemon; the gateway reads events via SSE.
|
||||
- Inbound messages are normalized into the shared channel envelope.
|
||||
- Replies always route back to the same number or group.
|
||||
|
||||
## Media + limits
|
||||
- Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
|
||||
- Attachments supported (base64 fetched from `signal-cli`).
|
||||
- Default media cap: `channels.signal.mediaMaxMb` (default 8).
|
||||
- Use `channels.signal.ignoreAttachments` to skip downloading media.
|
||||
- Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
|
||||
|
||||
## Delivery targets (CLI/cron)
|
||||
- DMs: `signal:+15551234567` (or plain E.164).
|
||||
- Groups: `signal:group:<groupId>`.
|
||||
- Usernames: `username:<name>` (if supported by your Signal account).
|
||||
|
||||
## Configuration reference (Signal)
|
||||
Full configuration: [Configuration](/gateway/configuration)
|
||||
|
||||
Provider options:
|
||||
- `channels.signal.enabled`: enable/disable channel startup.
|
||||
- `channels.signal.account`: E.164 for the bot account.
|
||||
- `channels.signal.cliPath`: path to `signal-cli`.
|
||||
- `channels.signal.httpUrl`: full daemon URL (overrides host/port).
|
||||
- `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080).
|
||||
- `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset).
|
||||
- `channels.signal.receiveMode`: `on-start | manual`.
|
||||
- `channels.signal.ignoreAttachments`: skip attachment downloads.
|
||||
- `channels.signal.ignoreStories`: ignore stories from the daemon.
|
||||
- `channels.signal.sendReadReceipts`: forward read receipts.
|
||||
- `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
|
||||
- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:<id>`). `open` requires `"*"`.
|
||||
- `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
|
||||
- `channels.signal.groupAllowFrom`: group sender allowlist.
|
||||
- `channels.signal.historyLimit`: max group messages to include as context (0 disables).
|
||||
- `channels.signal.textChunkLimit`: outbound chunk size (chars).
|
||||
- `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||
|
||||
Related global options:
|
||||
- `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions).
|
||||
- `messages.groupChat.mentionPatterns` (global fallback).
|
||||
- `messages.responsePrefix`.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
summary: "Channel-specific troubleshooting shortcuts (Discord/Telegram/WhatsApp)"
|
||||
read_when:
|
||||
- A channel connects but messages don’t flow
|
||||
- Investigating channel misconfiguration (intents, permissions, privacy mode)
|
||||
---
|
||||
# Channel troubleshooting
|
||||
|
||||
Start with:
|
||||
|
||||
```bash
|
||||
clawdbot doctor
|
||||
clawdbot channels status --probe
|
||||
```
|
||||
|
||||
`channels status --probe` prints warnings when it can detect common channel misconfigurations, and includes small live checks (credentials, some permissions/membership).
|
||||
|
||||
## Channels
|
||||
- Discord: [/channels/discord#troubleshooting](/channels/discord#troubleshooting)
|
||||
- Telegram: [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting)
|
||||
- WhatsApp: [/channels/whatsapp#troubleshooting-quick](/channels/whatsapp#troubleshooting-quick)
|
||||
+1
-1
@@ -8,7 +8,7 @@ read_when:
|
||||
|
||||
# Gateway CLI
|
||||
|
||||
The Gateway is Clawdbot’s WebSocket server (channels, nodes, sessions, hooks).
|
||||
The Gateway is Clawdbot’s WebSocket server (providers, nodes, sessions, hooks).
|
||||
|
||||
Subcommands in this page live under `clawdbot gateway …`.
|
||||
|
||||
|
||||
+41
-41
@@ -29,7 +29,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
|
||||
Clawdbot uses a lobster palette for CLI output.
|
||||
|
||||
- `accent` (#FF5A2D): headings, labels, primary highlights.
|
||||
- `accent` (#FF5A2D): headings, provider labels, primary highlights.
|
||||
- `accentBright` (#FF7A3D): command names, emphasis.
|
||||
- `accentDim` (#D14A22): secondary highlight text.
|
||||
- `info` (#FF8A5B): informational values.
|
||||
@@ -51,7 +51,7 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
reset
|
||||
uninstall
|
||||
update
|
||||
channels
|
||||
providers
|
||||
list
|
||||
status
|
||||
logs
|
||||
@@ -258,15 +258,15 @@ Options:
|
||||
- `--install-daemon`
|
||||
- `--no-install-daemon` (alias: `--skip-daemon`)
|
||||
- `--daemon-runtime <node|bun>`
|
||||
- `--skip-channels`
|
||||
- `--skip-providers`
|
||||
- `--skip-skills`
|
||||
- `--skip-health`
|
||||
- `--skip-ui`
|
||||
- `--node-manager <npm|pnpm|bun>` (pnpm recommended; bun not recommended for Gateway runtime)
|
||||
- `--node-manager <npm|pnpm|bun>`
|
||||
- `--json`
|
||||
|
||||
### `configure` / `config`
|
||||
Interactive configuration wizard (models, channels, skills, gateway).
|
||||
Interactive configuration wizard (models, providers, skills, gateway).
|
||||
|
||||
### `doctor`
|
||||
Health checks + quick fixes (config + gateway + legacy services).
|
||||
@@ -277,41 +277,41 @@ Options:
|
||||
- `--non-interactive`: skip prompts; apply safe migrations only.
|
||||
- `--deep`: scan system services for extra gateway installs.
|
||||
|
||||
## Channel helpers
|
||||
## Provider helpers
|
||||
|
||||
### `channels`
|
||||
Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams).
|
||||
### `providers`
|
||||
Manage chat provider accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams).
|
||||
|
||||
Subcommands:
|
||||
- `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included).
|
||||
- `channels status`: check gateway reachability and channel health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes).
|
||||
- Tip: `channels status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`).
|
||||
- `channels logs`: show recent channel logs from the gateway log file.
|
||||
- `channels add`: wizard-style setup when no flags are passed; flags switch to non-interactive mode.
|
||||
- `channels remove`: disable by default; pass `--delete` to remove config entries without prompts.
|
||||
- `channels login`: interactive channel login (WhatsApp Web only).
|
||||
- `channels logout`: log out of a channel session (if supported).
|
||||
- `providers list`: show configured chat providers and auth profiles (Claude Code + Codex CLI OAuth sync included).
|
||||
- `providers status`: check gateway reachability and provider health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes).
|
||||
- Tip: `providers status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`).
|
||||
- `providers logs`: show recent provider logs from the gateway log file.
|
||||
- `providers add`: wizard-style setup when no flags are passed; flags switch to non-interactive mode.
|
||||
- `providers remove`: disable by default; pass `--delete` to remove config entries without prompts.
|
||||
- `providers login`: interactive provider login (WhatsApp Web only).
|
||||
- `providers logout`: log out of a provider session (if supported).
|
||||
|
||||
Common options:
|
||||
- `--channel <name>`: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
|
||||
- `--account <id>`: channel account id (default `default`)
|
||||
- `--provider <name>`: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
|
||||
- `--account <id>`: provider account id (default `default`)
|
||||
- `--name <label>`: display name for the account
|
||||
|
||||
`channels login` options:
|
||||
- `--channel <channel>` (default `whatsapp`; supports `whatsapp`/`web`)
|
||||
`providers login` options:
|
||||
- `--provider <provider>` (default `whatsapp`; supports `whatsapp`/`web`)
|
||||
- `--account <id>`
|
||||
- `--verbose`
|
||||
|
||||
`channels logout` options:
|
||||
- `--channel <channel>` (default `whatsapp`)
|
||||
`providers logout` options:
|
||||
- `--provider <provider>` (default `whatsapp`)
|
||||
- `--account <id>`
|
||||
|
||||
`channels list` options:
|
||||
- `--no-usage`: skip model provider usage/quota snapshots (OAuth/API-backed only).
|
||||
`providers list` options:
|
||||
- `--no-usage`: skip provider usage/quota snapshots (OAuth/API-backed only).
|
||||
- `--json`: output JSON (includes usage unless `--no-usage` is set).
|
||||
|
||||
`channels logs` options:
|
||||
- `--channel <name|all>` (default `all`)
|
||||
`providers logs` options:
|
||||
- `--provider <name|all>` (default `all`)
|
||||
- `--lines <n>` (default `200`)
|
||||
- `--json`
|
||||
|
||||
@@ -325,10 +325,10 @@ More detail: [/concepts/oauth](/concepts/oauth)
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
clawdbot channels add --channel telegram --account alerts --name "Alerts Bot" --token $TELEGRAM_BOT_TOKEN
|
||||
clawdbot channels add --channel discord --account work --name "Work Bot" --token $DISCORD_BOT_TOKEN
|
||||
clawdbot channels remove --channel discord --account work --delete
|
||||
clawdbot channels status --probe
|
||||
clawdbot providers add --provider telegram --account alerts --name "Alerts Bot" --token $TELEGRAM_BOT_TOKEN
|
||||
clawdbot providers add --provider discord --account work --name "Work Bot" --token $DISCORD_BOT_TOKEN
|
||||
clawdbot providers remove --provider discord --account work --delete
|
||||
clawdbot providers status --probe
|
||||
clawdbot status --deep
|
||||
```
|
||||
|
||||
@@ -348,11 +348,11 @@ Options:
|
||||
Tip: use `npx clawdhub` to search, install, and sync skills.
|
||||
|
||||
### `pairing`
|
||||
Approve DM pairing requests across channels.
|
||||
Approve DM pairing requests across providers.
|
||||
|
||||
Subcommands:
|
||||
- `pairing list <channel> [--json]`
|
||||
- `pairing approve <channel> <code> [--notify]`
|
||||
- `pairing list <provider> [--json]`
|
||||
- `pairing approve <provider> <code> [--notify]`
|
||||
|
||||
### `hooks gmail`
|
||||
Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub).
|
||||
@@ -370,7 +370,7 @@ Options:
|
||||
## Messaging + agent
|
||||
|
||||
### `message`
|
||||
Unified outbound messaging + channel actions.
|
||||
Unified outbound messaging + provider actions.
|
||||
|
||||
See: [/cli/message](/cli/message)
|
||||
|
||||
@@ -387,7 +387,7 @@ Subcommands:
|
||||
|
||||
Examples:
|
||||
- `clawdbot message send --to +15555550123 --message "Hi"`
|
||||
- `clawdbot message poll --channel discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi`
|
||||
- `clawdbot message poll --provider discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi`
|
||||
|
||||
### `agent`
|
||||
Run one agent turn via the Gateway (or `--local` embedded).
|
||||
@@ -400,7 +400,7 @@ Options:
|
||||
- `--session-id <id>`
|
||||
- `--thinking <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
|
||||
- `--verbose <on|off>`
|
||||
- `--channel <whatsapp|telegram|discord|slack|signal|imessage>`
|
||||
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
|
||||
- `--local`
|
||||
- `--deliver`
|
||||
- `--json`
|
||||
@@ -423,11 +423,11 @@ Options:
|
||||
- `--workspace <dir>`
|
||||
- `--model <id>`
|
||||
- `--agent-dir <dir>`
|
||||
- `--bind <channel[:accountId]>` (repeatable)
|
||||
- `--bind <provider[:accountId]>` (repeatable)
|
||||
- `--non-interactive`
|
||||
- `--json`
|
||||
|
||||
Binding specs use `channel[:accountId]`. When `accountId` is omitted for WhatsApp, the default account id is used.
|
||||
Binding specs use `provider[:accountId]`. When `accountId` is omitted for WhatsApp, the default account id is used.
|
||||
|
||||
#### `agents delete <id>`
|
||||
Delete an agent and prune its workspace + state.
|
||||
@@ -442,8 +442,8 @@ Show linked session health and recent recipients.
|
||||
Options:
|
||||
- `--json`
|
||||
- `--all` (full diagnosis; read-only, pasteable)
|
||||
- `--deep` (probe channels)
|
||||
- `--usage` (show model provider usage/quota)
|
||||
- `--deep` (probe providers)
|
||||
- `--usage` (show provider usage/quota)
|
||||
- `--timeout <ms>`
|
||||
- `--verbose`
|
||||
- `--debug` (alias for `--verbose`)
|
||||
@@ -549,7 +549,7 @@ Notes:
|
||||
- `daemon status` supports `--no-probe`, `--deep`, and `--json` for scripting.
|
||||
- `daemon status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named Clawdbot services are treated as first-class and aren't flagged as "extra".
|
||||
- `daemon status` prints which config path the CLI uses vs which config the daemon likely uses (service env), plus the resolved probe target URL.
|
||||
- `daemon install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs).
|
||||
- `daemon install` defaults to Node runtime; use `--runtime bun` only when WhatsApp is disabled.
|
||||
- `daemon install` options: `--port`, `--runtime`, `--token`, `--force`.
|
||||
|
||||
### `logs`
|
||||
|
||||
+31
-31
@@ -1,13 +1,13 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot message` (send + channel actions)"
|
||||
summary: "CLI reference for `clawdbot message` (send + provider actions)"
|
||||
read_when:
|
||||
- Adding or modifying message CLI actions
|
||||
- Changing outbound channel behavior
|
||||
- Changing outbound provider behavior
|
||||
---
|
||||
|
||||
# `clawdbot message`
|
||||
|
||||
Single outbound command for sending messages and channel actions
|
||||
Single outbound command for sending messages and provider actions
|
||||
(Discord/Slack/Telegram/WhatsApp/Signal/iMessage/MS Teams).
|
||||
|
||||
## Usage
|
||||
@@ -16,9 +16,9 @@ Single outbound command for sending messages and channel actions
|
||||
clawdbot message <subcommand> [flags]
|
||||
```
|
||||
|
||||
Channel selection:
|
||||
- `--channel` required if more than one channel is configured.
|
||||
- If exactly one channel is configured, it becomes the default.
|
||||
Provider selection:
|
||||
- `--provider` required if more than one provider is configured.
|
||||
- If exactly one provider is configured, it becomes the default.
|
||||
- Values: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
|
||||
|
||||
Target formats (`--to`):
|
||||
@@ -32,7 +32,7 @@ Target formats (`--to`):
|
||||
|
||||
## Common flags
|
||||
|
||||
- `--channel <name>`
|
||||
- `--provider <name>`
|
||||
- `--account <id>`
|
||||
- `--json`
|
||||
- `--dry-run`
|
||||
@@ -43,82 +43,82 @@ Target formats (`--to`):
|
||||
### Core
|
||||
|
||||
- `send`
|
||||
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
|
||||
- Providers: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
|
||||
- Required: `--to`, `--message`
|
||||
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
|
||||
- Telegram only: `--buttons` (requires `"inlineButtons"` in `channels.telegram.capabilities` or `channels.telegram.accounts.<id>.capabilities`)
|
||||
- Telegram only: `--buttons` (requires `"inlineButtons"` in `telegram.capabilities` or `telegram.accounts.<id>.capabilities`)
|
||||
- Telegram only: `--thread-id` (forum topic id)
|
||||
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
|
||||
- WhatsApp only: `--gif-playback`
|
||||
|
||||
- `poll`
|
||||
- Channels: WhatsApp/Discord/MS Teams
|
||||
- Providers: WhatsApp/Discord/MS Teams
|
||||
- Required: `--to`, `--poll-question`, `--poll-option` (repeat)
|
||||
- Optional: `--poll-multi`
|
||||
- Discord only: `--poll-duration-hours`, `--message`
|
||||
|
||||
- `react`
|
||||
- Channels: Discord/Slack/Telegram/WhatsApp
|
||||
- Providers: Discord/Slack/Telegram/WhatsApp
|
||||
- Required: `--message-id`, `--to` or `--channel-id`
|
||||
- Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--channel-id`
|
||||
- Note: `--remove` requires `--emoji` (omit `--emoji` to clear own reactions where supported; see /tools/reactions)
|
||||
- WhatsApp only: `--participant`, `--from-me`
|
||||
|
||||
- `reactions`
|
||||
- Channels: Discord/Slack
|
||||
- Providers: Discord/Slack
|
||||
- Required: `--message-id`, `--to` or `--channel-id`
|
||||
- Optional: `--limit`, `--channel-id`
|
||||
|
||||
- `read`
|
||||
- Channels: Discord/Slack
|
||||
- Providers: Discord/Slack
|
||||
- Required: `--to` or `--channel-id`
|
||||
- Optional: `--limit`, `--before`, `--after`, `--channel-id`
|
||||
- Discord only: `--around`
|
||||
|
||||
- `edit`
|
||||
- Channels: Discord/Slack
|
||||
- Providers: Discord/Slack
|
||||
- Required: `--message-id`, `--message`, `--to` or `--channel-id`
|
||||
- Optional: `--channel-id`
|
||||
|
||||
- `delete`
|
||||
- Channels: Discord/Slack/Telegram
|
||||
- Providers: Discord/Slack
|
||||
- Required: `--message-id`, `--to` or `--channel-id`
|
||||
- Optional: `--channel-id`
|
||||
|
||||
- `pin` / `unpin`
|
||||
- Channels: Discord/Slack
|
||||
- Providers: Discord/Slack
|
||||
- Required: `--message-id`, `--to` or `--channel-id`
|
||||
- Optional: `--channel-id`
|
||||
|
||||
- `pins` (list)
|
||||
- Channels: Discord/Slack
|
||||
- Providers: Discord/Slack
|
||||
- Required: `--to` or `--channel-id`
|
||||
- Optional: `--channel-id`
|
||||
|
||||
- `permissions`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--to` or `--channel-id`
|
||||
- Optional: `--channel-id`
|
||||
|
||||
- `search`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--guild-id`, `--query`
|
||||
- Optional: `--channel-id`, `--channel-ids` (repeat), `--author-id`, `--author-ids` (repeat), `--limit`
|
||||
|
||||
### Threads
|
||||
|
||||
- `thread create`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--thread-name`, `--to` (channel id) or `--channel-id`
|
||||
- Optional: `--message-id`, `--auto-archive-min`
|
||||
|
||||
- `thread list`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--guild-id`
|
||||
- Optional: `--channel-id`, `--include-archived`, `--before`, `--limit`
|
||||
|
||||
- `thread reply`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--to` (thread id), `--message`
|
||||
- Optional: `--media`, `--reply-to`
|
||||
|
||||
@@ -129,19 +129,19 @@ Target formats (`--to`):
|
||||
- Slack: no extra flags
|
||||
|
||||
- `emoji upload`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--guild-id`, `--emoji-name`, `--media`
|
||||
- Optional: `--role-ids` (repeat)
|
||||
|
||||
### Stickers
|
||||
|
||||
- `sticker send`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--to`, `--sticker-id` (repeat)
|
||||
- Optional: `--message`
|
||||
|
||||
- `sticker upload`
|
||||
- Channels: Discord
|
||||
- Providers: Discord
|
||||
- Required: `--guild-id`, `--sticker-name`, `--sticker-desc`, `--sticker-tags`, `--media`
|
||||
|
||||
### Roles / Channels / Members / Voice
|
||||
@@ -170,13 +170,13 @@ Target formats (`--to`):
|
||||
|
||||
Send a Discord reply:
|
||||
```
|
||||
clawdbot message send --channel discord \
|
||||
clawdbot message send --provider discord \
|
||||
--to channel:123 --message "hi" --reply-to 456
|
||||
```
|
||||
|
||||
Create a Discord poll:
|
||||
```
|
||||
clawdbot message poll --channel discord \
|
||||
clawdbot message poll --provider discord \
|
||||
--to channel:123 \
|
||||
--poll-question "Snack?" \
|
||||
--poll-option Pizza --poll-option Sushi \
|
||||
@@ -185,13 +185,13 @@ clawdbot message poll --channel discord \
|
||||
|
||||
Send a Teams proactive message:
|
||||
```
|
||||
clawdbot message send --channel msteams \
|
||||
clawdbot message send --provider msteams \
|
||||
--to conversation:19:abc@thread.tacv2 --message "hi"
|
||||
```
|
||||
|
||||
Create a Teams poll:
|
||||
```
|
||||
clawdbot message poll --channel msteams \
|
||||
clawdbot message poll --provider msteams \
|
||||
--to conversation:19:abc@thread.tacv2 \
|
||||
--poll-question "Lunch?" \
|
||||
--poll-option Pizza --poll-option Sushi
|
||||
@@ -199,12 +199,12 @@ clawdbot message poll --channel msteams \
|
||||
|
||||
React in Slack:
|
||||
```
|
||||
clawdbot message react --channel slack \
|
||||
clawdbot message react --provider slack \
|
||||
--to C123 --message-id 456 --emoji "✅"
|
||||
```
|
||||
|
||||
Send Telegram inline buttons:
|
||||
```
|
||||
clawdbot message send --channel telegram --to @mychat --message "Choose:" \
|
||||
clawdbot message send --provider telegram --to @mychat --message "Choose:" \
|
||||
--buttons '[ [{"text":"Yes","callback_data":"cmd:yes"}], [{"text":"No","callback_data":"cmd:no"}] ]'
|
||||
```
|
||||
+2
-2
@@ -9,7 +9,7 @@ read_when:
|
||||
|
||||
Safely update a **source checkout** (git install) of Clawdbot.
|
||||
|
||||
If you installed via **npm/pnpm** (global install, no git metadata), use the package manager flow in [Updating](/install/updating).
|
||||
If you installed via **npm/pnpm/bun** (global install, no git metadata), use the package manager flow in [Updating](/install/updating).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -32,7 +32,7 @@ High-level:
|
||||
|
||||
1. Requires a clean worktree (no uncommitted changes).
|
||||
2. Fetches and rebases against `@{upstream}`.
|
||||
3. Installs deps (pnpm preferred; npm fallback).
|
||||
3. Installs deps (pnpm/bun/npm depending on the checkout).
|
||||
4. Builds + builds the Control UI.
|
||||
5. Runs `clawdbot doctor` as the final “safe update” check.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Short, exact flow of one agent run.
|
||||
- `assistant`: streamed deltas from pi-agent-core
|
||||
- `tool`: streamed tool events from pi-agent-core
|
||||
|
||||
## Chat channel handling
|
||||
## Chat provider handling
|
||||
- Assistant deltas are buffered into chat `delta` messages.
|
||||
- A chat `final` is emitted on **lifecycle end/error**.
|
||||
|
||||
|
||||
@@ -219,6 +219,6 @@ Suggested `.gitignore` starter:
|
||||
## Advanced notes
|
||||
|
||||
- Multi-agent routing can use different workspaces per agent. See
|
||||
[Channel routing](/concepts/channel-routing) for routing configuration.
|
||||
[Provider routing](/concepts/provider-routing) for routing configuration.
|
||||
- If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox
|
||||
workspaces under `agents.defaults.sandbox.workspaceRoot`.
|
||||
@@ -92,7 +92,7 @@ Tune the boundary via `agents.defaults.blockStreamingBreak` (`text_end` vs `mess
|
||||
Control soft block chunking with `agents.defaults.blockStreamingChunk` (defaults to
|
||||
800–1200 chars; prefers paragraph breaks, then newlines; sentences last).
|
||||
Coalesce streamed chunks with `agents.defaults.blockStreamingCoalesce` to reduce
|
||||
single-line spam (idle-based merging before send). Non-Telegram channels require
|
||||
single-line spam (idle-based merging before send). Non-Telegram providers require
|
||||
explicit `*.blockStreaming: true` to enable block replies.
|
||||
Verbose tool summaries are emitted at tool start (no debounce); Control UI
|
||||
streams tool output via agent events when available.
|
||||
@@ -102,7 +102,7 @@ More details: [Streaming + chunking](/concepts/streaming).
|
||||
|
||||
At minimum, set:
|
||||
- `agents.defaults.workspace`
|
||||
- `channels.whatsapp.allowFrom` (strongly recommended)
|
||||
- `whatsapp.allowFrom` (strongly recommended)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
---
|
||||
summary: "Context: what the model sees, how it is built, and how to inspect it"
|
||||
read_when:
|
||||
- You want to understand what “context” means in Clawdbot
|
||||
- You are debugging why the model “knows” something (or forgot it)
|
||||
- You want to reduce context overhead (/context, /status, /compact)
|
||||
---
|
||||
# Context
|
||||
|
||||
“Context” is **everything Clawdbot sends to the model for a run**. It is bounded by the model’s **context window** (token limit).
|
||||
|
||||
Beginner mental model:
|
||||
- **System prompt** (Clawdbot-built): rules, tools, skills list, time/runtime, and injected workspace files.
|
||||
- **Conversation history**: your messages + the assistant’s messages for this session.
|
||||
- **Tool calls/results + attachments**: command output, file reads, images/audio, etc.
|
||||
|
||||
Context is *not the same thing* as “memory”: memory can be stored on disk and reloaded later; context is what’s inside the model’s current window.
|
||||
|
||||
## Quick start (inspect context)
|
||||
|
||||
- `/status` → quick “how full is my window?” view + session settings.
|
||||
- `/context list` → what’s injected + rough sizes (per file + totals).
|
||||
- `/context detail` → deeper breakdown: per-file, per-tool schema sizes, per-skill entry sizes, and system prompt size.
|
||||
- `/cost on` → append per-reply usage line to normal replies.
|
||||
- `/compact` → summarize older history into a compact entry to free window space.
|
||||
|
||||
See also: [Slash commands](/tools/slash-commands), [Token use & costs](/token-use), [Compaction](/concepts/compaction).
|
||||
|
||||
## Example output
|
||||
|
||||
Values vary by model, provider, tool policy, and what’s in your workspace.
|
||||
|
||||
### `/context list`
|
||||
|
||||
```
|
||||
🧠 Context breakdown
|
||||
Workspace: <workspaceDir>
|
||||
Bootstrap max/file: 20,000 chars
|
||||
Sandbox: mode=non-main sandboxed=false
|
||||
System prompt (run): 38,412 chars (~9,603 tok) (Project Context 23,901 chars (~5,976 tok))
|
||||
|
||||
Injected workspace files:
|
||||
- AGENTS.md: OK | raw 1,742 chars (~436 tok) | injected 1,742 chars (~436 tok)
|
||||
- SOUL.md: OK | raw 912 chars (~228 tok) | injected 912 chars (~228 tok)
|
||||
- TOOLS.md: TRUNCATED | raw 54,210 chars (~13,553 tok) | injected 20,962 chars (~5,241 tok)
|
||||
- IDENTITY.md: OK | raw 211 chars (~53 tok) | injected 211 chars (~53 tok)
|
||||
- USER.md: OK | raw 388 chars (~97 tok) | injected 388 chars (~97 tok)
|
||||
- HEARTBEAT.md: MISSING | raw 0 | injected 0
|
||||
- BOOTSTRAP.md: OK | raw 0 chars (~0 tok) | injected 0 chars (~0 tok)
|
||||
|
||||
Skills list (system prompt text): 2,184 chars (~546 tok) (12 skills)
|
||||
Tools: read, edit, write, exec, process, browser, message, sessions_send, …
|
||||
Tool list (system prompt text): 1,032 chars (~258 tok)
|
||||
Tool schemas (JSON): 31,988 chars (~7,997 tok) (counts toward context; not shown as text)
|
||||
Tools: (same as above)
|
||||
|
||||
Session tokens (cached): 14,250 total / ctx=32,000
|
||||
```
|
||||
|
||||
### `/context detail`
|
||||
|
||||
```
|
||||
🧠 Context breakdown (detailed)
|
||||
…
|
||||
Top skills (prompt entry size):
|
||||
- frontend-design: 412 chars (~103 tok)
|
||||
- oracle: 401 chars (~101 tok)
|
||||
… (+10 more skills)
|
||||
|
||||
Top tools (schema size):
|
||||
- browser: 9,812 chars (~2,453 tok)
|
||||
- exec: 6,240 chars (~1,560 tok)
|
||||
… (+N more tools)
|
||||
```
|
||||
|
||||
## What counts toward the context window
|
||||
|
||||
Everything the model receives counts, including:
|
||||
- System prompt (all sections).
|
||||
- Conversation history.
|
||||
- Tool calls + tool results.
|
||||
- Attachments/transcripts (images/audio/files).
|
||||
- Compaction summaries and pruning artifacts.
|
||||
- Provider “wrappers” or hidden headers (not visible, still counted).
|
||||
|
||||
## How Clawdbot builds the system prompt
|
||||
|
||||
The system prompt is **Clawdbot-owned** and rebuilt each run. It includes:
|
||||
- Tool list + short descriptions.
|
||||
- Skills list (metadata only; see below).
|
||||
- Workspace location.
|
||||
- Time (UTC + converted user time if configured).
|
||||
- Runtime metadata (host/OS/model/thinking).
|
||||
- Injected workspace bootstrap files under **Project Context**.
|
||||
|
||||
Full breakdown: [System Prompt](/concepts/system-prompt).
|
||||
|
||||
## Injected workspace files (Project Context)
|
||||
|
||||
By default, Clawdbot injects a fixed set of workspace files (if present):
|
||||
- `AGENTS.md`
|
||||
- `SOUL.md`
|
||||
- `TOOLS.md`
|
||||
- `IDENTITY.md`
|
||||
- `USER.md`
|
||||
- `HEARTBEAT.md`
|
||||
- `BOOTSTRAP.md` (first-run only)
|
||||
|
||||
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
|
||||
|
||||
## Skills: what’s injected vs loaded on-demand
|
||||
|
||||
The system prompt includes a compact **skills list** (name + description + location). This list has real overhead.
|
||||
|
||||
Skill instructions are *not* included by default. The model is expected to `read` the skill’s `SKILL.md` **only when needed**.
|
||||
|
||||
## Tools: there are two costs
|
||||
|
||||
Tools affect context in two ways:
|
||||
1) **Tool list text** in the system prompt (what you see as “Tooling”).
|
||||
2) **Tool schemas** (JSON). These are sent to the model so it can call tools. They count toward context even though you don’t see them as plain text.
|
||||
|
||||
`/context detail` breaks down the biggest tool schemas so you can see what dominates.
|
||||
|
||||
## Commands, directives, and “inline shortcuts”
|
||||
|
||||
Slash commands are handled by the Gateway. There are a few different behaviors:
|
||||
- **Standalone commands**: a message that is only `/...` runs as a command.
|
||||
- **Directives**: `/think`, `/verbose`, `/reasoning`, `/elevated`, `/model`, `/queue` are stripped before the model sees the message.
|
||||
- Directive-only messages persist session settings.
|
||||
- Inline directives in a normal message act as per-message hints.
|
||||
- **Inline shortcuts** (allowlisted senders only): certain `/...` tokens inside a normal message can run immediately (example: “hey /status”), and are stripped before the model sees the remaining text.
|
||||
|
||||
Details: [Slash commands](/tools/slash-commands).
|
||||
|
||||
## Sessions, compaction, and pruning (what persists)
|
||||
|
||||
What persists across messages depends on the mechanism:
|
||||
- **Normal history** persists in the session transcript until compacted/pruned by policy.
|
||||
- **Compaction** persists a summary into the transcript and keeps recent messages intact.
|
||||
- **Pruning** removes old tool results from the *in-memory* prompt for a run, but does not rewrite the transcript.
|
||||
|
||||
Docs: [Session](/concepts/session), [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning).
|
||||
|
||||
## What `/context` actually reports
|
||||
|
||||
`/context` prefers the latest **run-built** system prompt report when available:
|
||||
- `System prompt (run)` = captured from the last embedded (tool-capable) run and persisted in the session store.
|
||||
- `System prompt (estimate)` = computed on the fly when no run report exists (or when running via a CLI backend that doesn’t generate the report).
|
||||
|
||||
Either way, it reports sizes and top contributors; it does **not** dump the full system prompt or tool schemas.
|
||||
|
||||
@@ -3,42 +3,42 @@ summary: "Behavior and config for WhatsApp group message handling (mentionPatter
|
||||
read_when:
|
||||
- Changing group message rules or mentions
|
||||
---
|
||||
# Group messages (WhatsApp web channel)
|
||||
# Group messages (web provider)
|
||||
|
||||
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
|
||||
|
||||
Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback).
|
||||
|
||||
## What’s implemented (2025-12-03)
|
||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||
- Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
|
||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`whatsapp.groups`) and overridden per group via `/activation`. When `whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||
- Group policy: `whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `whatsapp.groupAllowFrom` (fallback: explicit `whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
|
||||
- Per-group sessions: session keys look like `agent:<agentId>:whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
|
||||
- Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`.
|
||||
- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking.
|
||||
- Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger.
|
||||
- Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "<subject>". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context.` If metadata isn’t available we still tell the agent it’s a group chat.
|
||||
|
||||
## Config example (WhatsApp)
|
||||
## Config for Clawd UK (+447700900123)
|
||||
Add a `groupChat` block to `~/.clawdbot/clawdbot.json` so display-name pings work even when WhatsApp strips the visual `@` in the text body:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"*": { requireMention: true }
|
||||
}
|
||||
"whatsapp": {
|
||||
"groups": {
|
||||
"*": { "requireMention": true }
|
||||
}
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
id: "main",
|
||||
groupChat: {
|
||||
historyLimit: 50,
|
||||
mentionPatterns: [
|
||||
"id": "main",
|
||||
"groupChat": {
|
||||
"historyLimit": 50,
|
||||
"mentionPatterns": [
|
||||
"@?clawd",
|
||||
"@?clawd\\s*uk",
|
||||
"@?clawdbot",
|
||||
"\\+?15555550123"
|
||||
"\\+?447700900123"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,8 @@ Add a `groupChat` block to `~/.clawdbot/clawdbot.json` so display-name pings wor
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The regexes are case-insensitive; they cover a display-name ping like `@clawdbot` and the raw number with or without `+`/spaces.
|
||||
- WhatsApp still sends canonical mentions via `mentionedJids` when someone taps the contact, so the number fallback is rarely needed but is a useful safety net.
|
||||
- The regexes are case-insensitive; they cover `@clawd`, `@clawd uk`, `clawdbot`, and the raw number with or without `+`/spaces.
|
||||
- WhatsApp still sends canonical mentions via `mentionedJids` when someone taps the contact, so the number fallback is rarely needed but is a good safety net.
|
||||
|
||||
### Activation command (owner-only)
|
||||
|
||||
@@ -57,11 +57,11 @@ Use the group chat command:
|
||||
- `/activation mention`
|
||||
- `/activation always`
|
||||
|
||||
Only the owner number (from `channels.whatsapp.allowFrom`, or the bot’s own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode.
|
||||
Only the owner number (from `whatsapp.allowFrom`, or the bot’s own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode.
|
||||
|
||||
## How to use
|
||||
1) Add your WhatsApp account (the one running Clawdbot) to the group.
|
||||
2) Say `@clawdbot …` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`.
|
||||
1) Add Clawd UK (`+447700900123`) to the group.
|
||||
2) Say `@clawd …` (or `@clawd uk`, `@clawdbot`, or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`.
|
||||
3) The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person.
|
||||
4) Session-level directives (`/verbose on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group’s session; send them as standalone messages so they register. Your personal DM session remains independent.
|
||||
|
||||
|
||||
+66
-76
@@ -41,51 +41,49 @@ If you want...
|
||||
| Only you can trigger in groups | `groupPolicy: "allowlist"`, `groupAllowFrom: ["+1555..."]` |
|
||||
|
||||
## Session keys
|
||||
- Group sessions use `agent:<agentId>:<channel>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
|
||||
- Group sessions use `agent:<agentId>:<provider>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
|
||||
- Telegram forum topics add `:topic:<threadId>` to the group id so each topic has its own session.
|
||||
- Direct chats use the main session (or per-sender if configured).
|
||||
- Heartbeats are skipped for group sessions.
|
||||
|
||||
## Display labels
|
||||
- UI labels use `displayName` when available, formatted as `<channel>:<token>`.
|
||||
- UI labels use `displayName` when available, formatted as `<provider>:<token>`.
|
||||
- `#room` is reserved for rooms/channels; group chats use `g-<slug>` (lowercase, spaces -> `-`, keep `#@+._-`).
|
||||
|
||||
## Group policy
|
||||
Control how group/room messages are handled per channel:
|
||||
Control how group/room messages are handled per provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
telegram: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["123456789", "@username"]
|
||||
},
|
||||
signal: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
imessage: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["chat_id:123"]
|
||||
},
|
||||
msteams: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["user@org.com"]
|
||||
},
|
||||
discord: {
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"GUILD_ID": { channels: { help: { allow: true } } }
|
||||
}
|
||||
},
|
||||
slack: {
|
||||
groupPolicy: "allowlist",
|
||||
channels: { "#general": { allow: true } }
|
||||
whatsapp: {
|
||||
groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
telegram: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["123456789", "@username"]
|
||||
},
|
||||
signal: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
imessage: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["chat_id:123"]
|
||||
},
|
||||
msteams: {
|
||||
groupPolicy: "disabled",
|
||||
groupAllowFrom: ["user@org.com"]
|
||||
},
|
||||
discord: {
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"GUILD_ID": { channels: { help: { allow: true } } }
|
||||
}
|
||||
},
|
||||
slack: {
|
||||
groupPolicy: "allowlist",
|
||||
channels: { "#general": { allow: true } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -99,15 +97,15 @@ Control how group/room messages are handled per channel:
|
||||
Notes:
|
||||
- `groupPolicy` is separate from mention-gating (which requires @mentions).
|
||||
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use `groupAllowFrom` (fallback: explicit `allowFrom`).
|
||||
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
|
||||
- Slack: allowlist uses `channels.slack.channels`.
|
||||
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
|
||||
- Discord: allowlist uses `discord.guilds.<id>.channels`.
|
||||
- Slack: allowlist uses `slack.channels`.
|
||||
- Group DMs are controlled separately (`discord.dm.*`, `slack.dm.*`).
|
||||
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
|
||||
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
|
||||
|
||||
Quick mental model (evaluation order for group messages):
|
||||
1) `groupPolicy` (open/disabled/allowlist)
|
||||
2) group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist)
|
||||
2) group allowlists (`*.groups`, `*.groupAllowFrom`, provider-specific allowlist)
|
||||
3) mention gating (`requireMention`, `/activation`)
|
||||
|
||||
## Mention gating (default)
|
||||
@@ -115,24 +113,22 @@ Group messages require a mention unless overridden per group. Defaults live per
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"123@g.us": { requireMention: false }
|
||||
}
|
||||
},
|
||||
telegram: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"123456789": { requireMention: false }
|
||||
}
|
||||
},
|
||||
imessage: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"123": { requireMention: false }
|
||||
}
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"123@g.us": { requireMention: false }
|
||||
}
|
||||
},
|
||||
telegram: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"123456789": { requireMention: false }
|
||||
}
|
||||
},
|
||||
imessage: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"123": { requireMention: false }
|
||||
}
|
||||
},
|
||||
agents: {
|
||||
@@ -154,30 +150,28 @@ Notes:
|
||||
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
|
||||
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
|
||||
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
||||
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
|
||||
- Group history context is wrapped uniformly across channels; use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
||||
- Discord defaults live in `discord.guilds."*"` (overridable per guild/channel).
|
||||
- Group history context is wrapped uniformly across providers; use `messages.groupChat.historyLimit` for the global default and `<provider>.historyLimit` (or `<provider>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
||||
|
||||
## Group allowlists
|
||||
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
||||
When `whatsapp.groups`, `telegram.groups`, or `imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
||||
|
||||
Common intents (copy/paste):
|
||||
|
||||
1) Disable all group replies
|
||||
```json5
|
||||
{
|
||||
channels: { whatsapp: { groupPolicy: "disabled" } }
|
||||
whatsapp: { groupPolicy: "disabled" }
|
||||
}
|
||||
```
|
||||
|
||||
2) Allow only specific groups (WhatsApp)
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"123@g.us": { requireMention: true },
|
||||
"456@g.us": { requireMention: false }
|
||||
}
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"123@g.us": { requireMention: true },
|
||||
"456@g.us": { requireMention: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,10 +180,8 @@ Common intents (copy/paste):
|
||||
3) Allow all groups but require mention (explicit)
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
whatsapp: {
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -197,12 +189,10 @@ Common intents (copy/paste):
|
||||
4) Only the owner can trigger in groups (WhatsApp)
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15551234567"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
whatsapp: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15551234567"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -212,7 +202,7 @@ Group owners can toggle per-group activation:
|
||||
- `/activation mention`
|
||||
- `/activation always`
|
||||
|
||||
Owner is determined by `channels.whatsapp.allowFrom` (or the bot’s self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
|
||||
Owner is determined by `whatsapp.allowFrom` (or the bot’s self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
|
||||
|
||||
## Context fields
|
||||
Group inbound payloads set:
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
---
|
||||
summary: "Markdown formatting pipeline for outbound channels"
|
||||
read_when:
|
||||
- You are changing markdown formatting or chunking for outbound channels
|
||||
- You are adding a new channel formatter or style mapping
|
||||
- You are debugging formatting regressions across channels
|
||||
---
|
||||
# Markdown formatting
|
||||
|
||||
Clawdbot formats outbound Markdown by converting it into a shared intermediate
|
||||
representation (IR) before rendering channel-specific output. The IR keeps the
|
||||
source text intact while carrying style/link spans so chunking and rendering can
|
||||
stay consistent across channels.
|
||||
|
||||
## Goals
|
||||
|
||||
- **Consistency:** one parse step, multiple renderers.
|
||||
- **Safe chunking:** split text before rendering so inline formatting never
|
||||
breaks across chunks.
|
||||
- **Channel fit:** map the same IR to Slack mrkdwn, Telegram HTML, and Signal
|
||||
style ranges without re-parsing Markdown.
|
||||
|
||||
## Pipeline
|
||||
|
||||
1. **Parse Markdown -> IR**
|
||||
- IR is plain text plus style spans (bold/italic/strike/code/spoiler) and link spans.
|
||||
- Offsets are UTF-16 code units so Signal style ranges align with its API.
|
||||
2. **Chunk IR (format-first)**
|
||||
- Chunking happens on the IR text before rendering.
|
||||
- Inline formatting does not split across chunks; spans are sliced per chunk.
|
||||
3. **Render per channel**
|
||||
- **Slack:** mrkdwn tokens (bold/italic/strike/code), links as `<url|label>`.
|
||||
- **Telegram:** HTML tags (`<b>`, `<i>`, `<s>`, `<code>`, `<pre><code>`, `<a href>`).
|
||||
- **Signal:** plain text + `text-style` ranges; links become `label (url)` when label differs.
|
||||
|
||||
## IR example
|
||||
|
||||
Input Markdown:
|
||||
|
||||
```markdown
|
||||
Hello **world** — see [docs](https://docs.clawd.bot).
|
||||
```
|
||||
|
||||
IR (schematic):
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Hello world — see docs.",
|
||||
"styles": [
|
||||
{ "start": 6, "end": 11, "style": "bold" }
|
||||
],
|
||||
"links": [
|
||||
{ "start": 19, "end": 23, "href": "https://docs.clawd.bot" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Where it is used
|
||||
|
||||
- Slack, Telegram, and Signal outbound adapters render from the IR.
|
||||
- Other channels (WhatsApp, iMessage, MS Teams, Discord) still use plain text or
|
||||
their own formatting rules.
|
||||
|
||||
## Chunking rules
|
||||
|
||||
- Chunk limits come from channel adapters/config and are applied to the IR text.
|
||||
- Code fences are preserved as a single block with a trailing newline so channels
|
||||
render them correctly.
|
||||
- List prefixes and blockquote prefixes are part of the IR text, so chunking
|
||||
does not split mid-prefix.
|
||||
- Inline styles (bold/italic/strike/inline-code/spoiler) are never split across
|
||||
chunks; the renderer reopens styles inside each chunk.
|
||||
|
||||
If you need more on chunking behavior across channels, see
|
||||
[Streaming + chunking](/concepts/streaming).
|
||||
|
||||
## Link policy
|
||||
|
||||
- **Slack:** `[label](url)` -> `<url|label>`; bare URLs remain bare. Autolink
|
||||
is disabled during parse to avoid double-linking.
|
||||
- **Telegram:** `[label](url)` -> `<a href="url">label</a>` (HTML parse mode).
|
||||
- **Signal:** `[label](url)` -> `label (url)` unless label matches the URL.
|
||||
|
||||
## Spoilers
|
||||
|
||||
Spoiler markers (`||spoiler||`) are parsed only for Signal, where they map to
|
||||
SPOILER style ranges. Other channels treat them as plain text.
|
||||
|
||||
## How to add or update a channel formatter
|
||||
|
||||
1. **Parse once:** use the shared `markdownToIR(...)` helper with channel-appropriate
|
||||
options (autolink, heading style, blockquote prefix).
|
||||
2. **Render:** implement a renderer with `renderMarkdownWithMarkers(...)` and a
|
||||
style marker map (or Signal style ranges).
|
||||
3. **Chunk:** call `chunkMarkdownIR(...)` before rendering; render each chunk.
|
||||
4. **Wire adapter:** update the channel outbound adapter to use the new chunker
|
||||
and renderer.
|
||||
5. **Test:** add or update format tests and an outbound delivery test if the
|
||||
channel uses chunking.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- Slack angle-bracket tokens (`<@U123>`, `<#C123>`, `<https://...>`) must be
|
||||
preserved; escape raw HTML safely.
|
||||
- Telegram HTML requires escaping text outside tags to avoid broken markup.
|
||||
- Signal style ranges depend on UTF-16 offsets; do not use code point offsets.
|
||||
- Preserve trailing newlines for fenced code blocks so closing markers land on
|
||||
their own line.
|
||||
+16
-16
@@ -17,20 +17,20 @@ Inbound message
|
||||
-> routing/bindings -> session key
|
||||
-> queue (if a run is active)
|
||||
-> agent run (streaming + tools)
|
||||
-> outbound replies (channel limits + chunking)
|
||||
-> outbound replies (provider limits + chunking)
|
||||
```
|
||||
|
||||
Key knobs live in configuration:
|
||||
- `messages.*` for prefixes, queueing, and group behavior.
|
||||
- `agents.defaults.*` for block streaming and chunking defaults.
|
||||
- Channel overrides (`channels.whatsapp.*`, `channels.telegram.*`, etc.) for caps and streaming toggles.
|
||||
- Provider overrides (`whatsapp.*`, `telegram.*`, etc.) for caps and streaming toggles.
|
||||
|
||||
See [Configuration](/gateway/configuration) for full schema.
|
||||
|
||||
## Inbound dedupe
|
||||
|
||||
Channels can redeliver the same message after reconnects. Clawdbot keeps a
|
||||
short-lived cache keyed by channel/account/peer/session/message id so duplicate
|
||||
Providers can redeliver the same message after reconnects. Clawdbot keeps a
|
||||
short-lived cache keyed by provider/account/peer/session/message id so duplicate
|
||||
deliveries do not trigger another agent run.
|
||||
|
||||
## Sessions and devices
|
||||
@@ -40,7 +40,7 @@ Sessions are owned by the gateway, not by clients.
|
||||
- Groups/channels get their own session keys.
|
||||
- The session store and transcripts live on the gateway host.
|
||||
|
||||
Multiple devices/channels can map to the same session, but history is not fully
|
||||
Multiple devices/providers can map to the same session, but history is not fully
|
||||
synced back to every client. Recommendation: use one primary device for long
|
||||
conversations to avoid divergent context. The Control UI and TUI always show the
|
||||
gateway-backed session transcript, so they are the source of truth.
|
||||
@@ -50,28 +50,28 @@ Details: [Session management](/concepts/session).
|
||||
## Inbound bodies and history context
|
||||
|
||||
Clawdbot separates the **prompt body** from the **command body**:
|
||||
- `Body`: prompt text sent to the agent. This may include channel envelopes and
|
||||
- `Body`: prompt text sent to the agent. This may include provider envelopes and
|
||||
optional history wrappers.
|
||||
- `CommandBody`: raw user text for directive/command parsing.
|
||||
- `RawBody`: legacy alias for `CommandBody` (kept for compatibility).
|
||||
|
||||
When a channel supplies history, it uses a shared wrapper:
|
||||
When a provider supplies history, it uses a shared wrapper:
|
||||
- `[Chat messages since your last reply - for context]`
|
||||
- `[Current message - respond to this]`
|
||||
|
||||
Directive stripping only applies to the **current message** section so history
|
||||
remains intact. Channels that wrap history should set `CommandBody` (or
|
||||
remains intact. Providers that wrap history should set `CommandBody` (or
|
||||
`RawBody`) to the original message text and keep `Body` as the combined prompt.
|
||||
History buffers are configurable via `messages.groupChat.historyLimit` (global
|
||||
default) and per-channel overrides like `channels.slack.historyLimit` or
|
||||
`channels.telegram.accounts.<id>.historyLimit` (set `0` to disable).
|
||||
default) and per-provider overrides like `slack.historyLimit` or
|
||||
`telegram.accounts.<id>.historyLimit` (set `0` to disable).
|
||||
|
||||
## Queueing and followups
|
||||
|
||||
If a run is already active, inbound messages can be queued, steered into the
|
||||
current run, or collected for a followup turn.
|
||||
|
||||
- Configure via `messages.queue` (and `messages.queue.byChannel`).
|
||||
- Configure via `messages.queue` (and `messages.queue.byProvider`).
|
||||
- Modes: `interrupt`, `steer`, `followup`, `collect`, plus backlog variants.
|
||||
|
||||
Details: [Queueing](/concepts/queue).
|
||||
@@ -79,7 +79,7 @@ Details: [Queueing](/concepts/queue).
|
||||
## Streaming, chunking, and batching
|
||||
|
||||
Block streaming sends partial replies as the model produces text blocks.
|
||||
Chunking respects channel text limits and avoids splitting fenced code.
|
||||
Chunking respects provider text limits and avoids splitting fenced code.
|
||||
|
||||
Key settings:
|
||||
- `agents.defaults.blockStreamingDefault` (`on|off`, default off)
|
||||
@@ -87,7 +87,7 @@ Key settings:
|
||||
- `agents.defaults.blockStreamingChunk` (`minChars|maxChars|breakPreference`)
|
||||
- `agents.defaults.blockStreamingCoalesce` (idle-based batching)
|
||||
- `agents.defaults.humanDelay` (human-like pause between block replies)
|
||||
- Channel overrides: `*.blockStreaming` and `*.blockStreamingCoalesce` (non-Telegram channels require explicit `*.blockStreaming: true`)
|
||||
- Provider overrides: `*.blockStreaming` and `*.blockStreamingCoalesce` (non-Telegram providers require explicit `*.blockStreaming: true`)
|
||||
|
||||
Details: [Streaming + chunking](/concepts/streaming).
|
||||
|
||||
@@ -103,7 +103,7 @@ Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/tok
|
||||
## Prefixes, threading, and replies
|
||||
|
||||
Outbound message formatting is centralized in `messages`:
|
||||
- `messages.responsePrefix` (outbound prefix) and `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
|
||||
- Reply threading via `replyToMode` and per-channel defaults
|
||||
- `messages.responsePrefix` (outbound prefix) and `whatsapp.messagePrefix` (WhatsApp inbound prefix)
|
||||
- Reply threading via `replyToMode` and per-provider defaults
|
||||
|
||||
Details: [Configuration](/gateway/configuration#messages) and channel docs.
|
||||
Details: [Configuration](/gateway/configuration#messages) and provider docs.
|
||||
@@ -6,7 +6,7 @@ read_when:
|
||||
---
|
||||
# Model providers
|
||||
|
||||
This page covers **LLM/model providers** (not chat channels like WhatsApp/Telegram).
|
||||
This page covers **LLM/model providers** (not chat providers like WhatsApp/Telegram).
|
||||
For model selection rules, see [/concepts/models](/concepts/models).
|
||||
|
||||
## Quick rules
|
||||
|
||||
@@ -152,7 +152,7 @@ JSON includes `auth.oauth` (warn window + profiles) and `auth.providers`
|
||||
(effective auth per provider).
|
||||
Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
|
||||
|
||||
Preferred Anthropic auth is the Claude Code CLI setup-token (run on the gateway host):
|
||||
Preferred Anthropic auth is the Claude CLI setup-token (run on the gateway host):
|
||||
|
||||
```bash
|
||||
claude setup-token
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Multi-agent routing: isolated agents, channel accounts, and bindings"
|
||||
summary: "Multi-agent routing: isolated agents, provider accounts, and bindings"
|
||||
title: Multi-Agent Routing
|
||||
read_when: "You want multiple isolated agents (workspaces + auth) in one gateway process."
|
||||
status: active
|
||||
@@ -7,7 +7,7 @@ status: active
|
||||
|
||||
# Multi-Agent Routing
|
||||
|
||||
Goal: multiple *isolated* agents (separate workspace + `agentDir` + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.
|
||||
Goal: multiple *isolated* agents (separate workspace + `agentDir` + sessions), plus multiple provider accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.
|
||||
|
||||
## What is “one agent”?
|
||||
|
||||
@@ -64,7 +64,7 @@ clawdbot agents list --bindings
|
||||
|
||||
With **multiple agents**, each `agentId` becomes a **fully isolated persona**:
|
||||
|
||||
- **Different phone numbers/accounts** (per channel `accountId`).
|
||||
- **Different phone numbers/accounts** (per provider `accountId`).
|
||||
- **Different personalities** (per-agent workspace files like `AGENTS.md` and `SOUL.md`).
|
||||
- **Separate auth + sessions** (no cross-talk unless explicitly enabled).
|
||||
|
||||
@@ -87,14 +87,12 @@ Example:
|
||||
]
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "alex", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
|
||||
{ agentId: "mia", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } }
|
||||
{ agentId: "alex", match: { provider: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
|
||||
{ agentId: "mia", match: { provider: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } }
|
||||
],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15551230001", "+15551230002"]
|
||||
}
|
||||
whatsapp: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15551230001", "+15551230002"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -110,21 +108,21 @@ Bindings are **deterministic** and **most-specific wins**:
|
||||
1. `peer` match (exact DM/group/channel id)
|
||||
2. `guildId` (Discord)
|
||||
3. `teamId` (Slack)
|
||||
4. `accountId` match for a channel
|
||||
5. channel-level match (`accountId: "*"`)
|
||||
4. `accountId` match for a provider
|
||||
5. provider-level match (`accountId: "*"`)
|
||||
6. fallback to default agent (`agents.list[].default`, else first list entry, default: `main`)
|
||||
|
||||
## Multiple accounts / phone numbers
|
||||
|
||||
Channels that support **multiple accounts** (e.g. WhatsApp) use `accountId` to identify
|
||||
Providers that support **multiple accounts** (e.g. WhatsApp) use `accountId` to identify
|
||||
each login. Each `accountId` can be routed to a different agent, so one server can host
|
||||
multiple phone numbers without mixing sessions.
|
||||
|
||||
## Concepts
|
||||
|
||||
- `agentId`: one “brain” (workspace, per-agent auth, per-agent session store).
|
||||
- `accountId`: one channel account instance (e.g. WhatsApp account `"personal"` vs `"biz"`).
|
||||
- `binding`: routes inbound messages to an `agentId` by `(channel, accountId, peer)` and optionally guild/team ids.
|
||||
- `accountId`: one provider account instance (e.g. WhatsApp account `"personal"` vs `"biz"`).
|
||||
- `binding`: routes inbound messages to an `agentId` by `(provider, accountId, peer)` and optionally guild/team ids.
|
||||
- Direct chats collapse to `agent:<agentId>:<mainKey>` (per-agent “main”; `session.mainKey`).
|
||||
|
||||
## Example: two WhatsApps → two agents
|
||||
@@ -153,14 +151,14 @@ multiple phone numbers without mixing sessions.
|
||||
|
||||
// Deterministic routing: first match wins (most-specific first).
|
||||
bindings: [
|
||||
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
|
||||
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
|
||||
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
|
||||
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } },
|
||||
|
||||
// Optional per-peer override (example: send a specific group to work agent).
|
||||
{
|
||||
agentId: "work",
|
||||
match: {
|
||||
channel: "whatsapp",
|
||||
provider: "whatsapp",
|
||||
accountId: "personal",
|
||||
peer: { kind: "group", id: "1203630...@g.us" },
|
||||
},
|
||||
@@ -175,17 +173,15 @@ multiple phone numbers without mixing sessions.
|
||||
},
|
||||
},
|
||||
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: {
|
||||
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/personal
|
||||
// authDir: "~/.clawdbot/credentials/whatsapp/personal",
|
||||
},
|
||||
biz: {
|
||||
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
|
||||
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
|
||||
},
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: {
|
||||
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/personal
|
||||
// authDir: "~/.clawdbot/credentials/whatsapp/personal",
|
||||
},
|
||||
biz: {
|
||||
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
|
||||
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -194,7 +190,7 @@ multiple phone numbers without mixing sessions.
|
||||
|
||||
## Example: WhatsApp daily chat + Telegram deep work
|
||||
|
||||
Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.
|
||||
Split by provider: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -215,17 +211,17 @@ Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opu
|
||||
]
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "chat", match: { channel: "whatsapp" } },
|
||||
{ agentId: "opus", match: { channel: "telegram" } }
|
||||
{ agentId: "chat", match: { provider: "whatsapp" } },
|
||||
{ agentId: "opus", match: { provider: "telegram" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`).
|
||||
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules.
|
||||
- If you have multiple accounts for a provider, add `accountId` to the binding (for example `{ provider: "whatsapp", accountId: "personal" }`).
|
||||
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over provider-wide rules.
|
||||
|
||||
## Example: same channel, one peer to Opus
|
||||
## Example: same provider, one peer to Opus
|
||||
|
||||
Keep WhatsApp on the fast agent, but route one DM to Opus:
|
||||
|
||||
@@ -238,13 +234,13 @@ Keep WhatsApp on the fast agent, but route one DM to Opus:
|
||||
]
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "opus", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551234567" } } },
|
||||
{ agentId: "chat", match: { channel: "whatsapp" } }
|
||||
{ agentId: "opus", match: { provider: "whatsapp", peer: { kind: "dm", id: "+15551234567" } } },
|
||||
{ agentId: "chat", match: { provider: "whatsapp" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Peer bindings always win, so keep them above the channel-wide rule.
|
||||
Peer bindings always win, so keep them above the provider-wide rule.
|
||||
|
||||
## Per-Agent Sandbox and Tool Configuration
|
||||
|
||||
|
||||
@@ -56,13 +56,13 @@ How to verify:
|
||||
|
||||
```bash
|
||||
clawdbot models status
|
||||
clawdbot channels list
|
||||
clawdbot providers list
|
||||
```
|
||||
|
||||
Or JSON:
|
||||
|
||||
```bash
|
||||
clawdbot channels list --json
|
||||
clawdbot providers list --json
|
||||
```
|
||||
|
||||
## OAuth exchange (how login works)
|
||||
@@ -148,7 +148,7 @@ Example (session override):
|
||||
- `/model Opus@anthropic:work`
|
||||
|
||||
How to see what profile IDs exist:
|
||||
- `clawdbot channels list --json` (shows `auth[]`)
|
||||
- `clawdbot providers list --json` (shows `auth[]`)
|
||||
|
||||
Related docs:
|
||||
- [/concepts/model-failover](/concepts/model-failover) (rotation + cooldown rules)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
summary: "Routing rules per channel (WhatsApp, Telegram, Discord, Slack) and shared context"
|
||||
summary: "Routing rules per provider (WhatsApp, Telegram, Discord, Slack) and shared context"
|
||||
read_when:
|
||||
- Changing channel routing or inbox behavior
|
||||
- Changing provider routing or inbox behavior
|
||||
---
|
||||
# Channels & routing
|
||||
# Providers & routing
|
||||
|
||||
|
||||
Clawdbot routes replies **back to the channel where a message came from**. The
|
||||
model does not choose a channel; routing is deterministic and controlled by the
|
||||
Clawdbot routes replies **back to the provider where a message came from**. The
|
||||
model does not choose a provider; routing is deterministic and controlled by the
|
||||
host configuration.
|
||||
|
||||
## Key terms
|
||||
|
||||
- **Channel**: `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `webchat`.
|
||||
- **AccountId**: per‑channel account instance (when supported).
|
||||
- **Provider**: `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `webchat`.
|
||||
- **AccountId**: per‑provider account instance (when supported).
|
||||
- **AgentId**: an isolated workspace + session store (“brain”).
|
||||
- **SessionKey**: the bucket key used to store context and control concurrency.
|
||||
|
||||
@@ -23,10 +23,10 @@ Direct messages collapse to the agent’s **main** session:
|
||||
|
||||
- `agent:<agentId>:<mainKey>` (default: `agent:main:main`)
|
||||
|
||||
Groups and channels remain isolated per channel:
|
||||
Groups and channels remain isolated per provider:
|
||||
|
||||
- Groups: `agent:<agentId>:<channel>:group:<id>`
|
||||
- Channels/rooms: `agent:<agentId>:<channel>:channel:<id>`
|
||||
- Groups: `agent:<agentId>:<provider>:group:<id>`
|
||||
- Channels/rooms: `agent:<agentId>:<provider>:channel:<id>`
|
||||
|
||||
Threads:
|
||||
|
||||
@@ -45,8 +45,8 @@ Routing picks **one agent** for each inbound message:
|
||||
1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`).
|
||||
2. **Guild match** (Discord) via `guildId`.
|
||||
3. **Team match** (Slack) via `teamId`.
|
||||
4. **Account match** (`accountId` on the channel).
|
||||
5. **Channel match** (any account on that channel).
|
||||
4. **Account match** (`accountId` on the provider).
|
||||
5. **Provider match** (any account on that provider).
|
||||
6. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`).
|
||||
|
||||
The matched agent determines which workspace and session store are used.
|
||||
@@ -72,7 +72,7 @@ See: [Broadcast Groups](/broadcast-groups).
|
||||
## Config overview
|
||||
|
||||
- `agents.list`: named agent definitions (workspace, model, etc.).
|
||||
- `bindings`: map inbound channels/accounts/peers to agents.
|
||||
- `bindings`: map inbound providers/accounts/peers to agents.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -84,8 +84,8 @@ Example:
|
||||
]
|
||||
},
|
||||
bindings: [
|
||||
{ match: { channel: "slack", teamId: "T123" }, agentId: "support" },
|
||||
{ match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }
|
||||
{ match: { provider: "slack", teamId: "T123" }, agentId: "support" },
|
||||
{ match: { provider: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -102,7 +102,7 @@ You can override the store path via `session.store` and `{agentId}` templating.
|
||||
## WebChat behavior
|
||||
|
||||
WebChat attaches to the **selected agent** and defaults to the agent’s main
|
||||
session. Because of this, WebChat lets you see cross‑channel context for that
|
||||
session. Because of this, WebChat lets you see cross‑provider context for that
|
||||
agent in one place.
|
||||
|
||||
## Reply context
|
||||
@@ -111,4 +111,4 @@ Inbound replies include:
|
||||
- `ReplyToId`, `ReplyToBody`, and `ReplyToSender` when available.
|
||||
- Quoted context is appended to `Body` as a `[Replying to ...]` block.
|
||||
|
||||
This is consistent across channels.
|
||||
This is consistent across providers.
|
||||
@@ -18,7 +18,7 @@ We now serialize command-based auto-replies (WhatsApp Web listener) through a ti
|
||||
- When verbose logging is enabled, queued commands emit a short notice if they waited more than ~2s before starting.
|
||||
- Typing indicators (`onReplyStart`) still fire immediately on enqueue so user experience is unchanged while we wait our turn.
|
||||
|
||||
## Queue modes (per channel)
|
||||
## Queue modes (per provider)
|
||||
Inbound messages can steer the current run, wait for a followup turn, or do both:
|
||||
- `steer`: inject immediately into the current run (cancels pending tool calls after the next tool boundary). If not streaming, falls back to followup.
|
||||
- `followup`: enqueue for the next agent turn after the current run ends.
|
||||
@@ -30,12 +30,12 @@ Inbound messages can steer the current run, wait for a followup turn, or do both
|
||||
Steer-backlog means you can get a followup response after the steered run, so
|
||||
streaming surfaces can look like duplicates. Prefer `collect`/`steer` if you want
|
||||
one response per inbound message.
|
||||
Send `/queue collect` as a standalone command (per-session) or set `messages.queue.byChannel.discord: "collect"`.
|
||||
Send `/queue collect` as a standalone command (per-session) or set `messages.queue.byProvider.discord: "collect"`.
|
||||
|
||||
Defaults (when unset in config):
|
||||
- All surfaces → `collect`
|
||||
|
||||
Configure globally or per channel via `messages.queue`:
|
||||
Configure globally or per provider via `messages.queue`:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -45,7 +45,7 @@ Configure globally or per channel via `messages.queue`:
|
||||
debounceMs: 1000,
|
||||
cap: 20,
|
||||
drop: "summarize",
|
||||
byChannel: { discord: "collect" }
|
||||
byProvider: { discord: "collect" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-16
@@ -34,22 +34,20 @@ Set retry policy per provider in `~/.clawdbot/clawdbot.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
retry: {
|
||||
attempts: 3,
|
||||
minDelayMs: 400,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
}
|
||||
},
|
||||
discord: {
|
||||
retry: {
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
}
|
||||
telegram: {
|
||||
retry: {
|
||||
attempts: 3,
|
||||
minDelayMs: 400,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
}
|
||||
},
|
||||
discord: {
|
||||
retry: {
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history,
|
||||
|
||||
## Key Model
|
||||
- Main direct chat bucket is always the literal key `"main"` (resolved to the current agent’s main key).
|
||||
- Group chats use `agent:<agentId>:<channel>:group:<id>` or `agent:<agentId>:<channel>:channel:<id>` (pass the full key).
|
||||
- Group chats use `agent:<agentId>:<provider>:group:<id>` or `agent:<agentId>:<provider>:channel:<id>` (pass the full key).
|
||||
- Cron jobs use `cron:<job.id>`.
|
||||
- Hooks use `hook:<uuid>` unless explicitly set.
|
||||
- Node bridge uses `node-<nodeId>` unless explicitly set.
|
||||
@@ -40,14 +40,14 @@ Behavior:
|
||||
Row shape (JSON):
|
||||
- `key`: session key (string)
|
||||
- `kind`: `main | group | cron | hook | node | other`
|
||||
- `channel`: `whatsapp | telegram | discord | signal | imessage | webchat | internal | unknown`
|
||||
- `provider`: `whatsapp | telegram | discord | signal | imessage | webchat | internal | unknown`
|
||||
- `displayName` (group display label if available)
|
||||
- `updatedAt` (ms)
|
||||
- `sessionId`
|
||||
- `model`, `contextTokens`, `totalTokens`
|
||||
- `thinkingLevel`, `verboseLevel`, `systemSent`, `abortedLastRun`
|
||||
- `sendPolicy` (session override if set)
|
||||
- `lastChannel`, `lastTo`
|
||||
- `lastProvider`, `lastTo`
|
||||
- `transcriptPath` (best-effort path derived from store dir + sessionId)
|
||||
- `messages?` (only when `messageLimit > 0`)
|
||||
|
||||
@@ -85,17 +85,17 @@ Behavior:
|
||||
- Max turns is `session.agentToAgent.maxPingPongTurns` (0–5, default 5).
|
||||
- Once the loop ends, Clawdbot runs the **agent‑to‑agent announce step** (target agent only):
|
||||
- Reply exactly `ANNOUNCE_SKIP` to stay silent.
|
||||
- Any other reply is sent to the target channel.
|
||||
- Any other reply is sent to the target provider.
|
||||
- Announce step includes the original request + round‑1 reply + latest ping‑pong reply.
|
||||
|
||||
## Channel Field
|
||||
- For groups, `channel` is the channel recorded on the session entry.
|
||||
- For direct chats, `channel` maps from `lastChannel`.
|
||||
- For cron/hook/node, `channel` is `internal`.
|
||||
- If missing, `channel` is `unknown`.
|
||||
## Provider Field
|
||||
- For groups, `provider` is the provider recorded on the session entry.
|
||||
- For direct chats, `provider` maps from `lastProvider`.
|
||||
- For cron/hook/node, `provider` is `internal`.
|
||||
- If missing, `provider` is `unknown`.
|
||||
|
||||
## Security / Send Policy
|
||||
Policy-based blocking by channel/chat type (not per session id).
|
||||
Policy-based blocking by provider/chat type (not per session id).
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -103,7 +103,7 @@ Policy-based blocking by channel/chat type (not per session id).
|
||||
"sendPolicy": {
|
||||
"rules": [
|
||||
{
|
||||
"match": { "channel": "discord", "chatType": "group" },
|
||||
"match": { "provider": "discord", "chatType": "group" },
|
||||
"action": "deny"
|
||||
}
|
||||
],
|
||||
@@ -122,7 +122,7 @@ Enforcement points:
|
||||
- auto-reply delivery logic
|
||||
|
||||
## sessions_spawn
|
||||
Spawn a sub-agent run in an isolated session and announce the result back to the requester chat channel.
|
||||
Spawn a sub-agent run in an isolated session and announce the result back to the requester chat provider.
|
||||
|
||||
Parameters:
|
||||
- `task` (required)
|
||||
@@ -143,7 +143,7 @@ Behavior:
|
||||
- Sub-agents default to the full tool set **minus session tools** (configurable via `tools.subagents.tools`).
|
||||
- Sub-agents are not allowed to call `sessions_spawn` (no sub-agent → sub-agent spawning).
|
||||
- Always non-blocking: returns `{ status: "accepted", runId, childSessionKey }` immediately.
|
||||
- After completion, Clawdbot runs a sub-agent **announce step** and posts the result to the requester chat channel.
|
||||
- After completion, Clawdbot runs a sub-agent **announce step** and posts the result to the requester chat provider.
|
||||
- Reply exactly `ANNOUNCE_SKIP` during the announce step to stay silent.
|
||||
- Sub-agent sessions are auto-archived after `agents.defaults.subagents.archiveAfterMinutes` (default: 60).
|
||||
- Announce replies include a stats line (runtime, tokens, sessionKey/sessionId, transcript path, and optional cost).
|
||||
|
||||
@@ -18,7 +18,7 @@ All session state is **owned by the gateway** (the “master” Clawdbot). UI cl
|
||||
- Store file: `~/.clawdbot/agents/<agentId>/sessions/sessions.json` (per agent).
|
||||
- Transcripts: `~/.clawdbot/agents/<agentId>/sessions/<SessionId>.jsonl` (Telegram topic sessions use `.../<SessionId>-topic-<threadId>.jsonl`).
|
||||
- The store is a map `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries is safe; they are recreated on demand.
|
||||
- Group entries may include `displayName`, `channel`, `subject`, `room`, and `space` to label sessions in UIs.
|
||||
- Group entries may include `displayName`, `provider`, `subject`, `room`, and `space` to label sessions in UIs.
|
||||
- Clawdbot does **not** read legacy Pi/Tau session folders.
|
||||
|
||||
## Session pruning
|
||||
@@ -33,11 +33,11 @@ the workspace is writable. See [Memory](/concepts/memory) and
|
||||
|
||||
## Mapping transports → session keys
|
||||
- Direct chats collapse to the per-agent primary key: `agent:<agentId>:<mainKey>`.
|
||||
- Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
|
||||
- Group chats isolate state: `agent:<agentId>:<channel>:group:<id>` (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
|
||||
- Multiple phone numbers and providers can map to the same agent main key; they act as transports into one conversation.
|
||||
- Group chats isolate state: `agent:<agentId>:<provider>:group:<id>` (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
|
||||
- Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
|
||||
- Legacy `group:<id>` keys are still recognized for migration.
|
||||
- Inbound contexts may still use `group:<id>`; the channel is inferred from `Provider` and normalized to the canonical `agent:<agentId>:<channel>:group:<id>` form.
|
||||
- Inbound contexts may still use `group:<id>`; the provider is inferred from `Provider` and normalized to the canonical `agent:<agentId>:<provider>:group:<id>` form.
|
||||
- Other sources:
|
||||
- Cron jobs: `cron:<job.id>`
|
||||
- Webhooks: `hook:<uuid>` (unless explicitly set by the hook)
|
||||
@@ -56,7 +56,7 @@ Block delivery for specific session types without listing individual ids.
|
||||
session: {
|
||||
sendPolicy: {
|
||||
rules: [
|
||||
{ action: "deny", match: { channel: "discord", chatType: "group" } },
|
||||
{ action: "deny", match: { provider: "discord", chatType: "group" } },
|
||||
{ action: "deny", match: { keyPrefix: "cron:" } }
|
||||
],
|
||||
default: "allow"
|
||||
@@ -90,7 +90,6 @@ Send these as standalone messages so they register.
|
||||
- `pnpm clawdbot sessions --json` — dumps every entry (filter with `--active <minutes>`).
|
||||
- `clawdbot gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access).
|
||||
- Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
|
||||
- Send `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors).
|
||||
- Send `/stop` as a standalone message to abort the current run.
|
||||
- Send `/compact` (optional instructions) as a standalone message to summarize older context and free up window space. See [/concepts/compaction](/concepts/compaction).
|
||||
- JSONL transcripts can be opened directly to review full turns.
|
||||
|
||||
+19
-22
@@ -1,19 +1,19 @@
|
||||
---
|
||||
summary: "Streaming + chunking behavior (block replies, draft streaming, limits)"
|
||||
read_when:
|
||||
- Explaining how streaming or chunking works on channels
|
||||
- Changing block streaming or channel chunking behavior
|
||||
- Explaining how streaming or chunking works on providers
|
||||
- Changing block streaming or provider chunking behavior
|
||||
- Debugging duplicate/early block replies or draft streaming
|
||||
---
|
||||
# Streaming + chunking
|
||||
|
||||
Clawdbot has two separate “streaming” layers:
|
||||
- **Block streaming (channels):** emit completed **blocks** as the assistant writes. These are normal channel messages (not token deltas).
|
||||
- **Block streaming (providers):** emit completed **blocks** as the assistant writes. These are normal provider messages (not token deltas).
|
||||
- **Token-ish streaming (Telegram only):** update a **draft bubble** with partial text while generating; final message is sent at the end.
|
||||
|
||||
There is **no real token streaming** to external channel messages today. Telegram draft streaming is the only partial-stream surface.
|
||||
There is **no real token streaming** to external provider messages today. Telegram draft streaming is the only partial-stream surface.
|
||||
|
||||
## Block streaming (channel messages)
|
||||
## Block streaming (provider messages)
|
||||
|
||||
Block streaming sends assistant output in coarse chunks as it becomes available.
|
||||
|
||||
@@ -24,21 +24,21 @@ Model output
|
||||
│ └─ chunker emits blocks as buffer grows
|
||||
└─ (blockStreamingBreak=message_end)
|
||||
└─ chunker flushes at message_end
|
||||
└─ channel send (block replies)
|
||||
└─ provider send (block replies)
|
||||
```
|
||||
Legend:
|
||||
- `text_delta/events`: model stream events (may be sparse for non-streaming models).
|
||||
- `chunker`: `EmbeddedBlockChunker` applying min/max bounds + break preference.
|
||||
- `channel send`: actual outbound messages (block replies).
|
||||
- `provider send`: actual outbound messages (block replies).
|
||||
|
||||
**Controls:**
|
||||
- `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default off).
|
||||
- Channel overrides: `*.blockStreaming` (and per-account variants) to force `"on"`/`"off"` per channel.
|
||||
- Provider overrides: `*.blockStreaming` (and per-account variants) to force `"on"`/`"off"` per provider.
|
||||
- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"`.
|
||||
- `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`.
|
||||
- `agents.defaults.blockStreamingCoalesce`: `{ minChars?, maxChars?, idleMs? }` (merge streamed blocks before send).
|
||||
- Channel hard cap: `*.textChunkLimit` (e.g., `channels.whatsapp.textChunkLimit`).
|
||||
- Discord soft cap: `channels.discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
|
||||
- Provider hard cap: `*.textChunkLimit` (e.g., `whatsapp.textChunkLimit`).
|
||||
- Discord soft cap: `discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
|
||||
|
||||
**Boundary semantics:**
|
||||
- `text_end`: stream blocks as soon as chunker emits; flush on each `text_end`.
|
||||
@@ -54,7 +54,7 @@ Block chunking is implemented by `EmbeddedBlockChunker`:
|
||||
- **Break preference:** `paragraph` → `newline` → `sentence` → `whitespace` → hard break.
|
||||
- **Code fences:** never split inside fences; when forced at `maxChars`, close + reopen the fence to keep Markdown valid.
|
||||
|
||||
`maxChars` is clamped to the channel `textChunkLimit`, so you can’t exceed per-channel caps.
|
||||
`maxChars` is clamped to the provider `textChunkLimit`, so you can’t exceed per-provider caps.
|
||||
|
||||
## Coalescing (merge streamed blocks)
|
||||
|
||||
@@ -68,7 +68,7 @@ progressive output.
|
||||
(final flush always sends remaining text).
|
||||
- Joiner is derived from `blockStreamingChunk.breakPreference`
|
||||
(`paragraph` → `\n\n`, `newline` → `\n`, `sentence` → space).
|
||||
- Channel overrides are available via `*.blockStreamingCoalesce` (including per-account configs).
|
||||
- Provider overrides are available via `*.blockStreamingCoalesce` (including per-account configs).
|
||||
- Default coalesce `minChars` is bumped to 1500 for Signal/Slack/Discord unless overridden.
|
||||
|
||||
## Human-like pacing between blocks
|
||||
@@ -84,27 +84,24 @@ more natural.
|
||||
## “Stream chunks or everything”
|
||||
|
||||
This maps to:
|
||||
- **Stream chunks:** `blockStreamingDefault: "on"` + `blockStreamingBreak: "text_end"` (emit as you go). Non-Telegram channels also need `*.blockStreaming: true`.
|
||||
- **Stream chunks:** `blockStreamingDefault: "on"` + `blockStreamingBreak: "text_end"` (emit as you go). Non-Telegram providers also need `*.blockStreaming: true`.
|
||||
- **Stream everything at end:** `blockStreamingBreak: "message_end"` (flush once, possibly multiple chunks if very long).
|
||||
- **No block streaming:** `blockStreamingDefault: "off"` (only final reply).
|
||||
|
||||
**Channel note:** For non-Telegram channels, block streaming is **off unless**
|
||||
**Provider note:** For non-Telegram providers, block streaming is **off unless**
|
||||
`*.blockStreaming` is explicitly set to `true`. Telegram can stream drafts
|
||||
(`channels.telegram.streamMode`) without block replies.
|
||||
|
||||
Config location reminder: the `blockStreaming*` defaults live under
|
||||
`agents.defaults`, not the root config.
|
||||
(`telegram.streamMode`) without block replies.
|
||||
|
||||
## Telegram draft streaming (token-ish)
|
||||
|
||||
Telegram is the only channel with draft streaming:
|
||||
Telegram is the only provider with draft streaming:
|
||||
- Uses Bot API `sendMessageDraft` in **private chats with topics**.
|
||||
- `channels.telegram.streamMode: "partial" | "block" | "off"`.
|
||||
- `telegram.streamMode: "partial" | "block" | "off"`.
|
||||
- `partial`: draft updates with the latest stream text.
|
||||
- `block`: draft updates in chunked blocks (same chunker rules).
|
||||
- `off`: no draft streaming.
|
||||
- Draft chunk config (only for `streamMode: "block"`): `channels.telegram.draftChunk` (defaults: `minChars: 200`, `maxChars: 800`).
|
||||
- Draft streaming is separate from block streaming; block replies are off by default and only enabled by `*.blockStreaming: true` on non-Telegram channels.
|
||||
- Draft chunk config (only for `streamMode: "block"`): `telegram.draftChunk` (defaults: `minChars: 200`, `maxChars: 800`).
|
||||
- Draft streaming is separate from block streaming; block replies are off by default and only enabled by `*.blockStreaming: true` on non-Telegram providers.
|
||||
- Final reply is still a normal message.
|
||||
- `/reasoning stream` writes reasoning into the draft bubble (Telegram only).
|
||||
|
||||
|
||||
@@ -42,8 +42,6 @@ Large files are truncated with a marker. The max per-file size is controlled by
|
||||
`agents.defaults.bootstrapMaxChars` (default: 20000). Missing files inject a
|
||||
short missing-file marker.
|
||||
|
||||
To inspect how much each injected file contributes (raw vs injected, truncation, plus tool schema overhead), use `/context list` or `/context detail`. See [Context](/concepts/context).
|
||||
|
||||
## Time handling
|
||||
|
||||
The Time line is compact and explicit:
|
||||
|
||||
@@ -21,7 +21,7 @@ The timestamp in the envelope is **always UTC**, with minutes precision.
|
||||
|
||||
## Tool payloads (raw provider data)
|
||||
|
||||
Tool calls (`channels.discord.readMessages`, `channels.slack.readMessages`, etc.) return **raw provider timestamps**.
|
||||
Tool calls (`discord.readMessages`, `slack.readMessages`, etc.) return **raw provider timestamps**.
|
||||
These are typically UTC ISO strings (Discord) or UTC epoch strings (Slack). We do not rewrite them.
|
||||
|
||||
## User timezone for the system prompt
|
||||
|
||||
@@ -5,7 +5,7 @@ read_when:
|
||||
---
|
||||
# Typing indicators
|
||||
|
||||
Typing indicators are sent to the chat channel while a run is active. Use
|
||||
Typing indicators are sent to the chat provider while a run is active. Use
|
||||
`agents.defaults.typingMode` to control **when** typing starts and `typingIntervalSeconds`
|
||||
to control **how often** it refreshes.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ read_when:
|
||||
- `/status` in chats: emoji‑rich status card with session tokens + estimated cost (API key only) and provider quota windows when available.
|
||||
- `/cost on|off` in chats: toggles per‑response usage lines (OAuth shows tokens only).
|
||||
- CLI: `clawdbot status --usage` prints a full per-provider breakdown.
|
||||
- CLI: `clawdbot channels list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||
- CLI: `clawdbot providers list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||
- macOS menu bar: “Usage” section under Context (only if available).
|
||||
|
||||
## Providers + credentials
|
||||
@@ -23,7 +23,6 @@ read_when:
|
||||
- **Gemini CLI**: OAuth tokens in auth profiles.
|
||||
- **Antigravity**: OAuth tokens in auth profiles.
|
||||
- **OpenAI Codex**: OAuth tokens in auth profiles (accountId used when present).
|
||||
- **MiniMax**: API key (coding plan key; `MINIMAX_CODE_PLAN_KEY` or `MINIMAX_API_KEY`); uses the 5‑hour coding plan window.
|
||||
- **z.ai**: API key via env/config/auth store.
|
||||
|
||||
Usage is hidden if no matching OAuth/API credentials exist.
|
||||
+22
-135
@@ -45,14 +45,6 @@
|
||||
"source": "/messages/",
|
||||
"destination": "/concepts/messages"
|
||||
},
|
||||
{
|
||||
"source": "/context",
|
||||
"destination": "/concepts/context"
|
||||
},
|
||||
{
|
||||
"source": "/context/",
|
||||
"destination": "/concepts/context"
|
||||
},
|
||||
{
|
||||
"source": "/minimax",
|
||||
"destination": "/providers/minimax"
|
||||
@@ -125,86 +117,6 @@
|
||||
"source": "/message/",
|
||||
"destination": "/cli/message"
|
||||
},
|
||||
{
|
||||
"source": "/providers/discord",
|
||||
"destination": "/channels/discord"
|
||||
},
|
||||
{
|
||||
"source": "/providers/discord/",
|
||||
"destination": "/channels/discord"
|
||||
},
|
||||
{
|
||||
"source": "/providers/grammy",
|
||||
"destination": "/channels/grammy"
|
||||
},
|
||||
{
|
||||
"source": "/providers/grammy/",
|
||||
"destination": "/channels/grammy"
|
||||
},
|
||||
{
|
||||
"source": "/providers/imessage",
|
||||
"destination": "/channels/imessage"
|
||||
},
|
||||
{
|
||||
"source": "/providers/imessage/",
|
||||
"destination": "/channels/imessage"
|
||||
},
|
||||
{
|
||||
"source": "/providers/location",
|
||||
"destination": "/channels/location"
|
||||
},
|
||||
{
|
||||
"source": "/providers/location/",
|
||||
"destination": "/channels/location"
|
||||
},
|
||||
{
|
||||
"source": "/providers/msteams",
|
||||
"destination": "/channels/msteams"
|
||||
},
|
||||
{
|
||||
"source": "/providers/msteams/",
|
||||
"destination": "/channels/msteams"
|
||||
},
|
||||
{
|
||||
"source": "/providers/signal",
|
||||
"destination": "/channels/signal"
|
||||
},
|
||||
{
|
||||
"source": "/providers/signal/",
|
||||
"destination": "/channels/signal"
|
||||
},
|
||||
{
|
||||
"source": "/providers/slack",
|
||||
"destination": "/channels/slack"
|
||||
},
|
||||
{
|
||||
"source": "/providers/slack/",
|
||||
"destination": "/channels/slack"
|
||||
},
|
||||
{
|
||||
"source": "/providers/telegram",
|
||||
"destination": "/channels/telegram"
|
||||
},
|
||||
{
|
||||
"source": "/providers/telegram/",
|
||||
"destination": "/channels/telegram"
|
||||
},
|
||||
{
|
||||
"source": "/providers/troubleshooting",
|
||||
"destination": "/channels/troubleshooting"
|
||||
},
|
||||
{
|
||||
"source": "/providers/troubleshooting/",
|
||||
"destination": "/channels/troubleshooting"
|
||||
},
|
||||
{
|
||||
"source": "/providers/whatsapp",
|
||||
"destination": "/channels/whatsapp"
|
||||
},
|
||||
{
|
||||
"source": "/providers/whatsapp/",
|
||||
"destination": "/channels/whatsapp"
|
||||
},
|
||||
{
|
||||
"source": "/sandbox",
|
||||
"destination": "/cli/sandbox"
|
||||
@@ -319,7 +231,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/discord",
|
||||
"destination": "/channels/discord"
|
||||
"destination": "/providers/discord"
|
||||
},
|
||||
{
|
||||
"source": "/discovery",
|
||||
@@ -355,7 +267,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/grammy",
|
||||
"destination": "/channels/grammy"
|
||||
"destination": "/providers/grammy"
|
||||
},
|
||||
{
|
||||
"source": "/group-messages",
|
||||
@@ -383,7 +295,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/imessage",
|
||||
"destination": "/channels/imessage"
|
||||
"destination": "/providers/imessage"
|
||||
},
|
||||
{
|
||||
"source": "/ios",
|
||||
@@ -395,7 +307,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/location",
|
||||
"destination": "/channels/location"
|
||||
"destination": "/providers/location"
|
||||
},
|
||||
{
|
||||
"source": "/location-command",
|
||||
@@ -539,23 +451,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/provider-routing",
|
||||
"destination": "/concepts/channel-routing"
|
||||
},
|
||||
{
|
||||
"source": "/concepts/provider-routing",
|
||||
"destination": "/concepts/channel-routing"
|
||||
},
|
||||
{
|
||||
"source": "/concepts/provider-routing/",
|
||||
"destination": "/concepts/channel-routing"
|
||||
},
|
||||
{
|
||||
"source": "/refactor/provider-plugin",
|
||||
"destination": "/refactor/channel-plugin"
|
||||
},
|
||||
{
|
||||
"source": "/refactor/provider-plugin/",
|
||||
"destination": "/refactor/channel-plugin"
|
||||
"destination": "/concepts/provider-routing"
|
||||
},
|
||||
{
|
||||
"source": "/queue",
|
||||
@@ -603,7 +499,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/signal",
|
||||
"destination": "/channels/signal"
|
||||
"destination": "/providers/signal"
|
||||
},
|
||||
{
|
||||
"source": "/skills",
|
||||
@@ -615,7 +511,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/slack",
|
||||
"destination": "/channels/slack"
|
||||
"destination": "/providers/slack"
|
||||
},
|
||||
{
|
||||
"source": "/slash-commands",
|
||||
@@ -635,7 +531,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/telegram",
|
||||
"destination": "/channels/telegram"
|
||||
"destination": "/providers/telegram"
|
||||
},
|
||||
{
|
||||
"source": "/templates/AGENTS",
|
||||
@@ -707,7 +603,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/whatsapp",
|
||||
"destination": "/channels/whatsapp"
|
||||
"destination": "/providers/whatsapp"
|
||||
},
|
||||
{
|
||||
"source": "/windows",
|
||||
@@ -773,7 +669,6 @@
|
||||
"concepts/agent",
|
||||
"concepts/agent-loop",
|
||||
"concepts/system-prompt",
|
||||
"concepts/context",
|
||||
"token-use",
|
||||
"concepts/oauth",
|
||||
"concepts/agent-workspace",
|
||||
@@ -785,7 +680,7 @@
|
||||
"concepts/sessions",
|
||||
"concepts/session-tool",
|
||||
"concepts/presence",
|
||||
"concepts/channel-routing",
|
||||
"concepts/provider-routing",
|
||||
"concepts/messages",
|
||||
"concepts/streaming",
|
||||
"concepts/groups",
|
||||
@@ -841,28 +736,9 @@
|
||||
"tui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Channels",
|
||||
"pages": [
|
||||
"channels/index",
|
||||
"channels/whatsapp",
|
||||
"channels/telegram",
|
||||
"channels/grammy",
|
||||
"channels/discord",
|
||||
"channels/slack",
|
||||
"channels/signal",
|
||||
"channels/imessage",
|
||||
"channels/msteams",
|
||||
"broadcast-groups",
|
||||
"channels/troubleshooting",
|
||||
"channels/location"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Providers",
|
||||
"pages": [
|
||||
"providers/index",
|
||||
"providers/models",
|
||||
"providers/openai",
|
||||
"providers/anthropic",
|
||||
"providers/moonshot",
|
||||
@@ -870,7 +746,18 @@
|
||||
"providers/openrouter",
|
||||
"providers/opencode",
|
||||
"providers/glm",
|
||||
"providers/zai"
|
||||
"providers/zai",
|
||||
"providers/telegram",
|
||||
"providers/grammy",
|
||||
"providers/discord",
|
||||
"providers/slack",
|
||||
"providers/signal",
|
||||
"providers/imessage",
|
||||
"providers/whatsapp",
|
||||
"broadcast-groups",
|
||||
"providers/msteams",
|
||||
"providers/troubleshooting",
|
||||
"providers/location"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,4 +35,4 @@ false negatives when deciding whether to respond in DMs or groups.
|
||||
## Related docs
|
||||
|
||||
- [Group Chats](/concepts/groups)
|
||||
- [Telegram Provider](/channels/telegram)
|
||||
- [Telegram Provider](/providers/telegram)
|
||||
@@ -47,10 +47,10 @@ API keys for daemon use: `clawdbot onboard`.
|
||||
See [/start/faq](/start/faq) for details on env inheritance (`env.shellEnv`,
|
||||
`~/.clawdbot/.env`, systemd/launchd).
|
||||
|
||||
## Anthropic: Claude Code CLI setup-token (supported)
|
||||
## Anthropic: Claude CLI setup-token (supported)
|
||||
|
||||
For Anthropic, the recommended path is an **API key**. If you’re already using
|
||||
Claude Code CLI, the setup-token flow is also supported.
|
||||
Claude Code, the Claude CLI setup-token is also supported.
|
||||
Run it on the **gateway host**:
|
||||
|
||||
```bash
|
||||
@@ -138,7 +138,7 @@ Use `--agent <id>` to target a specific agent; omit it to use the configured def
|
||||
`~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` when the auth store is
|
||||
loaded.
|
||||
3. Refreshable OAuth profiles can be refreshed automatically on use. Static
|
||||
token profiles (including Claude Code CLI setup-token) are not refreshable by
|
||||
token profiles (including Claude CLI setup-token) are not refreshable by
|
||||
Clawdbot.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
summary: "CLI backends: text-only fallback via local AI CLIs"
|
||||
read_when:
|
||||
- You want a reliable fallback when API providers fail
|
||||
- You are running Claude Code CLI or other local AI CLIs and want to reuse them
|
||||
- You are running Claude CLI or other local AI CLIs and want to reuse them
|
||||
- You need a text-only, tool-free path that still supports sessions and images
|
||||
---
|
||||
# CLI backends (fallback runtime)
|
||||
@@ -20,7 +20,7 @@ want “always works” text responses without relying on external APIs.
|
||||
|
||||
## Beginner-friendly quick start
|
||||
|
||||
You can use Claude Code CLI **without any config** (Clawdbot ships a built-in default):
|
||||
You can use Claude CLI **without any config** (Clawdbot ships a built-in default):
|
||||
|
||||
```bash
|
||||
clawdbot agent --message "hi" --model claude-cli/opus-4.5
|
||||
@@ -162,7 +162,7 @@ imageMode: "repeat"
|
||||
Clawdbot will write base64 images to temp files. If `imageArg` is set, those
|
||||
paths are passed as CLI args. If `imageArg` is missing, Clawdbot appends the
|
||||
file paths to the prompt (path injection), which is enough for CLIs that auto-
|
||||
load local files from plain paths (Claude Code CLI behavior).
|
||||
load local files from plain paths (Claude CLI behavior).
|
||||
|
||||
## Inputs / outputs
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Examples below are aligned with the current config schema. For the exhaustive re
|
||||
```json5
|
||||
{
|
||||
agent: { workspace: "~/clawd" },
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } }
|
||||
whatsapp: { allowFrom: ["+15555550123"] }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,11 +33,9 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
workspace: "~/clawd",
|
||||
model: { primary: "anthropic/claude-sonnet-4-5" }
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
whatsapp: {
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -110,7 +108,7 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
debounceMs: 1000,
|
||||
cap: 20,
|
||||
drop: "summarize",
|
||||
byChannel: {
|
||||
byProvider: {
|
||||
whatsapp: "collect",
|
||||
telegram: "collect",
|
||||
discord: "collect",
|
||||
@@ -143,60 +141,58 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
sendPolicy: {
|
||||
default: "allow",
|
||||
rules: [
|
||||
{ action: "deny", match: { channel: "discord", chatType: "group" } }
|
||||
{ action: "deny", match: { provider: "discord", chatType: "group" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Channels
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"],
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
},
|
||||
// Providers
|
||||
whatsapp: {
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"],
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
},
|
||||
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "YOUR_TELEGRAM_BOT_TOKEN",
|
||||
allowFrom: ["123456789"],
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["123456789"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
},
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "YOUR_TELEGRAM_BOT_TOKEN",
|
||||
allowFrom: ["123456789"],
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["123456789"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
},
|
||||
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_DISCORD_BOT_TOKEN",
|
||||
dm: { enabled: true, allowFrom: ["steipete"] },
|
||||
guilds: {
|
||||
"123456789012345678": {
|
||||
slug: "friends-of-clawd",
|
||||
requireMention: false,
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
help: { allow: true, requireMention: true }
|
||||
}
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_DISCORD_BOT_TOKEN",
|
||||
dm: { enabled: true, allowFrom: ["steipete"] },
|
||||
guilds: {
|
||||
"123456789012345678": {
|
||||
slug: "friends-of-clawd",
|
||||
requireMention: false,
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
help: { allow: true, requireMention: true }
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
slack: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-REPLACE_ME",
|
||||
appToken: "xapp-REPLACE_ME",
|
||||
channels: {
|
||||
"#general": { allow: true, requireMention: true }
|
||||
},
|
||||
dm: { enabled: true, allowFrom: ["U123"] },
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-REPLACE_ME",
|
||||
appToken: "xapp-REPLACE_ME",
|
||||
channels: {
|
||||
"#general": { allow: true, requireMention: true }
|
||||
},
|
||||
dm: { enabled: true, allowFrom: ["U123"] },
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
name: "clawd",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true
|
||||
}
|
||||
name: "clawd",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -345,7 +341,7 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
messageTemplate: "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}",
|
||||
textTemplate: "{{messages[0].snippet}}",
|
||||
deliver: true,
|
||||
channel: "last",
|
||||
provider: "last",
|
||||
to: "+15555550123",
|
||||
thinking: "low",
|
||||
timeoutSeconds: 300,
|
||||
@@ -410,18 +406,16 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
```json5
|
||||
{
|
||||
agent: { workspace: "~/clawd" },
|
||||
channels: {
|
||||
whatsapp: { allowFrom: ["+15555550123"] },
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "YOUR_TOKEN",
|
||||
allowFrom: ["123456789"]
|
||||
},
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_TOKEN",
|
||||
dm: { allowFrom: ["yourname"] }
|
||||
}
|
||||
whatsapp: { allowFrom: ["+15555550123"] },
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "YOUR_TOKEN",
|
||||
allowFrom: ["123456789"]
|
||||
},
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_TOKEN",
|
||||
dm: { allowFrom: ["yourname"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -466,14 +460,12 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
workspace: "~/work-clawd",
|
||||
elevated: { enabled: false }
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-...",
|
||||
channels: {
|
||||
"#engineering": { allow: true, requireMention: true },
|
||||
"#general": { allow: true, requireMention: true }
|
||||
}
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-...",
|
||||
channels: {
|
||||
"#engineering": { allow: true, requireMention: true },
|
||||
"#general": { allow: true, requireMention: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,4 +507,4 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
- If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`.
|
||||
- Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format.
|
||||
- Optional sections to add later: `web`, `browser`, `ui`, `bridge`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`.
|
||||
- See [Providers](/channels/whatsapp) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes.
|
||||
- See [Providers](/providers/whatsapp) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes.
|
||||
+287
-335
@@ -8,8 +8,8 @@ read_when:
|
||||
Clawdbot reads an optional **JSON5** config from `~/.clawdbot/clawdbot.json` (comments + trailing commas allowed).
|
||||
|
||||
If the file is missing, Clawdbot uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace `~/clawd`). You usually only need a config to:
|
||||
- restrict who can trigger the bot (`channels.whatsapp.allowFrom`, `channels.telegram.allowFrom`, etc.)
|
||||
- control group allowlists + mention behavior (`channels.whatsapp.groups`, `channels.telegram.groups`, `channels.discord.guilds`, `agents.list[].groupChat`)
|
||||
- restrict who can trigger the bot (`whatsapp.allowFrom`, `telegram.allowFrom`, etc.)
|
||||
- control group allowlists + mention behavior (`whatsapp.groups`, `telegram.groups`, `discord.guilds`, `agents.list[].groupChat`)
|
||||
- customize message prefixes (`messages`)
|
||||
- set the agent's workspace (`agents.defaults.workspace` or `agents.list[].workspace`)
|
||||
- tune the embedded agent defaults (`agents.defaults`) and session behavior (`session`)
|
||||
@@ -50,7 +50,7 @@ clawdbot gateway call config.apply --params '{
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { workspace: "~/clawd" } },
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } }
|
||||
whatsapp: { allowFrom: ["+15555550123"] }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,12 +74,10 @@ To prevent the bot from responding to WhatsApp @-mentions in groups (only respon
|
||||
}
|
||||
]
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
// Allowlist is DMs only; including your own number enables self-chat mode.
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
whatsapp: {
|
||||
// Allowlist is DMs only; including your own number enables self-chat mode.
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -191,7 +189,7 @@ Included files can themselves contain `$include` directives (up to 10 levels dee
|
||||
"./clients/schmidt/broadcast.json5"
|
||||
]},
|
||||
|
||||
channels: { whatsapp: { groupPolicy: "allowlist" } }
|
||||
whatsapp: { groupPolicy: "allowlist" }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -368,54 +366,50 @@ Metadata written by CLI wizards (`onboard`, `configure`, `doctor`).
|
||||
}
|
||||
```
|
||||
|
||||
### `channels.whatsapp.dmPolicy`
|
||||
### `whatsapp.dmPolicy`
|
||||
|
||||
Controls how WhatsApp direct chats (DMs) are handled:
|
||||
- `"pairing"` (default): unknown senders get a pairing code; owner must approve
|
||||
- `"allowlist"`: only allow senders in `channels.whatsapp.allowFrom` (or paired allow store)
|
||||
- `"open"`: allow all inbound DMs (**requires** `channels.whatsapp.allowFrom` to include `"*"`)
|
||||
- `"allowlist"`: only allow senders in `whatsapp.allowFrom` (or paired allow store)
|
||||
- `"open"`: allow all inbound DMs (**requires** `whatsapp.allowFrom` to include `"*"`)
|
||||
- `"disabled"`: ignore all inbound DMs
|
||||
|
||||
Pairing codes expire after 1 hour; the bot only sends a pairing code when a new request is created. Pending DM pairing requests are capped at **3 per channel** by default.
|
||||
Pairing codes expire after 1 hour; the bot only sends a pairing code when a new request is created. Pending DM pairing requests are capped at **3 per provider** by default.
|
||||
|
||||
Pairing approvals:
|
||||
- `clawdbot pairing list whatsapp`
|
||||
- `clawdbot pairing approve whatsapp <code>`
|
||||
|
||||
### `channels.whatsapp.allowFrom`
|
||||
### `whatsapp.allowFrom`
|
||||
|
||||
Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (**DMs only**).
|
||||
If empty and `channels.whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code.
|
||||
For groups, use `channels.whatsapp.groupPolicy` + `channels.whatsapp.groupAllowFrom`.
|
||||
If empty and `whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code.
|
||||
For groups, use `whatsapp.groupPolicy` + `whatsapp.groupAllowFrom`.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["+15555550123", "+447700900123"],
|
||||
textChunkLimit: 4000, // optional outbound chunk size (chars)
|
||||
mediaMaxMb: 50 // optional inbound media cap (MB)
|
||||
}
|
||||
whatsapp: {
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["+15555550123", "+447700900123"],
|
||||
textChunkLimit: 4000, // optional outbound chunk size (chars)
|
||||
mediaMaxMb: 50 // optional inbound media cap (MB)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `channels.whatsapp.accounts` (multi-account)
|
||||
### `whatsapp.accounts` (multi-account)
|
||||
|
||||
Run multiple WhatsApp accounts in one gateway:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
default: {}, // optional; keeps the default id stable
|
||||
personal: {},
|
||||
biz: {
|
||||
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
|
||||
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
|
||||
}
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
default: {}, // optional; keeps the default id stable
|
||||
personal: {},
|
||||
biz: {
|
||||
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
|
||||
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,23 +420,21 @@ Notes:
|
||||
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
|
||||
- The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`.
|
||||
|
||||
### `channels.telegram.accounts` / `channels.discord.accounts` / `channels.slack.accounts` / `channels.signal.accounts` / `channels.imessage.accounts`
|
||||
### `telegram.accounts` / `discord.accounts` / `slack.accounts` / `signal.accounts` / `imessage.accounts`
|
||||
|
||||
Run multiple accounts per channel (each account has its own `accountId` and optional `name`):
|
||||
Run multiple accounts per provider (each account has its own `accountId` and optional `name`):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
default: {
|
||||
name: "Primary bot",
|
||||
botToken: "123456:ABC..."
|
||||
},
|
||||
alerts: {
|
||||
name: "Alerts bot",
|
||||
botToken: "987654:XYZ..."
|
||||
}
|
||||
telegram: {
|
||||
accounts: {
|
||||
default: {
|
||||
name: "Primary bot",
|
||||
botToken: "123456:ABC..."
|
||||
},
|
||||
alerts: {
|
||||
name: "Alerts bot",
|
||||
botToken: "987654:XYZ..."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,7 +444,7 @@ Run multiple accounts per channel (each account has its own `accountId` and opti
|
||||
Notes:
|
||||
- `default` is used when `accountId` is omitted (CLI + routing).
|
||||
- Env tokens only apply to the **default** account.
|
||||
- Base channel settings (group policy, mention gating, etc.) apply to all accounts unless overridden per account.
|
||||
- Base provider settings (group policy, mention gating, etc.) apply to all accounts unless overridden per account.
|
||||
- Use `bindings[].match.accountId` to route each account to a different agents.defaults.
|
||||
|
||||
### Group chat mention gating (`agents.list[].groupChat` + `messages.groupChat`)
|
||||
@@ -460,7 +452,7 @@ Notes:
|
||||
Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats.
|
||||
|
||||
**Mention types:**
|
||||
- **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `channels.whatsapp.allowFrom`).
|
||||
- **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `whatsapp.allowFrom`).
|
||||
- **Text patterns**: Regex patterns defined in `agents.list[].groupChat.mentionPatterns`. Always checked regardless of self-chat mode.
|
||||
- Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).
|
||||
|
||||
@@ -477,7 +469,7 @@ Group messages default to **require mention** (either metadata mention or regex
|
||||
}
|
||||
```
|
||||
|
||||
`messages.groupChat.historyLimit` sets the global default for group history context. Channels can override with `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit` for multi-account). Set `0` to disable history wrapping.
|
||||
`messages.groupChat.historyLimit` sets the global default for group history context. Providers can override with `<provider>.historyLimit` (or `<provider>.accounts.*.historyLimit` for multi-account). Set `0` to disable history wrapping.
|
||||
|
||||
Per-agent override (takes precedence when set, even `[]`):
|
||||
```json5
|
||||
@@ -491,17 +483,15 @@ Per-agent override (takes precedence when set, even `[]`):
|
||||
}
|
||||
```
|
||||
|
||||
Mention gating defaults live per channel (`channels.whatsapp.groups`, `channels.telegram.groups`, `channels.imessage.groups`, `channels.discord.guilds`). When `*.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups.
|
||||
Mention gating defaults live per provider (`whatsapp.groups`, `telegram.groups`, `imessage.groups`, `discord.guilds`). When `*.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups.
|
||||
|
||||
To respond **only** to specific text triggers (ignoring native @-mentions):
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
// Include your own number to enable self-chat mode (ignore native @-mentions).
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
whatsapp: {
|
||||
// Include your own number to enable self-chat mode (ignore native @-mentions).
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
@@ -517,45 +507,43 @@ To respond **only** to specific text triggers (ignoring native @-mentions):
|
||||
}
|
||||
```
|
||||
|
||||
### Group policy (per channel)
|
||||
### Group policy (per provider)
|
||||
|
||||
Use `channels.*.groupPolicy` to control whether group/room messages are accepted at all:
|
||||
Use `*.groupPolicy` to control whether group/room messages are accepted at all:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
telegram: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["tg:123456789", "@alice"]
|
||||
},
|
||||
signal: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["chat_id:123"]
|
||||
},
|
||||
msteams: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["user@org.com"]
|
||||
},
|
||||
discord: {
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"GUILD_ID": {
|
||||
channels: { help: { allow: true } }
|
||||
}
|
||||
whatsapp: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
telegram: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["tg:123456789", "@alice"]
|
||||
},
|
||||
signal: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15551234567"]
|
||||
},
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["chat_id:123"]
|
||||
},
|
||||
msteams: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["user@org.com"]
|
||||
},
|
||||
discord: {
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"GUILD_ID": {
|
||||
channels: { help: { allow: true } }
|
||||
}
|
||||
},
|
||||
slack: {
|
||||
groupPolicy: "allowlist",
|
||||
channels: { "#general": { allow: true } }
|
||||
}
|
||||
},
|
||||
slack: {
|
||||
groupPolicy: "allowlist",
|
||||
channels: { "#general": { allow: true } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -565,7 +553,7 @@ Notes:
|
||||
- `"disabled"`: block all group/room messages.
|
||||
- `"allowlist"`: only allow groups/rooms that match the configured allowlist.
|
||||
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams use `groupAllowFrom` (fallback: explicit `allowFrom`).
|
||||
- Discord/Slack use channel allowlists (`channels.discord.guilds.*.channels`, `channels.slack.channels`).
|
||||
- Discord/Slack use channel allowlists (`discord.guilds.*.channels`, `slack.channels`).
|
||||
- Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`.
|
||||
- Default is `groupPolicy: "allowlist"`; if no allowlist is configured, group messages are blocked.
|
||||
|
||||
@@ -602,17 +590,17 @@ Inbound messages are routed to an agent via bindings.
|
||||
- `deny`: array of denied tool names (deny wins)
|
||||
- `agents.defaults`: shared agent defaults (model, workspace, sandbox, etc.).
|
||||
- `bindings[]`: routes inbound messages to an `agentId`.
|
||||
- `match.channel` (required)
|
||||
- `match.provider` (required)
|
||||
- `match.accountId` (optional; `*` = any account; omitted = default account)
|
||||
- `match.peer` (optional; `{ kind: dm|group|channel, id }`)
|
||||
- `match.guildId` / `match.teamId` (optional; channel-specific)
|
||||
- `match.guildId` / `match.teamId` (optional; provider-specific)
|
||||
|
||||
Deterministic match order:
|
||||
1) `match.peer`
|
||||
2) `match.guildId`
|
||||
3) `match.teamId`
|
||||
4) `match.accountId` (exact, no peer/guild/team)
|
||||
5) `match.accountId: "*"` (channel-wide, no peer/guild/team)
|
||||
5) `match.accountId: "*"` (provider-wide, no peer/guild/team)
|
||||
6) default agent (`agents.list[].default`, else first list entry, else `"main"`)
|
||||
|
||||
Within each match tier, the first matching entry in `bindings` wins.
|
||||
@@ -700,15 +688,13 @@ Example: two WhatsApp accounts → two agents:
|
||||
]
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
|
||||
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } }
|
||||
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
|
||||
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } }
|
||||
],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: {},
|
||||
biz: {},
|
||||
}
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: {},
|
||||
biz: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -741,7 +727,7 @@ Controls how inbound messages behave when an agent run is already active.
|
||||
debounceMs: 1000,
|
||||
cap: 20,
|
||||
drop: "summarize", // old | new | summarize
|
||||
byChannel: {
|
||||
byProvider: {
|
||||
whatsapp: "collect",
|
||||
telegram: "collect",
|
||||
discord: "collect",
|
||||
@@ -775,19 +761,18 @@ Controls how chat commands are enabled across connectors.
|
||||
Notes:
|
||||
- Text commands must be sent as a **standalone** message and use the leading `/` (no plain-text aliases).
|
||||
- `commands.text: false` disables parsing chat messages for commands.
|
||||
- `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported channels stay text-only.
|
||||
- Set `commands.native: true|false` to force all, or override per channel with `channels.discord.commands.native`, `channels.telegram.commands.native`, `channels.slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app.
|
||||
- `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<channel>`.
|
||||
- `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported providers stay text-only.
|
||||
- Set `commands.native: true|false` to force all, or override per provider with `discord.commands.native`, `telegram.commands.native`, `slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app.
|
||||
- `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<provider>`.
|
||||
- `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! <cmd>` requests are rejected (one at a time).
|
||||
- `commands.config: true` enables `/config` (reads/writes `clawdbot.json`).
|
||||
- `channels.<provider>.configWrites` gates config mutations initiated by that channel (default: true). This applies to `/config set|unset` plus provider-specific auto-migrations (Telegram supergroup ID changes, Slack channel ID changes).
|
||||
- `commands.debug: true` enables `/debug` (runtime-only overrides).
|
||||
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
|
||||
- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
|
||||
|
||||
### `web` (WhatsApp web channel runtime)
|
||||
### `web` (WhatsApp web provider)
|
||||
|
||||
WhatsApp runs through the gateway’s web channel (Baileys Web). It starts automatically when a linked session exists.
|
||||
WhatsApp runs through the gateway’s web provider. It starts automatically when a linked session exists.
|
||||
Set `web.enabled: false` to keep it off by default.
|
||||
|
||||
```json5
|
||||
@@ -806,56 +791,53 @@ Set `web.enabled: false` to keep it off by default.
|
||||
}
|
||||
```
|
||||
|
||||
### `channels.telegram` (bot transport)
|
||||
### `telegram` (bot transport)
|
||||
|
||||
Clawdbot starts Telegram only when a `channels.telegram` config section exists. The bot token is resolved from `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken`.
|
||||
Set `channels.telegram.enabled: false` to disable automatic startup.
|
||||
Multi-account support lives under `channels.telegram.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||||
Set `channels.telegram.configWrites: false` to block Telegram-initiated config writes (including supergroup ID migrations and `/config set|unset`).
|
||||
Clawdbot starts Telegram only when a `telegram` config section exists. The bot token is resolved from `TELEGRAM_BOT_TOKEN` or `telegram.botToken`.
|
||||
Set `telegram.enabled: false` to disable automatic startup.
|
||||
Multi-account support lives under `telegram.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "your-bot-token",
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"-1001234567890": {
|
||||
allowFrom: ["@admin"],
|
||||
systemPrompt: "Keep answers brief.",
|
||||
topics: {
|
||||
"99": {
|
||||
requireMention: false,
|
||||
skills: ["search"],
|
||||
systemPrompt: "Stay on topic."
|
||||
}
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "your-bot-token",
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"-1001234567890": {
|
||||
allowFrom: ["@admin"],
|
||||
systemPrompt: "Keep answers brief.",
|
||||
topics: {
|
||||
"99": {
|
||||
requireMention: false,
|
||||
skills: ["search"],
|
||||
systemPrompt: "Stay on topic."
|
||||
}
|
||||
}
|
||||
},
|
||||
historyLimit: 50, // include last N group messages as context (0 disables)
|
||||
replyToMode: "first", // off | first | all
|
||||
streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
|
||||
draftChunk: { // optional; only for streamMode=block
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph" // paragraph | newline | sentence
|
||||
},
|
||||
actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
|
||||
mediaMaxMb: 5,
|
||||
retry: { // outbound retry policy
|
||||
attempts: 3,
|
||||
minDelayMs: 400,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
},
|
||||
proxy: "socks5://localhost:9050",
|
||||
webhookUrl: "https://example.com/telegram-webhook",
|
||||
webhookSecret: "secret",
|
||||
webhookPath: "/telegram-webhook"
|
||||
}
|
||||
}
|
||||
},
|
||||
historyLimit: 50, // include last N group messages as context (0 disables)
|
||||
replyToMode: "first", // off | first | all
|
||||
streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
|
||||
draftChunk: { // optional; only for streamMode=block
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph" // paragraph | newline | sentence
|
||||
},
|
||||
actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
|
||||
mediaMaxMb: 5,
|
||||
retry: { // outbound retry policy
|
||||
attempts: 3,
|
||||
minDelayMs: 400,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
},
|
||||
proxy: "socks5://localhost:9050",
|
||||
webhookUrl: "https://example.com/telegram-webhook",
|
||||
webhookSecret: "secret",
|
||||
webhookPath: "/telegram-webhook"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -866,153 +848,148 @@ Draft streaming notes:
|
||||
- `/reasoning stream` streams reasoning into the draft, then sends the final answer.
|
||||
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
|
||||
|
||||
### `channels.discord` (bot transport)
|
||||
### `discord` (bot transport)
|
||||
|
||||
Configure the Discord bot by setting the bot token and optional gating:
|
||||
Multi-account support lives under `channels.discord.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||||
Multi-account support lives under `discord.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "your-bot-token",
|
||||
mediaMaxMb: 8, // clamp inbound media size
|
||||
allowBots: false, // allow bot-authored messages
|
||||
actions: { // tool action gates (false disables)
|
||||
reactions: true,
|
||||
stickers: true,
|
||||
polls: true,
|
||||
permissions: true,
|
||||
messages: true,
|
||||
threads: true,
|
||||
pins: true,
|
||||
search: true,
|
||||
memberInfo: true,
|
||||
roleInfo: true,
|
||||
roles: false,
|
||||
channelInfo: true,
|
||||
voiceStatus: true,
|
||||
events: true,
|
||||
moderation: false
|
||||
},
|
||||
replyToMode: "off", // off | first | all
|
||||
dm: {
|
||||
enabled: true, // disable all DMs when false
|
||||
policy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["1234567890", "steipete"], // optional DM allowlist ("open" requires ["*"])
|
||||
groupEnabled: false, // enable group DMs
|
||||
groupChannels: ["clawd-dm"] // optional group DM allowlist
|
||||
},
|
||||
guilds: {
|
||||
"123456789012345678": { // guild id (preferred) or slug
|
||||
slug: "friends-of-clawd",
|
||||
requireMention: false, // per-guild default
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
users: ["987654321098765432"], // optional per-guild user allowlist
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
help: {
|
||||
allow: true,
|
||||
requireMention: true,
|
||||
users: ["987654321098765432"],
|
||||
skills: ["docs"],
|
||||
systemPrompt: "Short answers only."
|
||||
}
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "your-bot-token",
|
||||
mediaMaxMb: 8, // clamp inbound media size
|
||||
allowBots: false, // allow bot-authored messages
|
||||
actions: { // tool action gates (false disables)
|
||||
reactions: true,
|
||||
stickers: true,
|
||||
polls: true,
|
||||
permissions: true,
|
||||
messages: true,
|
||||
threads: true,
|
||||
pins: true,
|
||||
search: true,
|
||||
memberInfo: true,
|
||||
roleInfo: true,
|
||||
roles: false,
|
||||
channelInfo: true,
|
||||
voiceStatus: true,
|
||||
events: true,
|
||||
moderation: false
|
||||
},
|
||||
replyToMode: "off", // off | first | all
|
||||
dm: {
|
||||
enabled: true, // disable all DMs when false
|
||||
policy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["1234567890", "steipete"], // optional DM allowlist ("open" requires ["*"])
|
||||
groupEnabled: false, // enable group DMs
|
||||
groupChannels: ["clawd-dm"] // optional group DM allowlist
|
||||
},
|
||||
guilds: {
|
||||
"123456789012345678": { // guild id (preferred) or slug
|
||||
slug: "friends-of-clawd",
|
||||
requireMention: false, // per-guild default
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
users: ["987654321098765432"], // optional per-guild user allowlist
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
help: {
|
||||
allow: true,
|
||||
requireMention: true,
|
||||
users: ["987654321098765432"],
|
||||
skills: ["docs"],
|
||||
systemPrompt: "Short answers only."
|
||||
}
|
||||
}
|
||||
},
|
||||
historyLimit: 20, // include last N guild messages as context
|
||||
textChunkLimit: 2000, // optional outbound text chunk size (chars)
|
||||
maxLinesPerMessage: 17, // soft max lines per message (Discord UI clipping)
|
||||
retry: { // outbound retry policy
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
}
|
||||
},
|
||||
historyLimit: 20, // include last N guild messages as context
|
||||
textChunkLimit: 2000, // optional outbound text chunk size (chars)
|
||||
maxLinesPerMessage: 17, // soft max lines per message (Discord UI clipping)
|
||||
retry: { // outbound retry policy
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clawdbot starts Discord only when a `channels.discord` config section exists. The token is resolved from `DISCORD_BOT_TOKEN` or `channels.discord.token` (unless `channels.discord.enabled` is `false`). Use `user:<id>` (DM) or `channel:<id>` (guild channel) when specifying delivery targets for cron/CLI commands; bare numeric IDs are ambiguous and rejected.
|
||||
Clawdbot starts Discord only when a `discord` config section exists. The token is resolved from `DISCORD_BOT_TOKEN` or `discord.token` (unless `discord.enabled` is `false`). Use `user:<id>` (DM) or `channel:<id>` (guild channel) when specifying delivery targets for cron/CLI commands; bare numeric IDs are ambiguous and rejected.
|
||||
Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity.
|
||||
Bot-authored messages are ignored by default. Enable with `channels.discord.allowBots` (own messages are still filtered to prevent self-reply loops).
|
||||
Bot-authored messages are ignored by default. Enable with `discord.allowBots` (own messages are still filtered to prevent self-reply loops).
|
||||
Reaction notification modes:
|
||||
- `off`: no reaction events.
|
||||
- `own`: reactions on the bot's own messages (default).
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
||||
Outbound text is chunked by `channels.discord.textChunkLimit` (default 2000). Discord clients can clip very tall messages, so `channels.discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
|
||||
Outbound text is chunked by `discord.textChunkLimit` (default 2000). Discord clients can clip very tall messages, so `discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
|
||||
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
|
||||
|
||||
### `channels.slack` (socket mode)
|
||||
### `slack` (socket mode)
|
||||
|
||||
Slack runs in Socket Mode and requires both a bot token and app token:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-...",
|
||||
appToken: "xapp-...",
|
||||
dm: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-...",
|
||||
appToken: "xapp-...",
|
||||
dm: {
|
||||
enabled: true,
|
||||
policy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["U123", "U456", "*"], // optional; "open" requires ["*"]
|
||||
groupEnabled: false,
|
||||
groupChannels: ["G123"]
|
||||
},
|
||||
channels: {
|
||||
C123: { allow: true, requireMention: true, allowBots: false },
|
||||
"#general": {
|
||||
allow: true,
|
||||
requireMention: true,
|
||||
allowBots: false,
|
||||
users: ["U123"],
|
||||
skills: ["docs"],
|
||||
systemPrompt: "Short answers only."
|
||||
}
|
||||
},
|
||||
historyLimit: 50, // include last N channel/group messages as context (0 disables)
|
||||
allowBots: false,
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
reactionAllowlist: ["U123"],
|
||||
replyToMode: "off", // off | first | all
|
||||
actions: {
|
||||
reactions: true,
|
||||
messages: true,
|
||||
pins: true,
|
||||
memberInfo: true,
|
||||
emojiList: true
|
||||
},
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
name: "clawd",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true
|
||||
},
|
||||
textChunkLimit: 4000,
|
||||
mediaMaxMb: 20
|
||||
}
|
||||
policy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["U123", "U456", "*"], // optional; "open" requires ["*"]
|
||||
groupEnabled: false,
|
||||
groupChannels: ["G123"]
|
||||
},
|
||||
channels: {
|
||||
C123: { allow: true, requireMention: true, allowBots: false },
|
||||
"#general": {
|
||||
allow: true,
|
||||
requireMention: true,
|
||||
allowBots: false,
|
||||
users: ["U123"],
|
||||
skills: ["docs"],
|
||||
systemPrompt: "Short answers only."
|
||||
}
|
||||
},
|
||||
historyLimit: 50, // include last N channel/group messages as context (0 disables)
|
||||
allowBots: false,
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
reactionAllowlist: ["U123"],
|
||||
replyToMode: "off", // off | first | all
|
||||
actions: {
|
||||
reactions: true,
|
||||
messages: true,
|
||||
pins: true,
|
||||
memberInfo: true,
|
||||
emojiList: true
|
||||
},
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
name: "clawd",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true
|
||||
},
|
||||
textChunkLimit: 4000,
|
||||
mediaMaxMb: 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account support lives under `channels.slack.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||||
Multi-account support lives under `slack.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||||
|
||||
Clawdbot starts Slack when the provider is enabled and both tokens are set (via config or `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`). Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands.
|
||||
Set `channels.slack.configWrites: false` to block Slack-initiated config writes (including channel ID migrations and `/config set|unset`).
|
||||
|
||||
Bot-authored messages are ignored by default. Enable with `channels.slack.allowBots` or `channels.slack.channels.<id>.allowBots`.
|
||||
Bot-authored messages are ignored by default. Enable with `slack.allowBots` or `slack.channels.<id>.allowBots`.
|
||||
|
||||
Reaction notification modes:
|
||||
- `off`: no reaction events.
|
||||
- `own`: reactions on the bot's own messages (default).
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `channels.slack.reactionAllowlist` on all messages (empty list disables).
|
||||
- `allowlist`: reactions from `slack.reactionAllowlist` on all messages (empty list disables).
|
||||
|
||||
Slack action groups (gate `slack` tool actions):
|
||||
| Action group | Default | Notes |
|
||||
@@ -1023,18 +1000,16 @@ Slack action groups (gate `slack` tool actions):
|
||||
| memberInfo | enabled | Member info |
|
||||
| emojiList | enabled | Custom emoji list |
|
||||
|
||||
### `channels.signal` (signal-cli)
|
||||
### `signal` (signal-cli)
|
||||
|
||||
Signal reactions can emit system events (shared reaction tooling):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
signal: {
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
|
||||
historyLimit: 50 // include last N group messages as context (0 disables)
|
||||
}
|
||||
signal: {
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
|
||||
historyLimit: 50 // include last N group messages as context (0 disables)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1043,38 +1018,36 @@ Reaction notification modes:
|
||||
- `off`: no reaction events.
|
||||
- `own`: reactions on the bot's own messages (default).
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `channels.signal.reactionAllowlist` on all messages (empty list disables).
|
||||
- `allowlist`: reactions from `signal.reactionAllowlist` on all messages (empty list disables).
|
||||
|
||||
### `channels.imessage` (imsg CLI)
|
||||
### `imessage` (imsg CLI)
|
||||
|
||||
Clawdbot spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "imsg",
|
||||
dbPath: "~/Library/Messages/chat.db",
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
|
||||
historyLimit: 50, // include last N group messages as context (0 disables)
|
||||
includeAttachments: false,
|
||||
mediaMaxMb: 16,
|
||||
service: "auto",
|
||||
region: "US"
|
||||
}
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "imsg",
|
||||
dbPath: "~/Library/Messages/chat.db",
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
|
||||
historyLimit: 50, // include last N group messages as context (0 disables)
|
||||
includeAttachments: false,
|
||||
mediaMaxMb: 16,
|
||||
service: "auto",
|
||||
region: "US"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account support lives under `channels.imessage.accounts` (see the multi-account section above).
|
||||
Multi-account support lives under `imessage.accounts` (see the multi-account section above).
|
||||
|
||||
Notes:
|
||||
- Requires Full Disk Access to the Messages DB.
|
||||
- The first send will prompt for Messages automation permission.
|
||||
- Prefer `chat_id:<id>` targets. Use `imsg chats --limit 20` to list chats.
|
||||
- `channels.imessage.cliPath` can point to a wrapper script (e.g. `ssh` to another Mac that runs `imsg rpc`); use SSH keys to avoid password prompts.
|
||||
- `imessage.cliPath` can point to a wrapper script (e.g. `ssh` to another Mac that runs `imsg rpc`); use SSH keys to avoid password prompts.
|
||||
|
||||
Example wrapper:
|
||||
```bash
|
||||
@@ -1151,19 +1124,19 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
|
||||
```
|
||||
|
||||
`responsePrefix` is applied to **all outbound replies** (tool summaries, block
|
||||
streaming, final replies) across channels unless already present.
|
||||
streaming, final replies) across providers unless already present.
|
||||
|
||||
If `messages.responsePrefix` is unset, no prefix is applied by default.
|
||||
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
|
||||
|
||||
WhatsApp inbound prefix is configured via `channels.whatsapp.messagePrefix` (deprecated:
|
||||
WhatsApp inbound prefix is configured via `whatsapp.messagePrefix` (deprecated:
|
||||
`messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when
|
||||
`channels.whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
|
||||
`whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
|
||||
`"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when the routed
|
||||
agent has `identity.name` set.
|
||||
|
||||
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
|
||||
on channels that support reactions (Slack/Discord/Telegram). Defaults to the
|
||||
on providers that support reactions (Slack/Discord/Telegram). Defaults to the
|
||||
active agent’s `identity.emoji` when set, otherwise `"👀"`. Set it to `""` to disable.
|
||||
|
||||
`ackReactionScope` controls when reactions fire:
|
||||
@@ -1244,27 +1217,6 @@ is already present in `agents.defaults.models`:
|
||||
|
||||
If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override).
|
||||
|
||||
Example: Opus 4.5 primary with MiniMax M2.1 fallback (hosted MiniMax):
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": { alias: "opus" },
|
||||
"minimax/MiniMax-M2.1": { alias: "minimax" }
|
||||
},
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-5",
|
||||
fallbacks: ["minimax/MiniMax-M2.1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
MiniMax auth: set `MINIMAX_API_KEY` (env) or configure `models.providers.minimax`.
|
||||
|
||||
#### `agents.defaults.cliBackends` (CLI fallback)
|
||||
|
||||
Optional CLI backends for text-only fallback runs (no tool calls). These are useful as a
|
||||
@@ -1486,8 +1438,8 @@ Example (tuned):
|
||||
|
||||
Block streaming:
|
||||
- `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default off).
|
||||
- Channel overrides: `*.blockStreaming` (and per-account variants) to force block streaming on/off.
|
||||
Non-Telegram channels require an explicit `*.blockStreaming: true` to enable block replies.
|
||||
- Provider overrides: `*.blockStreaming` (and per-account variants) to force block streaming on/off.
|
||||
Non-Telegram providers require an explicit `*.blockStreaming: true` to enable block replies.
|
||||
- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"` (default: text_end).
|
||||
- `agents.defaults.blockStreamingChunk`: soft chunking for streamed blocks. Defaults to
|
||||
800–1200 chars, prefers paragraph breaks (`\n\n`), then newlines, then sentences.
|
||||
@@ -1499,11 +1451,11 @@ Block streaming:
|
||||
```
|
||||
- `agents.defaults.blockStreamingCoalesce`: merge streamed blocks before sending.
|
||||
Defaults to `{ idleMs: 1000 }` and inherits `minChars` from `blockStreamingChunk`
|
||||
with `maxChars` capped to the channel text limit. Signal/Slack/Discord default
|
||||
with `maxChars` capped to the provider text limit. Signal/Slack/Discord default
|
||||
to `minChars: 1500` unless overridden.
|
||||
Channel overrides: `channels.whatsapp.blockStreamingCoalesce`, `channels.telegram.blockStreamingCoalesce`,
|
||||
`channels.discord.blockStreamingCoalesce`, `channels.slack.blockStreamingCoalesce`, `channels.signal.blockStreamingCoalesce`,
|
||||
`channels.imessage.blockStreamingCoalesce`, `channels.msteams.blockStreamingCoalesce` (and per-account variants).
|
||||
Provider overrides: `whatsapp.blockStreamingCoalesce`, `telegram.blockStreamingCoalesce`,
|
||||
`discord.blockStreamingCoalesce`, `slack.blockStreamingCoalesce`, `signal.blockStreamingCoalesce`,
|
||||
`imessage.blockStreamingCoalesce`, `msteams.blockStreamingCoalesce` (and per-account variants).
|
||||
- `agents.defaults.humanDelay`: randomized pause between **block replies** after the first.
|
||||
Modes: `off` (default), `natural` (800–2500ms), `custom` (use `minMs`/`maxMs`).
|
||||
Per-agent override: `agents.list[].humanDelay`.
|
||||
@@ -1535,8 +1487,8 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
||||
`30m`. Set `0m` to disable.
|
||||
- `model`: optional override model for heartbeat runs (`provider/model`).
|
||||
- `includeReasoning`: when `true`, heartbeats will also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`). Default: `false`.
|
||||
- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`.
|
||||
- `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp, chat id for Telegram).
|
||||
- `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`.
|
||||
- `to`: optional recipient override (provider-specific id, e.g. E.164 for WhatsApp, chat id for Telegram).
|
||||
- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md if exists` line if you still want the file read.
|
||||
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 300).
|
||||
|
||||
@@ -1609,10 +1561,10 @@ Tool groups (shorthands) work in **global** and **per-agent** tool policies:
|
||||
|
||||
`tools.elevated` controls elevated (host) exec access:
|
||||
- `enabled`: allow elevated mode (default true)
|
||||
- `allowFrom`: per-channel allowlists (empty = disabled)
|
||||
- `allowFrom`: per-provider allowlists (empty = disabled)
|
||||
- `whatsapp`: E.164 numbers
|
||||
- `telegram`: chat ids or usernames
|
||||
- `discord`: user ids or usernames (falls back to `channels.discord.dm.allowFrom` if omitted)
|
||||
- `discord`: user ids or usernames (falls back to `discord.dm.allowFrom` if omitted)
|
||||
- `signal`: E.164 numbers
|
||||
- `imessage`: handles/chat ids
|
||||
- `webchat`: session ids or usernames
|
||||
@@ -2085,12 +2037,12 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
|
||||
// Max ping-pong reply turns between requester/target (0–5).
|
||||
maxPingPongTurns: 5
|
||||
},
|
||||
sendPolicy: {
|
||||
rules: [
|
||||
{ action: "deny", match: { channel: "discord", chatType: "group" } }
|
||||
],
|
||||
default: "allow"
|
||||
}
|
||||
sendPolicy: {
|
||||
rules: [
|
||||
{ action: "deny", match: { provider: "discord", chatType: "group" } }
|
||||
],
|
||||
default: "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -2100,7 +2052,7 @@ Fields:
|
||||
- Sandbox note: `agents.defaults.sandbox.mode: "non-main"` uses this key to detect the main session. Any session key that does not match `mainKey` (groups/channels) is sandboxed.
|
||||
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
||||
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
||||
- `sendPolicy.rules[]`: match by `channel`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
||||
- `sendPolicy.rules[]`: match by `provider`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
||||
|
||||
### `skills` (skills config)
|
||||
|
||||
@@ -2352,8 +2304,8 @@ Hot-applied (no full gateway restart):
|
||||
- `browser` (browser control server restart)
|
||||
- `cron` (cron service restart + concurrency update)
|
||||
- `agents.defaults.heartbeat` (heartbeat runner restart)
|
||||
- `web` (WhatsApp web channel restart)
|
||||
- `telegram`, `discord`, `signal`, `imessage` (channel restarts)
|
||||
- `web` (WhatsApp web provider restart)
|
||||
- `telegram`, `discord`, `signal`, `imessage` (provider restarts)
|
||||
- `agent`, `models`, `routing`, `messages`, `session`, `whatsapp`, `logging`, `skills`, `ui`, `talk`, `identity`, `wizard` (dynamic reads)
|
||||
|
||||
Requires full Gateway restart:
|
||||
@@ -2412,7 +2364,7 @@ Defaults:
|
||||
messageTemplate:
|
||||
"From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}",
|
||||
deliver: true,
|
||||
channel: "last",
|
||||
provider: "last",
|
||||
model: "openai/gpt-5.2-mini",
|
||||
},
|
||||
],
|
||||
@@ -2427,7 +2379,7 @@ Requests must include the hook token:
|
||||
|
||||
Endpoints:
|
||||
- `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat" }`
|
||||
- `POST /hooks/agent` → `{ message, name?, sessionKey?, wakeMode?, deliver?, channel?, to?, model?, thinking?, timeoutSeconds? }`
|
||||
- `POST /hooks/agent` → `{ message, name?, sessionKey?, wakeMode?, deliver?, provider?, to?, model?, thinking?, timeoutSeconds? }`
|
||||
- `POST /hooks/<name>` → resolved via `hooks.mappings`
|
||||
|
||||
`/hooks/agent` always posts a summary into the main session (and can optionally trigger an immediate heartbeat via `wakeMode: "now"`).
|
||||
@@ -2437,8 +2389,8 @@ Mapping notes:
|
||||
- `match.source` matches a payload field (e.g. `{ source: "gmail" }`) so you can use a generic `/hooks/ingest` path.
|
||||
- Templates like `{{messages[0].subject}}` read from the payload.
|
||||
- `transform` can point to a JS/TS module that returns a hook action.
|
||||
- `deliver: true` sends the final reply to a channel; `channel` defaults to `last` (falls back to WhatsApp).
|
||||
- If there is no prior delivery route, set `channel` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage/MS Teams).
|
||||
- `deliver: true` sends the final reply to a provider; `provider` defaults to `last` (falls back to WhatsApp).
|
||||
- If there is no prior delivery route, set `provider` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage/MS Teams).
|
||||
- `model` overrides the LLM for this hook run (`provider/model` or alias; must be allowed if `agents.defaults.models` is set).
|
||||
|
||||
Gmail helper config (used by `clawdbot hooks gmail setup` / `run`):
|
||||
@@ -2577,9 +2529,9 @@ Template placeholders are expanded in `tools.audio.transcription.args` (and any
|
||||
| `{{Body}}` | Full inbound message body |
|
||||
| `{{RawBody}}` | Raw inbound message body (no history/sender wrappers; best for command parsing) |
|
||||
| `{{BodyStripped}}` | Body with group mentions stripped (best default for agents) |
|
||||
| `{{From}}` | Sender identifier (E.164 for WhatsApp; may differ per channel) |
|
||||
| `{{From}}` | Sender identifier (E.164 for WhatsApp; may differ per provider) |
|
||||
| `{{To}}` | Destination identifier |
|
||||
| `{{MessageSid}}` | Channel message id (when available) |
|
||||
| `{{MessageSid}}` | Provider message id (when available) |
|
||||
| `{{SessionId}}` | Current session UUID |
|
||||
| `{{IsNewSession}}` | `"true"` when a new session was created |
|
||||
| `{{MediaUrl}}` | Inbound media pseudo-URL (if present) |
|
||||
|
||||
@@ -16,7 +16,7 @@ The design goal is to keep all network discovery/advertising in the **Node Gatew
|
||||
|
||||
## Terms
|
||||
|
||||
- **Gateway**: the single, long-running gateway process that owns state (sessions, pairing, node registry) and runs channels.
|
||||
- **Gateway**: the single, long-running gateway process that owns state (sessions, pairing, node registry) and runs providers.
|
||||
- **Gateway WS (loopback)**: the existing gateway WebSocket control endpoint on `127.0.0.1:18789`.
|
||||
- **Bridge (direct transport)**: a LAN/tailnet-facing endpoint owned by the gateway that allows authenticated clients/nodes to call a scoped subset of gateway methods. The bridge exists so the gateway can remain loopback-only.
|
||||
- **SSH transport (fallback)**: remote control by forwarding `127.0.0.1:18789` over SSH.
|
||||
|
||||
@@ -69,14 +69,13 @@ cat ~/.clawdbot/clawdbot.json
|
||||
- Sandbox image repair when sandboxing is enabled.
|
||||
- Legacy service migration and extra gateway detection.
|
||||
- Gateway runtime checks (service installed but not running; cached launchd label).
|
||||
- Channel status warnings (probed from the running gateway).
|
||||
- Provider status warnings (probed from the running gateway).
|
||||
- Supervisor config audit (launchd/systemd/schtasks) with optional repair.
|
||||
- Gateway runtime best-practice checks (Node vs Bun, version-manager paths).
|
||||
- Gateway port collision diagnostics (default `18789`).
|
||||
- Security warnings for open DM policies.
|
||||
- Gateway auth warnings when no `gateway.auth.token` is set (local mode; offers token generation).
|
||||
- systemd linger check on Linux.
|
||||
- Source install checks (pnpm workspace mismatch, missing UI assets, missing tsx binary).
|
||||
- Writes updated config + wizard metadata.
|
||||
|
||||
## Detailed behavior and rationale
|
||||
@@ -103,8 +102,8 @@ The Gateway also auto-runs doctor migrations on startup when it detects a
|
||||
legacy config format, so stale configs are repaired without manual intervention.
|
||||
|
||||
Current migrations:
|
||||
- `routing.allowFrom` → `channels.whatsapp.allowFrom`
|
||||
- `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention`
|
||||
- `routing.allowFrom` → `whatsapp.allowFrom`
|
||||
- `routing.groupChat.requireMention` → `whatsapp/telegram/imessage.groups."*".requireMention`
|
||||
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
|
||||
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
|
||||
- `routing.queue` → `messages.queue`
|
||||
@@ -209,8 +208,8 @@ creation in automation.
|
||||
Doctor runs a health check and offers to restart the gateway when it looks
|
||||
unhealthy.
|
||||
|
||||
### 14) Channel status warnings
|
||||
If the gateway is healthy, doctor runs a channel status probe and reports
|
||||
### 14) Provider status warnings
|
||||
If the gateway is healthy, doctor runs a provider status probe and reports
|
||||
warnings with suggested fixes.
|
||||
|
||||
### 15) Supervisor config audit + repair
|
||||
@@ -234,7 +233,7 @@ running, SSH tunnel).
|
||||
|
||||
### 17) Gateway runtime best practices
|
||||
Doctor warns when the gateway service runs on Bun or a version-managed Node path
|
||||
(`nvm`, `fnm`, `volta`, `asdf`, etc.). WhatsApp + Telegram channels require Node,
|
||||
(`nvm`, `fnm`, `volta`, `asdf`, etc.). WhatsApp + Telegram providers require Node,
|
||||
and version-manager paths can break after upgrades because the daemon does not
|
||||
load your shell init. Doctor offers to migrate to a system Node install when
|
||||
available (Homebrew/apt/choco).
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
summary: "Health check steps for channel connectivity"
|
||||
summary: "Health check steps for provider connectivity"
|
||||
read_when:
|
||||
- Diagnosing WhatsApp channel health
|
||||
- Diagnosing web provider health
|
||||
---
|
||||
# Health Checks (CLI)
|
||||
|
||||
Short guide to verify channel connectivity without guessing.
|
||||
Short guide to verify provider connectivity without guessing.
|
||||
|
||||
## Quick checks
|
||||
- `clawdbot status` — local summary: gateway reachability/mode, update hint, linked channel auth age, sessions + recent activity.
|
||||
- `clawdbot status` — local summary: gateway reachability/mode, update hint, link provider auth age, sessions + recent activity.
|
||||
- `clawdbot status --all` — full local diagnosis (read-only, color, safe to paste for debugging).
|
||||
- `clawdbot status --deep` — also probes the running Gateway (per-channel probes when supported).
|
||||
- `clawdbot status --deep` — also probes the running Gateway (per-provider probes when supported).
|
||||
- `clawdbot health --json` — asks the running Gateway for a full health snapshot (WS-only; no direct Baileys socket).
|
||||
- Send `/status` as a standalone message in WhatsApp/WebChat to get a status reply without invoking the agent.
|
||||
- Logs: tail `/tmp/clawdbot/clawdbot-*.log` and filter for `web-heartbeat`, `web-reconnect`, `web-auto-reply`, `web-inbound`.
|
||||
@@ -18,12 +18,12 @@ Short guide to verify channel connectivity without guessing.
|
||||
## Deep diagnostics
|
||||
- Creds on disk: `ls -l ~/.clawdbot/credentials/whatsapp/<accountId>/creds.json` (mtime should be recent).
|
||||
- Session store: `ls -l ~/.clawdbot/agents/<agentId>/sessions/sessions.json` (path can be overridden in config). Count and recent recipients are surfaced via `status`.
|
||||
- Relink flow: `clawdbot channels logout && clawdbot channels login --verbose` when status codes 409–515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.)
|
||||
- Relink flow: `clawdbot providers logout && clawdbot providers login --verbose` when status codes 409–515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.)
|
||||
|
||||
## When something fails
|
||||
- `logged out` or status 409–515 → relink with `clawdbot channels logout` then `clawdbot channels login`.
|
||||
- `logged out` or status 409–515 → relink with `clawdbot providers logout` then `clawdbot providers login`.
|
||||
- Gateway unreachable → start it: `clawdbot gateway --port 18789` (use `--force` if the port is busy).
|
||||
- No inbound messages → confirm linked phone is online and the sender is allowed (`channels.whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`channels.whatsapp.groups`, `agents.list[].groupChat.mentionPatterns`).
|
||||
- No inbound messages → confirm linked phone is online and the sender is allowed (`whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`whatsapp.groups`, `agents.list[].groupChat.mentionPatterns`).
|
||||
|
||||
## Dedicated "health" command
|
||||
`clawdbot health --json` asks the running Gateway for its health snapshot (no direct channel sockets from the CLI). It reports linked creds/auth age when available, per-channel probe summaries, session-store summary, and a probe duration. It exits non-zero if the Gateway is unreachable or the probe fails/timeouts. Use `--timeout <ms>` to override the 10s default.
|
||||
`clawdbot health --json` asks the running Gateway for its health snapshot (no direct provider sockets from the CLI). It reports linked creds/auth age when available, per-provider probe summaries, session-store summary, and a probe duration. It exits non-zero if the Gateway is unreachable or the probe fails/timeouts. Use `--timeout <ms>` to override the 10s default.
|
||||
@@ -76,7 +76,7 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
||||
target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none
|
||||
to: "+15551234567", // optional channel-specific override
|
||||
to: "+15551234567", // optional provider-specific override
|
||||
prompt: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.",
|
||||
ackMaxChars: 300 // max chars allowed after HEARTBEAT_OK
|
||||
}
|
||||
@@ -91,8 +91,8 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
||||
- `model`: optional model override for heartbeat runs (`provider/model`).
|
||||
- `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
|
||||
- `target`:
|
||||
- `last` (default): deliver to the last used external channel.
|
||||
- explicit channel: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage`.
|
||||
- `last` (default): deliver to the last used external provider.
|
||||
- explicit provider: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage`.
|
||||
- `none`: run the heartbeat but **do not deliver** externally.
|
||||
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram, etc.).
|
||||
- `prompt`: overrides the default prompt body (not merged).
|
||||
|
||||
@@ -125,7 +125,7 @@ CLAWDBOT_CONFIG_PATH=~/.clawdbot/b.json CLAWDBOT_STATE_DIR=~/.clawdbot-b clawdbo
|
||||
- `status` — short summary.
|
||||
- `system-presence` — current presence list.
|
||||
- `system-event` — post a presence/system note (structured).
|
||||
- `send` — send a message via the active channel(s).
|
||||
- `send` — send a message via the active provider(s).
|
||||
- `agent` — run an agent turn (streams events back on same connection).
|
||||
- `node.list` — list paired + currently-connected bridge nodes (includes `caps`, `deviceFamily`, `modelIdentifier`, `paired`, `connected`, and advertised `commands`).
|
||||
- `node.describe` — describe a node (capabilities + supported `node.invoke` commands; works for paired nodes and for currently-connected unpaired nodes).
|
||||
@@ -268,7 +268,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above.
|
||||
|
||||
## Operational checks
|
||||
- Liveness: open WS and send `req:connect` → expect `res` with `payload.type="hello-ok"` (with snapshot).
|
||||
- Readiness: call `health` → expect `ok: true` and a linked channel in `linkChannel` (when applicable).
|
||||
- Readiness: call `health` → expect `ok: true` and a linked provider in the `providers` payload (when applicable).
|
||||
- Debug: subscribe to `tick` and `presence` events; ensure `status` shows linked/auth age; presence entries show Gateway host and connected clients.
|
||||
|
||||
## Safety guarantees
|
||||
|
||||
@@ -101,7 +101,7 @@ Behavior:
|
||||
- **Subsystem prefixes** on every line (e.g. `[gateway]`, `[canvas]`, `[tailscale]`)
|
||||
- **Subsystem colors** (stable per subsystem) plus level coloring
|
||||
- **Color when output is a TTY or the environment looks like a rich terminal** (`TERM`/`COLORTERM`/`TERM_PROGRAM`), respects `NO_COLOR`
|
||||
- **Shortened subsystem prefixes**: drops leading `gateway/` + `channels/`, keeps last 2 segments (e.g. `whatsapp/outbound`)
|
||||
- **Shortened subsystem prefixes**: drops leading `gateway/` + `providers/`, keeps last 2 segments (e.g. `whatsapp/outbound`)
|
||||
- **Sub-loggers by subsystem** (auto prefix + structured field `{ subsystem }`)
|
||||
- **`logRaw()`** for QR/UX output (no prefix, no formatting)
|
||||
- **Console styles** (e.g. `pretty | compact | json`)
|
||||
|
||||
@@ -81,6 +81,6 @@ Side-effecting methods require **idempotency keys** (see schema).
|
||||
|
||||
## Scope
|
||||
|
||||
This protocol exposes the **full gateway API** (status, channels, models,
|
||||
This protocol exposes the **full gateway API** (status, providers, models,
|
||||
chat, agent, sessions, nodes, etc.). The exact surface is defined by the
|
||||
TypeBox schemas in `src/gateway/protocol/schema.ts`.
|
||||
@@ -17,7 +17,7 @@ This repo supports “remote over SSH” by keeping a single Gateway (the master
|
||||
|
||||
## Command flow (what runs where)
|
||||
|
||||
One gateway daemon owns state + channels. Nodes are peripherals.
|
||||
One gateway daemon owns state + providers. Nodes are peripherals.
|
||||
|
||||
Flow example (Telegram → node):
|
||||
- Telegram message arrives at the **Gateway**.
|
||||
|
||||
+20
-128
@@ -42,27 +42,21 @@ Plugins run **in-process** with the Gateway. Treat them as trusted code:
|
||||
- Prefer explicit `plugins.allow` allowlists.
|
||||
- Review plugin config before enabling.
|
||||
- Restart the Gateway after plugin changes.
|
||||
- If you install plugins from npm (`clawdbot plugins install <npm-spec>`), treat it like running untrusted code:
|
||||
- The install path is `~/.clawdbot/extensions/<pluginId>/` (or `$CLAWDBOT_STATE_DIR/extensions/<pluginId>/`).
|
||||
- Clawdbot uses `npm pack` and then runs `npm install --omit=dev` in that directory (npm lifecycle scripts can execute code during install).
|
||||
- Prefer pinned, exact versions (`@scope/pkg@1.2.3`), and inspect the unpacked code on disk before enabling.
|
||||
|
||||
Details: [Plugins](/plugin)
|
||||
|
||||
## DM access model (pairing / allowlist / open / disabled)
|
||||
|
||||
All current DM-capable channels support a DM policy (`dmPolicy` or `*.dm.policy`) that gates inbound DMs **before** the message is processed:
|
||||
All current DM-capable providers support a DM policy (`dmPolicy` or `*.dm.policy`) that gates inbound DMs **before** the message is processed:
|
||||
|
||||
- `pairing` (default): unknown senders receive a short pairing code and the bot ignores their message until approved. Codes expire after 1 hour; repeated DMs won’t resend a code until a new request is created. Pending requests are capped at **3 per channel** by default.
|
||||
- `pairing` (default): unknown senders receive a short pairing code and the bot ignores their message until approved. Codes expire after 1 hour; repeated DMs won’t resend a code until a new request is created. Pending requests are capped at **3 per provider** by default.
|
||||
- `allowlist`: unknown senders are blocked (no pairing handshake).
|
||||
- `open`: allow anyone to DM (public). **Requires** the channel allowlist to include `"*"` (explicit opt-in).
|
||||
- `open`: allow anyone to DM (public). **Requires** the provider allowlist to include `"*"` (explicit opt-in).
|
||||
- `disabled`: ignore inbound DMs entirely.
|
||||
|
||||
Approve via CLI:
|
||||
|
||||
```bash
|
||||
clawdbot pairing list <channel>
|
||||
clawdbot pairing approve <channel> <code>
|
||||
clawdbot pairing list <provider>
|
||||
clawdbot pairing approve <provider> <code>
|
||||
```
|
||||
|
||||
Details + files on disk: [Pairing](/start/pairing)
|
||||
@@ -71,13 +65,13 @@ Details + files on disk: [Pairing](/start/pairing)
|
||||
|
||||
Clawdbot has two separate “who can trigger me?” layers:
|
||||
|
||||
- **DM allowlist** (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`): who is allowed to talk to the bot in direct messages.
|
||||
- When `dmPolicy="pairing"`, approvals are written to `~/.clawdbot/credentials/<channel>-allowFrom.json` (merged with config allowlists).
|
||||
- **Group allowlist** (channel-specific): which groups/channels/guilds the bot will accept messages from at all.
|
||||
- **DM allowlist** (`allowFrom` / `discord.dm.allowFrom` / `slack.dm.allowFrom`): who is allowed to talk to the bot in direct messages.
|
||||
- When `dmPolicy="pairing"`, approvals are written to `~/.clawdbot/credentials/<provider>-allowFrom.json` (merged with config allowlists).
|
||||
- **Group allowlist** (provider-specific): which groups/channels/guilds the bot will accept messages from at all.
|
||||
- Common patterns:
|
||||
- `channels.whatsapp.groups`, `channels.telegram.groups`, `channels.imessage.groups`: per-group defaults like `requireMention`; when set, it also acts as a group allowlist (include `"*"` to keep allow-all behavior).
|
||||
- `whatsapp.groups`, `telegram.groups`, `imessage.groups`: per-group defaults like `requireMention`; when set, it also acts as a group allowlist (include `"*"` to keep allow-all behavior).
|
||||
- `groupPolicy="allowlist"` + `groupAllowFrom`: restrict who can trigger the bot *inside* a group session (WhatsApp/Telegram/Signal/iMessage/Microsoft Teams).
|
||||
- `channels.discord.guilds` / `channels.slack.channels`: per-surface allowlists + mention defaults.
|
||||
- `discord.guilds` / `slack.channels`: per-surface allowlists + mention defaults.
|
||||
- **Security note:** treat `dmPolicy="open"` and `groupPolicy="open"` as last-resort settings. They should be barely used; prefer pairing + allowlists unless you fully trust every member of the room.
|
||||
|
||||
Details: [Configuration](/gateway/configuration) and [Groups](/concepts/groups)
|
||||
@@ -126,21 +120,6 @@ Keep config + state private on the gateway host:
|
||||
|
||||
`clawdbot doctor` can warn and offer to tighten these permissions.
|
||||
|
||||
### 0.4) Network exposure (bind + port + firewall)
|
||||
|
||||
The Gateway multiplexes **WebSocket + HTTP** on a single port:
|
||||
- Default: `18789`
|
||||
- Config/flags/env: `gateway.port`, `--port`, `CLAWDBOT_GATEWAY_PORT`
|
||||
|
||||
Bind mode controls where the Gateway listens:
|
||||
- `gateway.bind: "loopback"` (default): only local clients can connect.
|
||||
- Non-loopback binds (`"lan"`, `"tailnet"`, `"auto"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall.
|
||||
|
||||
Rules of thumb:
|
||||
- Prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback, and Tailscale handles access).
|
||||
- If you must bind to LAN, firewall the port to a tight allowlist of source IPs; do not port-forward it broadly.
|
||||
- Never expose the Gateway unauthenticated on `0.0.0.0`.
|
||||
|
||||
### 0.5) Lock down the Gateway WebSocket (local auth)
|
||||
|
||||
Gateway auth is **only** enforced when you set `gateway.auth`. If it’s unset,
|
||||
@@ -166,16 +145,6 @@ Doctor can generate one for you: `clawdbot doctor --generate-gateway-token`.
|
||||
Note: `gateway.remote.token` is **only** for remote CLI calls; it does not
|
||||
protect local WS access.
|
||||
|
||||
Auth modes:
|
||||
- `gateway.auth.mode: "token"`: shared bearer token (recommended for most setups).
|
||||
- `gateway.auth.mode: "password"`: password auth (prefer setting via env: `CLAWDBOT_GATEWAY_PASSWORD`).
|
||||
|
||||
Rotation checklist (token/password):
|
||||
1. Generate/set a new secret (`gateway.auth.token` or `CLAWDBOT_GATEWAY_PASSWORD`).
|
||||
2. Restart the Gateway (or restart the macOS app if it supervises the Gateway).
|
||||
3. Update any remote clients (`gateway.remote.token` / `.password` on machines that call into the Gateway).
|
||||
4. Verify you can no longer connect with the old credentials.
|
||||
|
||||
### 0.6) Tailscale Serve identity headers
|
||||
|
||||
When `gateway.auth.allowTailscale` is `true` (default for Serve), Clawdbot
|
||||
@@ -190,41 +159,11 @@ you terminate TLS or proxy in front of the gateway, disable
|
||||
|
||||
See [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
||||
|
||||
### 0.7) Secrets on disk (what’s sensitive)
|
||||
|
||||
Assume anything under `~/.clawdbot/` (or `$CLAWDBOT_STATE_DIR/`) may contain secrets or private data:
|
||||
|
||||
- `clawdbot.json`: config may include tokens (gateway, remote gateway), provider settings, and allowlists.
|
||||
- `credentials/**`: channel credentials (example: WhatsApp creds), pairing allowlists, legacy OAuth imports.
|
||||
- `agents/<agentId>/agent/auth-profiles.json`: API keys + OAuth tokens (imported from legacy `credentials/oauth.json`).
|
||||
- `agents/<agentId>/sessions/**`: session transcripts (`*.jsonl`) + routing metadata (`sessions.json`) that can contain private messages and tool output.
|
||||
- `extensions/**`: installed plugins (plus their `node_modules/`).
|
||||
- `sandboxes/**`: tool sandbox workspaces; can accumulate copies of files you read/write inside the sandbox.
|
||||
|
||||
Hardening tips:
|
||||
- Keep permissions tight (`700` on dirs, `600` on files).
|
||||
- Use full-disk encryption on the gateway host.
|
||||
- Prefer a dedicated OS user account for the Gateway if the host is shared.
|
||||
|
||||
### 0.8) Logs + transcripts (redaction + retention)
|
||||
|
||||
Logs and transcripts can leak sensitive info even when access controls are correct:
|
||||
- Gateway logs may include tool summaries, errors, and URLs.
|
||||
- Session transcripts can include pasted secrets, file contents, command output, and links.
|
||||
|
||||
Recommendations:
|
||||
- Keep tool summary redaction on (`logging.redactSensitive: "tools"`; default).
|
||||
- Add custom patterns for your environment via `logging.redactPatterns` (tokens, hostnames, internal URLs).
|
||||
- When sharing diagnostics, prefer `clawdbot status --all` (pasteable, secrets redacted) over raw logs.
|
||||
- Prune old session transcripts and log files if you don’t need long retention.
|
||||
|
||||
Details: [Logging](/gateway/logging)
|
||||
|
||||
### 1) DMs: pairing by default
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: { whatsapp: { dmPolicy: "pairing" } }
|
||||
whatsapp: { dmPolicy: "pairing" }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -232,11 +171,9 @@ Details: [Logging](/gateway/logging)
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"whatsapp": {
|
||||
"groups": {
|
||||
"*": { "requireMention": true }
|
||||
}
|
||||
"whatsapp": {
|
||||
"groups": {
|
||||
"*": { "requireMention": true }
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
@@ -266,29 +203,6 @@ You can already build a read-only profile by combining:
|
||||
|
||||
We may add a single `readOnlyMode` flag later to simplify this configuration.
|
||||
|
||||
### 5) Secure baseline (copy/paste)
|
||||
|
||||
One “safe default” config that keeps the Gateway private, requires DM pairing, and avoids always-on group bots:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "loopback",
|
||||
port: 18789,
|
||||
auth: { mode: "token", token: "your-long-random-token" }
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "pairing",
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want “safer by default” tool execution too, add a sandbox + deny dangerous tools for any non-owner agent (example below under “Per-agent access profiles”).
|
||||
|
||||
## Sandboxing (recommended)
|
||||
|
||||
Dedicated doc: [Sandboxing](/gateway/sandboxing)
|
||||
@@ -317,9 +231,6 @@ access those accounts and data. Treat browser profiles as **sensitive state**:
|
||||
- Prefer a dedicated profile for the agent (the default `clawd` profile).
|
||||
- Avoid pointing the agent at your personal daily-driver profile.
|
||||
- Keep host browser control disabled for sandboxed agents unless you trust them.
|
||||
- Treat browser downloads as untrusted input; prefer an isolated downloads directory.
|
||||
- Disable browser sync/password managers in the agent profile if possible (reduces blast radius).
|
||||
- For remote gateways, assume “browser control” is equivalent to “operator access” to whatever that profile can reach.
|
||||
|
||||
## Per-agent access profiles (multi-agent)
|
||||
|
||||
@@ -388,7 +299,7 @@ Common use cases:
|
||||
workspaceAccess: "none"
|
||||
},
|
||||
tools: {
|
||||
allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord"],
|
||||
allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"],
|
||||
deny: ["read", "write", "edit", "apply_patch", "exec", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
|
||||
}
|
||||
}
|
||||
@@ -414,30 +325,11 @@ Include security guidelines in your agent's system prompt:
|
||||
|
||||
If your AI does something bad:
|
||||
|
||||
### Contain
|
||||
|
||||
1. **Stop it:** stop the macOS app (if it supervises the Gateway) or terminate your `clawdbot gateway` process.
|
||||
2. **Close exposure:** set `gateway.bind: "loopback"` (or disable Tailscale Funnel/Serve) until you understand what happened.
|
||||
3. **Freeze access:** switch risky DMs/groups to `dmPolicy: "disabled"` / require mentions, and remove `"*"` allow-all entries if you had them.
|
||||
|
||||
### Rotate (assume compromise if secrets leaked)
|
||||
|
||||
1. Rotate Gateway auth (`gateway.auth.token` / `CLAWDBOT_GATEWAY_PASSWORD`) and restart.
|
||||
2. Rotate remote client secrets (`gateway.remote.token` / `.password`) on any machine that can call the Gateway.
|
||||
3. Rotate provider/API credentials (WhatsApp creds, Slack/Discord tokens, model/API keys in `auth-profiles.json`).
|
||||
|
||||
### Audit
|
||||
|
||||
1. Check Gateway logs: `/tmp/clawdbot/clawdbot-YYYY-MM-DD.log` (or `logging.file`).
|
||||
2. Review the relevant transcript(s): `~/.clawdbot/agents/<agentId>/sessions/*.jsonl`.
|
||||
3. Review recent config changes (anything that could have widened access: `gateway.bind`, `gateway.auth`, dm/group policies, `tools.elevated`, plugin changes).
|
||||
|
||||
### Collect for a report
|
||||
|
||||
- Timestamp, gateway host OS + Clawdbot version
|
||||
- The session transcript(s) + a short log tail (after redacting)
|
||||
- What the attacker sent + what the agent did
|
||||
- Whether the Gateway was exposed beyond loopback (LAN/Tailscale Funnel/Serve)
|
||||
1. **Stop it:** stop the macOS app (if it’s supervising the Gateway) or terminate your `clawdbot gateway` process
|
||||
2. **Check logs:** `/tmp/clawdbot/clawdbot-YYYY-MM-DD.log` (or your configured `logging.file`)
|
||||
3. **Review session:** Check `~/.clawdbot/agents/<agentId>/sessions/` for what happened
|
||||
4. **Rotate secrets:** If credentials were exposed
|
||||
5. **Update rules:** Add to your security prompt
|
||||
|
||||
## The Trust Hierarchy
|
||||
|
||||
|
||||
@@ -73,8 +73,6 @@ clawdbot gateway --tailscale funnel --auth password
|
||||
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
|
||||
- Set `gateway.tailscale.resetOnExit` if you want Clawdbot to undo `tailscale serve`
|
||||
or `tailscale funnel` configuration on shutdown.
|
||||
- Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic
|
||||
uses the separate bridge port (default `18790`) and is **not** proxied by Serve.
|
||||
|
||||
## Tailscale prerequisites + limits
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ When Clawdbot misbehaves, here's how to fix it.
|
||||
|
||||
Start with the FAQ’s [First 60 seconds](/start/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics.
|
||||
|
||||
Provider-specific shortcuts: [/channels/troubleshooting](/channels/troubleshooting)
|
||||
Provider-specific shortcuts: [/providers/troubleshooting](/providers/troubleshooting)
|
||||
|
||||
## Status & Diagnostics
|
||||
|
||||
@@ -21,7 +21,7 @@ Quick triage commands (in order):
|
||||
| `clawdbot status --all` | Full local diagnosis (read-only, pasteable, safe-ish) incl. log tail | When you need to share a debug report |
|
||||
| `clawdbot status --deep` | Runs gateway health checks (incl. provider probes; requires reachable gateway) | When “configured” doesn’t mean “working” |
|
||||
| `clawdbot gateway status` | Gateway discovery + reachability (local + remote targets) | When you suspect you’re probing the wrong gateway |
|
||||
| `clawdbot channels status --probe` | Asks the running gateway for channel status (and optionally probes) | When gateway is reachable but channels misbehave |
|
||||
| `clawdbot providers status --probe` | Asks the running gateway for provider status (and optionally probes) | When gateway is reachable but providers misbehave |
|
||||
| `clawdbot daemon status` | Supervisor state (launchd/systemd/schtasks), runtime PID/exit, last gateway error | When the daemon “looks loaded” but nothing runs |
|
||||
| `clawdbot logs --follow` | Live logs (best signal for runtime issues) | When you need the actual failure reason |
|
||||
|
||||
@@ -75,7 +75,7 @@ managers (pnpm/npm) because the daemon does not load your shell init. Runtime
|
||||
variables like `DISPLAY` should live in `~/.clawdbot/.env` (loaded early by the
|
||||
gateway).
|
||||
|
||||
WhatsApp + Telegram channels require **Node**; Bun is unsupported. If your
|
||||
WhatsApp + Telegram providers require **Node**; Bun is unsupported. If your
|
||||
service was installed with Bun or a version-managed Node path, run `clawdbot doctor`
|
||||
to migrate to a system Node install.
|
||||
|
||||
@@ -174,9 +174,9 @@ Look for `AllowFrom: ...` in the output.
|
||||
|
||||
**Check 2:** For group chats, is mention required?
|
||||
```bash
|
||||
# The message must match mentionPatterns or explicit mentions; defaults live in channel groups/guilds.
|
||||
# The message must match mentionPatterns or explicit mentions; defaults live in provider groups/guilds.
|
||||
# Multi-agent: `agents.list[].groupChat.mentionPatterns` overrides global patterns.
|
||||
grep -n "agents\\|groupChat\\|mentionPatterns\\|channels\\.whatsapp\\.groups\\|channels\\.telegram\\.groups\\|channels\\.imessage\\.groups\\|channels\\.discord\\.guilds" \
|
||||
grep -n "agents\\|groupChat\\|mentionPatterns\\|whatsapp\\.groups\\|telegram\\.groups\\|imessage\\.groups\\|discord\\.guilds" \
|
||||
"${CLAWDBOT_CONFIG_PATH:-$HOME/.clawdbot/clawdbot.json}"
|
||||
```
|
||||
|
||||
@@ -193,17 +193,17 @@ If `dmPolicy` is `pairing`, unknown senders should receive a code and their mess
|
||||
|
||||
**Check 1:** Is a pending request already waiting?
|
||||
```bash
|
||||
clawdbot pairing list <channel>
|
||||
clawdbot pairing list <provider>
|
||||
```
|
||||
|
||||
Pending DM pairing requests are capped at **3 per channel** by default. If the list is full, new requests won’t generate a code until one is approved or expires.
|
||||
Pending DM pairing requests are capped at **3 per provider** by default. If the list is full, new requests won’t generate a code until one is approved or expires.
|
||||
|
||||
**Check 2:** Did the request get created but no reply was sent?
|
||||
```bash
|
||||
clawdbot logs --follow | grep "pairing request"
|
||||
```
|
||||
|
||||
**Check 3:** Confirm `dmPolicy` isn’t `open`/`allowlist` for that channel.
|
||||
**Check 3:** Confirm `dmPolicy` isn’t `open`/`allowlist` for that provider.
|
||||
|
||||
### Image + Mention Not Working
|
||||
|
||||
@@ -250,7 +250,7 @@ Or use the `process` tool to background long commands.
|
||||
```bash
|
||||
# Check local status (creds, sessions, queued events)
|
||||
clawdbot status
|
||||
# Probe the running gateway + channels (WA connect + Telegram + Discord APIs)
|
||||
# Probe the running gateway + providers (WA connect + Telegram + Discord APIs)
|
||||
clawdbot status --deep
|
||||
|
||||
# View recent connection events
|
||||
@@ -266,9 +266,9 @@ clawdbot gateway --verbose
|
||||
If you’re logged out / unlinked:
|
||||
|
||||
```bash
|
||||
clawdbot channels logout
|
||||
clawdbot providers logout
|
||||
trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/credentials" # if logout can't cleanly remove everything
|
||||
clawdbot channels login --verbose # re-scan QR
|
||||
clawdbot providers login --verbose # re-scan QR
|
||||
```
|
||||
|
||||
### Media Send Failing
|
||||
@@ -356,7 +356,7 @@ Get verbose logging:
|
||||
#
|
||||
# Then run verbose commands to mirror debug output to stdout:
|
||||
clawdbot gateway --verbose
|
||||
clawdbot channels login --verbose
|
||||
clawdbot providers login --verbose
|
||||
```
|
||||
|
||||
## Log Locations
|
||||
@@ -401,7 +401,7 @@ clawdbot daemon stop
|
||||
# clawdbot daemon uninstall
|
||||
|
||||
trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}"
|
||||
clawdbot channels login # re-pair WhatsApp
|
||||
clawdbot providers login # re-pair WhatsApp
|
||||
clawdbot daemon restart # or: clawdbot gateway
|
||||
```
|
||||
|
||||
|
||||
+12
-14
@@ -23,7 +23,7 @@ read_when:
|
||||
<a href="/start/clawd">Clawdbot assistant setup</a>
|
||||
</p>
|
||||
|
||||
Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / channels.discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono).
|
||||
Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono).
|
||||
Clawdbot also powers [Clawd](https://clawd.me), the space‑lobster assistant.
|
||||
|
||||
## Start here
|
||||
@@ -62,7 +62,7 @@ WhatsApp / Telegram / Discord
|
||||
└─ Android node via Bridge + pairing
|
||||
```
|
||||
|
||||
Most operations flow through the **Gateway** (`clawdbot gateway`), a single long-running process that owns channel connections and the WebSocket control plane.
|
||||
Most operations flow through the **Gateway** (`clawdbot gateway`), a single long-running process that owns provider connections and the WebSocket control plane.
|
||||
|
||||
## Network model
|
||||
|
||||
@@ -78,7 +78,7 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
|
||||
|
||||
- 📱 **WhatsApp Integration** — Uses Baileys for WhatsApp Web protocol
|
||||
- ✈️ **Telegram Bot** — DMs + groups via grammY
|
||||
- 🎮 **Discord Bot** — DMs + guild channels via channels.discord.js
|
||||
- 🎮 **Discord Bot** — DMs + guild channels via discord.js
|
||||
- 💬 **iMessage** — Local imsg CLI integration (macOS)
|
||||
- 🤖 **Agent bridge** — Pi (RPC mode) with tool streaming
|
||||
- ⏱️ **Streaming + chunking** — Block streaming + Telegram draft streaming details ([/concepts/streaming](/concepts/streaming))
|
||||
@@ -107,7 +107,7 @@ npm install -g clawdbot@latest
|
||||
clawdbot onboard --install-daemon
|
||||
|
||||
# Pair WhatsApp Web (shows QR)
|
||||
clawdbot channels login
|
||||
clawdbot providers login
|
||||
|
||||
# Gateway runs via daemon after onboarding; manual run is still possible:
|
||||
clawdbot gateway --port 18789
|
||||
@@ -145,19 +145,17 @@ clawdbot message send --to +15555550123 --message "Hello from Clawdbot"
|
||||
Config lives at `~/.clawdbot/clawdbot.json`.
|
||||
|
||||
- If you **do nothing**, Clawdbot uses the bundled Pi binary in RPC mode with per-sender sessions.
|
||||
- If you want to lock it down, start with `channels.whatsapp.allowFrom` and (for groups) mention rules.
|
||||
- If you want to lock it down, start with `whatsapp.allowFrom` and (for groups) mention rules.
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
}
|
||||
whatsapp: {
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } }
|
||||
},
|
||||
messages: { groupChat: { mentionPatterns: ["@clawd"] } }
|
||||
routing: { groupChat: { mentionPatterns: ["@clawd"] } }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -186,9 +184,9 @@ Example:
|
||||
- Providers and UX:
|
||||
- [WebChat](/web/webchat)
|
||||
- [Control UI (browser)](/web/control-ui)
|
||||
- [Telegram](/channels/telegram)
|
||||
- [Discord](/channels/discord)
|
||||
- [iMessage](/channels/imessage)
|
||||
- [Telegram](/providers/telegram)
|
||||
- [Discord](/providers/discord)
|
||||
- [iMessage](/providers/imessage)
|
||||
- [Groups](/concepts/groups)
|
||||
- [WhatsApp group messages](/concepts/group-messages)
|
||||
- [Media: images](/nodes/images)
|
||||
|
||||
@@ -80,7 +80,7 @@ sudo systemctl restart clawdbot
|
||||
|
||||
# Provider login (run as clawdbot user)
|
||||
sudo -i -u clawdbot
|
||||
clawdbot channels login
|
||||
clawdbot providers login
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
@@ -187,7 +187,7 @@ Make sure you're running as the `clawdbot` user:
|
||||
|
||||
```bash
|
||||
sudo -i -u clawdbot
|
||||
clawdbot channels login
|
||||
clawdbot providers login
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
+3
-6
@@ -1,16 +1,13 @@
|
||||
---
|
||||
summary: "Bun workflow (experimental): installs, patches, and gotchas vs pnpm"
|
||||
summary: "Bun workflow (preferred): installs, patches, and gotchas vs pnpm"
|
||||
read_when:
|
||||
- You want the fastest local dev loop (bun + watch)
|
||||
- You hit Bun install/patch/lifecycle script issues
|
||||
---
|
||||
|
||||
# Bun (experimental)
|
||||
# Bun
|
||||
|
||||
Goal: run this repo with **Bun** (optional, not recommended for WhatsApp/Telegram)
|
||||
without losing pnpm patch behavior.
|
||||
|
||||
⚠️ **Not recommended for Gateway runtime** (WhatsApp/Telegram bugs). Use Node for production.
|
||||
Goal: run this repo with **Bun** (optional) without losing pnpm patch behavior.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -167,26 +167,26 @@ ENV NODE_ENV=production
|
||||
CMD ["node","dist/index.js"]
|
||||
```
|
||||
|
||||
### Channel setup (optional)
|
||||
### Provider setup (optional)
|
||||
|
||||
Use the CLI container to configure channels, then restart the gateway if needed.
|
||||
Use the CLI container to configure providers, then restart the gateway if needed.
|
||||
|
||||
WhatsApp (QR):
|
||||
```bash
|
||||
docker compose run --rm clawdbot-cli channels login
|
||||
docker compose run --rm clawdbot-cli providers login
|
||||
```
|
||||
|
||||
Telegram (bot token):
|
||||
```bash
|
||||
docker compose run --rm clawdbot-cli channels add --channel telegram --token "<token>"
|
||||
docker compose run --rm clawdbot-cli providers add --provider telegram --token "<token>"
|
||||
```
|
||||
|
||||
Discord (bot token):
|
||||
```bash
|
||||
docker compose run --rm clawdbot-cli channels add --channel discord --token "<token>"
|
||||
docker compose run --rm clawdbot-cli providers add --provider discord --token "<token>"
|
||||
```
|
||||
|
||||
Docs: [WhatsApp](/channels/whatsapp), [Telegram](/channels/telegram), [Discord](/channels/discord)
|
||||
Docs: [WhatsApp](/providers/whatsapp), [Telegram](/providers/telegram), [Discord](/providers/discord)
|
||||
|
||||
### Health check
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ What it does (high level):
|
||||
- `git`: clone/build a source checkout and install a wrapper script
|
||||
- On Linux: avoid global npm permission errors by switching npm’s prefix to `~/.npm-global` when needed.
|
||||
- If upgrading an existing install: runs `clawdbot doctor --non-interactive` (best effort).
|
||||
- For git installs: runs `clawdbot doctor --non-interactive` after install/update (best effort).
|
||||
- Mitigates `sharp` native install gotchas by defaulting `SHARP_IGNORE_GLOBAL_LIBVIPS=1` (avoids building against system libvips).
|
||||
|
||||
If you *want* `sharp` to link against a globally-installed libvips (or you’re debugging), set:
|
||||
|
||||
+12
-24
@@ -9,28 +9,9 @@ read_when:
|
||||
|
||||
Clawdbot is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart → verify.
|
||||
|
||||
## Recommended: re-run the website installer (upgrade in place)
|
||||
|
||||
The **preferred** update path is to re-run the installer from the website. It
|
||||
detects existing installs, upgrades in place, and runs `clawdbot doctor` when
|
||||
needed.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://clawd.bot/install.sh | bash
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Add `--no-onboard` if you don’t want the onboarding wizard to run again.
|
||||
- For **source installs**, use:
|
||||
```bash
|
||||
curl -fsSL https://clawd.bot/install.sh | bash -s -- --install-method git --no-onboard
|
||||
```
|
||||
The installer will `git pull --rebase` **only** if the repo is clean.
|
||||
- For **global installs**, the script uses `npm install -g clawdbot@latest` under the hood.
|
||||
|
||||
## Before you update
|
||||
|
||||
- Know how you installed: **global** (npm/pnpm) vs **from source** (git clone).
|
||||
- Know how you installed: **global** (npm/pnpm/bun) vs **from source** (git clone).
|
||||
- Know how your Gateway is running: **foreground terminal** vs **supervised service** (launchd/systemd).
|
||||
- Snapshot your tailoring:
|
||||
- Config: `~/.clawdbot/clawdbot.json`
|
||||
@@ -48,7 +29,10 @@ npm i -g clawdbot@latest
|
||||
```bash
|
||||
pnpm add -g clawdbot@latest
|
||||
```
|
||||
We do **not** recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs).
|
||||
|
||||
```bash
|
||||
bun add -g clawdbot@latest
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
@@ -75,7 +59,7 @@ It runs a safe-ish update flow:
|
||||
- Fetches + rebases against the configured upstream.
|
||||
- Installs deps, builds, builds the Control UI, and runs `clawdbot doctor`.
|
||||
|
||||
If you installed via **npm/pnpm** (no git metadata), `clawdbot update` will skip. Use “Update (global install)” instead.
|
||||
If you installed via **npm/pnpm/bun** (no git metadata), `clawdbot update` will skip. Use “Update (global install)” instead.
|
||||
|
||||
## Update (Control UI / RPC)
|
||||
|
||||
@@ -109,7 +93,7 @@ pnpm clawdbot health
|
||||
|
||||
Notes:
|
||||
- `pnpm build` matters when you run the packaged `clawdbot` binary ([`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js)) or use Node to run `dist/`.
|
||||
- If you run directly from TypeScript (`pnpm clawdbot ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor.
|
||||
- If you run directly from TypeScript (`pnpm clawdbot ...` / `bun run clawdbot ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor.
|
||||
- Switching between global and git installs is easy: install the other flavor, then run `clawdbot doctor` so the gateway service entrypoint is rewritten to the current install.
|
||||
|
||||
## Always run: `clawdbot doctor`
|
||||
@@ -161,6 +145,10 @@ npm i -g clawdbot@<version>
|
||||
pnpm add -g clawdbot@<version>
|
||||
```
|
||||
|
||||
```bash
|
||||
bun add -g clawdbot@<version>
|
||||
```
|
||||
|
||||
Tip: to see the current published version, run `npm view clawdbot version`.
|
||||
|
||||
Then restart + re-run doctor:
|
||||
@@ -198,4 +186,4 @@ git pull
|
||||
|
||||
- Run `clawdbot doctor` again and read the output carefully (it often tells you the fix).
|
||||
- Check: [Troubleshooting](/gateway/troubleshooting)
|
||||
- Ask in Discord: https://channels.discord.gg/clawd
|
||||
- Ask in Discord: https://discord.gg/clawd
|
||||
+4
-4
@@ -68,12 +68,12 @@ clawdbot doctor
|
||||
The Control UI’s **Logs** tab tails the same file using `logs.tail`.
|
||||
See [/web/control-ui](/web/control-ui) for how to open it.
|
||||
|
||||
### Channel-only logs
|
||||
### Provider-only logs
|
||||
|
||||
To filter channel activity (WhatsApp/Telegram/etc), use:
|
||||
To filter provider activity (WhatsApp/Telegram/etc), use:
|
||||
|
||||
```bash
|
||||
clawdbot channels logs --channel whatsapp
|
||||
clawdbot providers logs --provider whatsapp
|
||||
```
|
||||
|
||||
## Log formats
|
||||
@@ -87,7 +87,7 @@ entries to render structured output (time, level, subsystem, message).
|
||||
|
||||
Console logs are **TTY-aware** and formatted for readability:
|
||||
|
||||
- Subsystem prefixes (e.g. `gateway/channels/whatsapp`)
|
||||
- Subsystem prefixes (e.g. `gateway/providers/whatsapp`)
|
||||
- Level coloring (info/warn/error)
|
||||
- Optional compact or JSON mode
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ read_when:
|
||||
---
|
||||
# Image & Media Support — 2025-12-05
|
||||
|
||||
The WhatsApp channel runs via **Baileys Web**. This document captures the current media handling rules for send, gateway, and agent replies.
|
||||
Clawdbot is now **web-only** (Baileys). This document captures the current media handling rules for send, gateway, and agent replies.
|
||||
|
||||
## Goals
|
||||
- Send media with optional captions via `clawdbot message send --media`.
|
||||
@@ -15,9 +15,9 @@ The WhatsApp channel runs via **Baileys Web**. This document captures the curren
|
||||
## CLI Surface
|
||||
- `clawdbot message send --media <path-or-url> [--message <caption>]`
|
||||
- `--media` optional; caption can be empty for media-only sends.
|
||||
- `--dry-run` prints the resolved payload; `--json` emits `{ channel, to, messageId, mediaUrl, caption }`.
|
||||
- `--dry-run` prints the resolved payload; `--json` emits `{ provider, to, messageId, mediaUrl, caption }`.
|
||||
|
||||
## WhatsApp Web channel behavior
|
||||
## Web Provider Behavior
|
||||
- Input: local file path **or** HTTP(S) URL.
|
||||
- Flow: load into a Buffer, detect media kind, and build the correct payload:
|
||||
- **Images:** resize & recompress to JPEG (max side 2048px) targeting `agents.defaults.mediaMaxMb` (default 5 MB), capped at 6 MB.
|
||||
|
||||
@@ -96,7 +96,7 @@ It can set up:
|
||||
- `~/clawd` workspace bootstrap
|
||||
- `~/.clawdbot/clawdbot.json` config
|
||||
- model auth profiles
|
||||
- model provider config/login
|
||||
- provider config/login
|
||||
- Linux systemd **user** service (daemon)
|
||||
|
||||
If you’re doing OAuth on a headless VM: do OAuth on a normal machine first, then copy the auth profile to the VM (see [FAQ](/start/faq)).
|
||||
|
||||
@@ -6,8 +6,7 @@ read_when:
|
||||
---
|
||||
# Platforms
|
||||
|
||||
Clawdbot core is written in TypeScript. **Node is the recommended runtime**.
|
||||
Bun is not recommended for the Gateway (WhatsApp/Telegram bugs).
|
||||
Clawdbot core is written in TypeScript, so the CLI + Gateway run anywhere Node or Bun runs.
|
||||
|
||||
Companion apps exist for macOS (menu bar app) and mobile nodes (iOS/Android). Windows and
|
||||
Linux companion apps are planned, but the Gateway is fully supported today.
|
||||
|
||||
Loaded 100 of 1987 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user