Compare commits

...

32 Commits

Author SHA1 Message Date
Peter Steinberger
e8779ac329 fix: handle Anthropic overloaded_error gracefully (#832) (thanks @danielz1z) 2026-01-13 08:32:06 +00:00
danielz1z
36725ce153 fix: handle Anthropic overloaded_error gracefully
When Anthropic's API returns an overloaded_error (temporary capacity issue),
the raw JSON error was being sent to users instead of a friendly message.

Changes:
- Add overloaded error pattern detection
- Return user-friendly message for overloaded errors
- Classify overloaded as failover-worthy (triggers retry logic)
2026-01-13 08:18:59 +00:00
Peter Steinberger
0d537ece10 docs: add minimax coding plan referral 2026-01-13 08:15:44 +00:00
Peter Steinberger
f825dd2897 chore(lint): format live profiles test 2026-01-13 08:03:58 +00:00
Peter Steinberger
9faa95d558 fix: recover from compaction overflow 2026-01-13 08:03:11 +00:00
Peter Steinberger
365cbe8d50 docs: clarify Discord requireMention placement 2026-01-13 08:03:11 +00:00
Peter Steinberger
9a322d52e2 docs: update faq and install guidance 2026-01-13 08:03:11 +00:00
Peter Steinberger
89013efbca docs: fix Discord invite link in showcase 2026-01-13 08:03:11 +00:00
Peter Steinberger
3a90335b5a chore(lint): satisfy biome checks 2026-01-13 07:53:38 +00:00
Peter Steinberger
676b64e8a3 chore(changelog): note auth profile order fix 2026-01-13 07:53:38 +00:00
Peter Steinberger
9007920695 fix(auth): drop invalid auth profiles from order 2026-01-13 07:53:38 +00:00
Peter Steinberger
2887376646 test: stabilize gpt-5.2 tool-only live check 2026-01-13 07:51:24 +00:00
Peter Steinberger
e48d452c63 docs: credit MiniMax agent-dir fix 2026-01-13 07:30:54 +00:00
Peter Steinberger
165841ae79 fix: suppress experimental sqlite warning at startup 2026-01-13 07:27:32 +00:00
Peter Steinberger
76acdb7ae7 test: silence experimental sqlite warnings in docker 2026-01-13 07:27:32 +00:00
Peter Steinberger
dfbe4041f5 fix: skip Control UI asset check when UI is skipped 2026-01-13 07:27:32 +00:00
Peter Steinberger
4fd1a6dec3 fix: harden doctor install checks 2026-01-13 07:25:25 +00:00
Peter Steinberger
aa394d0e14 chore: update clawtributors (thanks @vrknetha) 2026-01-13 07:19:53 +00:00
Peter Steinberger
d5b17d344b Merge pull request #722 from vrknetha/feat/slash-bash-command
feat(bash): add host-only /bash slash command
2026-01-13 07:18:35 +00:00
Peter Steinberger
8111e18dbd docs: add Opus/MiniMax fallback examples 2026-01-13 07:15:20 +00:00
Peter Steinberger
ba7d12f205 refactor: centralize onboarding auth paths 2026-01-13 07:12:20 +00:00
Peter Steinberger
ef66ad3b52 docs: group FAQ sections 2026-01-13 07:07:48 +00:00
Peter Steinberger
69e4339af9 Tests: run e2e gateway with node 2026-01-13 07:06:41 +00:00
Peter Steinberger
779904657f Docs: reorder 2025.1.12 changelog 2026-01-13 07:03:44 +00:00
Peter Steinberger
cb0f6cefa4 Deps: update Pi + Vitest and add Bedrock docs 2026-01-13 06:57:11 +00:00
Peter Steinberger
25ef01b74a docs: add FAQ table of contents 2026-01-13 06:55:00 +00:00
Peter Steinberger
6db0201fcd Merge pull request #583 from mitschabaude-bot/feat/agent-model-fallbacks
Config: per-agent model fallbacks
2026-01-13 06:54:00 +00:00
Peter Steinberger
40e508823f Merge pull request #444 from grp06/feature/xhigh-thinking-models
Thinking: gate xhigh by model
2026-01-13 06:51:39 +00:00
Peter Steinberger
18d22aa426 fix: gate xhigh by model (#444) (thanks @grp06) 2026-01-13 06:48:41 +00:00
George Pickett
a3641526ab Thinking: gate xhigh by model 2026-01-13 06:48:26 +00:00
vrknetha
721a6bf0f9 Docs: document bash chat command 2026-01-13 11:56:51 +05:30
vrknetha
25a5f1cb96 Auto-reply: add host-only /bash + ! bash command 2026-01-13 11:54:34 +05:30
82 changed files with 3661 additions and 802 deletions

View File

@@ -1,120 +1,55 @@
# Changelog
## 2026.1.13-1
### Changes
- Cron: accept ISO timestamps for one-shot schedules (UTC) and allow optional delete-after-run; wired into CLI + macOS editor.
- Gateway: add Tailscale binary discovery, custom bind mode, and probe auth retry for password changes. (#740 — thanks @jeffersonwarrior)
- Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow)
- Agents: support per-agent model fallbacks via `agents.list[].model`. (#583 — thanks @mitschabaude-bot)
- Tools: add tool profiles plus group shorthands for tool policy allow/deny (global, per-agent, sandbox).
### Fixes
- Gateway: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides when checking or restarting the daemon.
- Tools: harden tool input schemas for strict providers (no unions; flattened actions; enum helpers + tests).
- Discord: allow clearing channel parents via message tool without nullable schema inputs.
## 2026.1.12-4
### Changes
- Models/Moonshot: add Kimi K2 0905 + turbo/thinking variants to the preset + docs. (#818 — thanks @mickahouan)
- Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm)
- Auth: add Chutes OAuth (PKCE + refresh + onboarding choice). (#726 — thanks @FrieSei)
- Agents: make workspace bootstrap truncation configurable (default 20k) and warn when files are truncated.
### Fixes
- Typing: keep typing indicators alive during tool execution. (#450, #447 — thanks @thewilloftheshadow)
- Cron: coerce enabled patches so disabling jobs persists correctly. (#205 — thanks @thewilloftheshadow)
- Control UI: keep chat scroll position unless user is near the bottom. (#217 — thanks @thewilloftheshadow)
- Fallback: treat credential validation failures ("no credentials found", "no API key found") as auth errors that trigger model fallback. (#822 — thanks @sebslight)
- Telegram: preserve forum topic thread ids, including General topic replies. (#727 — thanks @thewilloftheshadow)
- Telegram: persist polling update offsets across restarts to avoid duplicate updates. (#739 — thanks @thewilloftheshadow)
- Discord: avoid duplicate message/reaction listeners on monitor reloads. (#744 — thanks @thewilloftheshadow)
- System events: include local timestamps when events are injected into prompts. (#245 — thanks @thewilloftheshadow)
- Cron: accept `jobId` aliases for cron update/run/remove params in gateway validation. (#252 — thanks @thewilloftheshadow)
- Models/Google: normalize Gemini 3 model ids to preview variants before runtime selection. (#795 — thanks @thewilloftheshadow)
- Models/Google: strip Gemini CLI tool call/response ids in patched provider handling. (#783 — thanks @ananth-vardhan-cn)
- TUI: keep the last streamed response instead of replacing it with "(no output)". (#747 — thanks @thewilloftheshadow)
- Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow)
- Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm)
- Onboarding: quote Windows browser URLs when launching via `cmd start` to preserve OAuth query params. (#794 — thanks @roshanasingh4)
- Gateway/Auth: allow Tailscale Serve identity headers to satisfy token auth when `allowTailscale` is enabled. (#823 — thanks @roshanasingh4)
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
- Telegram: respect account-scoped bindings when webhook mode is enabled. (#821 — thanks @gumadeiras)
- Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus)
- Browser tools: treat explicit `maxChars: 0` as unlimited while keeping the default limit only when omitted. (#796 — thanks @gabriel-trigo)
- Tools: allow Claude/Gemini tool param aliases (`file_path`, `old_string`, `new_string`) while enforcing required params at runtime. (#793 — thanks @hsrvc)
- Gemini: downgrade tool-call history missing `thought_signature` to avoid INVALID_ARGUMENT errors. (#793 — thanks @hsrvc)
- Messaging: enforce context isolation for message tool sends across providers (normalized targets + tests). (#793 — thanks @hsrvc)
- Auto-reply: re-evaluate reasoning tag enforcement on fallback providers to prevent leaked reasoning. (#810 — thanks @mcinteerj)
- Tools/Gemini: drop null-only union variants while cleaning tool schemas to avoid Cloud Code Assist schema errors. (#782 — thanks @AbhisekBasu1)
- Connections UI: polish multi-account account cards in the Connections view. (#816 — thanks @steipete)
- Gemini: strip Claude `msg_*` thought_signature fields from session history to avoid base64 decode errors. (#805 — thanks @marcmarg)
## 2026.1.12-3
### Changes
- Sandbox: drop legacy `memory` tool-policy shorthand; require explicit `group:memory`.
### Fixes
- Telegram: tolerate mocked bots missing native-command APIs (`setMyCommands`, `command`) during tests.
- Auto-reply: fix streaming block reply media handling (no redeclared/use-before-declare vars).
## 2026.1.12-2
### Changes
- Subagents: add config to set default sub-agent model (`agents.defaults.subagents.model` + per-agent override); still overridden by `sessions_spawn.model`.
- Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI).
- Sandbox: support tool-policy groups in `tools.sandbox.tools` (e.g. `group:memory`, `group:fs`) to reduce config churn.
### Fixes
- Models/MiniMax: strip malformed tool invocation XML (`<invoke>...</invoke>` and `</minimax:tool_call>`) from assistant text to prevent tool call leaks into user messages. (#809 — thanks @latitudeki5223)
- Tools/Models: MiniMax vision now uses the Coding Plan VLM endpoint (`/v1/coding_plan/vlm`) so the `image` tool works with MiniMax keys (also accepts `@/path/to/file.png`-style inputs).
- Gateway/macOS: reduce noisy loopback WS "closed before connect" logs during tests.
- Auto-reply: resolve ambiguous `/model` fuzzy matches by picking the best candidate instead of erroring.
## 2026.1.12-1
### Changes
- Heartbeat: raise default `ackMaxChars` to 300 so any `HEARTBEAT_OK` replies with short padding stay internal (fewer noisy heartbeat posts on providers).
- Onboarding: normalize API key inputs (strip `export KEY=...` wrappers) so shell-style entries paste cleanly.
## 2026.1.12 (Unreleased)
## 2025.1.12 (Unreleased)
### Highlights
- Memory: add vector search for agent memories (Markdown-only scope) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback.
- 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).
- 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.
### Changes
- Agents: strengthen memory recall guidance (memory_search mandatory for past work/preferences; system prompt injects conditional recall section; memory_get now described as safe snippet fetch).
- Browser: add `scrollintoview` action to scroll refs into view before click/type.
- Memory: embedding providers support OpenAI or local `node-llama-cpp`; config adds defaults + per-agent overrides, provider/fallback metadata surfaced in tools/CLI.
- CLI/Tools: new `clawdbot memory` commands plus `memory_search`/`memory_get` tools returning snippets + line ranges and provider info.
- Runtime: memory index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default; inline status replies now stay auth-gated while inline prompts continue to the agent.
- Doctor: rebuild Control UI assets when protocol schema is newer to avoid stale UI connect errors. (#786) — thanks @meaningfool.
- Cron: allow jobs to target a specific agent and expose agent selection in the macOS app + Control UI.
- Discord: add `discord.allowBots` to permit bot-authored messages (still ignores its own messages) with docs warning about bot loops. (#802) — thanks @zknicker.
- CLI/Onboarding: `clawdbot dashboard` prints/copies the tokenized Control UI link and opens it; onboarding now auto-opens the dashboard with your token and keeps the link in the summary.
- Commands: native slash commands now default to `"auto"` (on for Discord/Telegram, off for Slack) with per-provider overrides (`discord/telegram/slack.commands.native`) and docs updated.
- Sandbox: allow Docker bind mounts via `docker.binds`; merges global + per-agent binds (per-agent ignored under shared scope) for custom host paths. (#790 — thanks @akonyer)
- Models: add Synthetic provider (Anthropic-compatible) and trim legacy MiniMax M2 from default catalogs. (#811 — thanks @siraht)
### 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.
- 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`.
- 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
- Auto-reply: inline `/status` now honors allowlists (authorized stripped + replied inline; unauthorized leaves text for the agent) to match command gating tests.
- Models: normalize `${ENV_VAR}` apiKey config values and auto-fill missing provider `apiKey` from env/auth when custom provider models are configured (fixes MiniMax “Unknown model” on fresh installs).
- Models/Tools: include `MiniMax-VL-01` in implicit MiniMax provider so image pairing uses a real vision model.
- Telegram: show typing indicator in General forum topics. (#779) — thanks @azade-c.
- Discord: keep reasoning italics intact when messages are chunked, so reasoning stays italic across multi-part sends.
- Models: keep explicit GitHub Copilot provider config and honor agent-dir auth profiles for auto-injection. (#705) — thanks @TAGOOZ.
- Auto-reply: restore 300-char heartbeat ack limit and keep >300 char replies instead of dropping them; adjust long heartbeat test content accordingly.
- Gateway: `agents.list` now honors explicit `agents.list` config without pulling stray agents from disk; GitHub Copilot CLI auth path uses the updated provider build.
- Google: apply patched pi-ai `google-gemini-cli` function call handling (strips ids) after upgrading to pi-ai 0.43.0.
- Tools: allow Gemini/Claude-style aliases (`file_path`, `old_string`, `new_string`) in read/write/edit schemas while still enforcing required params at runtime to avoid validation loops.
- Auto-reply: elevated/reasoning toggles now enqueue system events so the model sees the mode change immediately.
- Tools: keep `image` available in sandbox and fail over when image models return empty output (fixes “(no text returned)”).
- Discord: add per-channel `autoThread` to auto-create threads for top-level messages. (#800) — thanks @davidguttman.
- Discord: fix autoThread routing so replies stay in the created thread and avoid reply references. (#807) — thanks @davidguttman.
- Onboarding: TUI defaults to `deliver: false` to avoid cross-provider auto-delivery leaks; onboarding spawns the TUI with explicit `deliver: false`. (#791 — thanks @roshanasingh4)
- 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.
- 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.
- 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.
- 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.
- 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: 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.
- 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.
- Testing: update Vitest + browser-playwright to 4.0.17.
- Docs: add Amazon Bedrock provider notes and link from models/FAQ.
## 2026.1.11

View File

@@ -247,7 +247,7 @@ Send these in WhatsApp/Telegram/Slack/Microsoft Teams/WebChat (group commands ar
- `/status` — compact session status (model + tokens, cost when available)
- `/new` or `/reset` — reset the session
- `/compact` — compact session context (summary)
- `/think <level>` — off|minimal|low|medium|high
- `/think <level>` — off|minimal|low|medium|high|xhigh (GPT-5.2 + Codex models only)
- `/verbose on|off`
- `/cost on|off` — append per-response token/cost usage lines
- `/restart` — restart the gateway (owner-only in groups)
@@ -467,21 +467,21 @@ Special thanks to @andrewting19 for the Anthropic OAuth tool-name fix.
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/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/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/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/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/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/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>
<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/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/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/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/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/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/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/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/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=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/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/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>

View File

@@ -105,7 +105,7 @@ Isolation options (only for `session=isolated`):
### Model and thinking overrides
Isolated jobs (`agentTurn`) can override the model and thinking level:
- `model`: Provider/model string (e.g., `anthropic/claude-sonnet-4-20250514`) or alias (e.g., `opus`)
- `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`)
- `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only)
Note: You can set `model` on main-session jobs too, but it changes the shared main
session model. We recommend model overrides only for isolated jobs to avoid

71
docs/bedrock.md Normal file
View File

@@ -0,0 +1,71 @@
---
summary: "Use Amazon Bedrock (Converse API) models with Clawdbot"
read_when:
- You want to use Amazon Bedrock models with Clawdbot
- You need AWS credential/region setup for model calls
---
# Amazon Bedrock
Clawdbot can use **Amazon Bedrock** models via pi‑ai’s **Bedrock Converse**
streaming provider. Bedrock auth uses the **AWS SDK default credential chain**,
not an API key.
## What pi‑ai supports
- Provider: `amazon-bedrock`
- API: `bedrock-converse-stream`
- Auth: AWS credentials (env vars, shared config, or instance role)
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
## Setup (manual)
1) Ensure AWS credentials are available on the **gateway host**:
```bash
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-east-1"
# Optional:
export AWS_SESSION_TOKEN="..."
export AWS_PROFILE="your-profile"
```
2) Add a Bedrock provider and model to your config:
```json5
{
models: {
providers: {
"amazon-bedrock": {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
models: [
{
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
name: "Claude 3.7 Sonnet (Bedrock)",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192
}
]
}
}
},
agents: {
defaults: {
model: { primary: "amazon-bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0" }
}
}
}
```
## Notes
- Bedrock requires **model access** enabled in your AWS account/region.
- If you use profiles, set `AWS_PROFILE` on the gateway host.
- Reasoning support depends on the model; check the Bedrock model card for
current capabilities.
- If you prefer a managed key flow, you can also place an OpenAI‑compatible
proxy in front of Bedrock and configure it as an OpenAI provider instead.

View File

@@ -257,12 +257,12 @@ Options:
- `--tailscale-reset-on-exit`
- `--install-daemon`
- `--no-install-daemon` (alias: `--skip-daemon`)
- `--daemon-runtime <node|bun>`
- `--daemon-runtime <node|bun>` (bun not recommended for WhatsApp/Telegram)
- `--skip-providers`
- `--skip-skills`
- `--skip-health`
- `--skip-ui`
- `--node-manager <npm|pnpm|bun>`
- `--node-manager <npm|pnpm|bun>` (pnpm recommended; bun not recommended for Gateway runtime)
- `--json`
### `configure` / `config`
@@ -398,7 +398,7 @@ Required:
Options:
- `--to <dest>` (for session key and optional delivery)
- `--session-id <id>`
- `--thinking <off|minimal|low|medium|high>`
- `--thinking <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
- `--verbose <on|off>`
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
- `--local`
@@ -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; use `--runtime bun` only when WhatsApp is disabled.
- `daemon install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs).
- `daemon install` options: `--port`, `--runtime`, `--token`, `--force`.
### `logs`

View File

@@ -9,7 +9,7 @@ read_when:
Safely update a **source checkout** (git install) of Clawdbot.
If you installed via **npm/pnpm/bun** (global install, no git metadata), use the package manager flow in [Updating](/install/updating).
If you installed via **npm/pnpm** (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/bun/npm depending on the checkout).
3. Installs deps (pnpm preferred; npm fallback).
4. Builds + builds the Control UI.
5. Runs `clawdbot doctor` as the final “safe update” check.

View File

@@ -92,6 +92,9 @@ This maps to:
`*.blockStreaming` is explicitly set to `true`. Telegram can stream drafts
(`telegram.streamMode`) without block replies.
Config location reminder: the `blockStreaming*` defaults live under
`agents.defaults`, not the root config.
## Telegram draft streaming (token-ish)
Telegram is the only provider with draft streaming:

View File

@@ -39,7 +39,7 @@ Add `hooks.gmail.model` config option to specify an optional cheaper model for G
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `hooks.gmail.model` | `string` | (none) | Model to use for Gmail hook processing. Accepts `provider/model` refs or aliases from `agents.defaults.models`. |
| `hooks.gmail.thinking` | `string` | (inherited) | Thinking level override (`off`, `minimal`, `low`, `medium`, `high`). If unset, inherits from `agents.defaults.thinkingDefault` or model's default. |
| `hooks.gmail.thinking` | `string` | (inherited) | Thinking level override (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only). If unset, inherits from `agents.defaults.thinkingDefault` or model's default. |
### Alias Support

View File

@@ -746,8 +746,10 @@ Controls how chat commands are enabled across connectors.
```json5
{
commands: {
native: false, // register native commands when supported
native: "auto", // register native commands when supported (auto)
text: true, // parse slash commands in chat messages
bash: false, // allow ! (alias: /bash) (host-only; requires tools.elevated allowlists)
bashForegroundMs: 2000, // bash foreground window (0 backgrounds immediately)
config: false, // allow /config (writes to disk)
debug: false, // allow /debug (runtime-only overrides)
restart: false, // allow /restart + gateway restart tool
@@ -761,6 +763,8 @@ Notes:
- `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 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`).
- `commands.debug: true` enables `/debug` (runtime-only overrides).
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
@@ -1213,6 +1217,27 @@ 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

View File

@@ -76,6 +76,7 @@ cat ~/.clawdbot/clawdbot.json
- 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

View File

@@ -73,6 +73,8 @@ 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

View File

@@ -1,13 +1,16 @@
---
summary: "Bun workflow (preferred): installs, patches, and gotchas vs pnpm"
summary: "Bun workflow (experimental): 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
# Bun (experimental)
Goal: run this repo with **Bun** (optional) without losing pnpm patch behavior.
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.
## Status

View File

@@ -30,6 +30,7 @@ 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:

View File

@@ -9,9 +9,28 @@ 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/bun) vs **from source** (git clone).
- Know how you installed: **global** (npm/pnpm) 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`
@@ -29,10 +48,7 @@ npm i -g clawdbot@latest
```bash
pnpm add -g clawdbot@latest
```
```bash
bun add -g clawdbot@latest
```
We do **not** recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs).
Then:
@@ -59,7 +75,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/bun** (no git metadata), `clawdbot update` will skip. Use “Update (global install)” instead.
If you installed via **npm/pnpm** (no git metadata), `clawdbot update` will skip. Use “Update (global install)” instead.
## Update (Control UI / RPC)
@@ -93,7 +109,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 ...` / `bun run clawdbot ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor.
- If you run directly from TypeScript (`pnpm 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`
@@ -145,10 +161,6 @@ 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:

View File

@@ -6,7 +6,8 @@ read_when:
---
# Platforms
Clawdbot core is written in TypeScript, so the CLI + Gateway run anywhere Node or Bun runs.
Clawdbot core is written in TypeScript. **Node is the recommended runtime**.
Bun is not recommended for the Gateway (WhatsApp/Telegram bugs).
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.

View File

@@ -6,7 +6,8 @@ read_when:
---
# Linux App
The Gateway is fully supported on Linux. The core is written in TypeScript, so it runs anywhere Node or Bun runs.
The Gateway is fully supported on Linux. **Node is the recommended runtime**.
Bun is not recommended for the Gateway (WhatsApp/Telegram bugs).
Native Linux companion apps are planned. Contributions are welcome if you want to help build one.
@@ -23,7 +24,7 @@ Step-by-step VPS guide: [exe.dev](/platforms/exe-dev)
## Install
- [Getting Started](/start/getting-started)
- [Install & updates](/install/updating)
- Optional flows: [Bun](/install/bun), [Nix](/install/nix), [Docker](/install/docker)
- Optional flows: [Bun (experimental)](/install/bun), [Nix](/install/nix), [Docker](/install/docker)
## Gateway
- [Gateway runbook](/gateway)

View File

@@ -20,7 +20,7 @@ You need Node 22+ on the Mac, then install `clawdbot` globally:
npm install -g clawdbot@<version>
```
The macOS app’s **Install CLI** button runs the same flow via npm/pnpm/bun.
The macOS app’s **Install CLI** button runs the same flow via npm/pnpm (bun not recommended for Gateway runtime).
## Launchd (Gateway as LaunchAgent)

View File

@@ -18,7 +18,7 @@ node.
- Runs or connects to the Gateway (local or remote).
- Exposes macOS‑only tools (Canvas, Camera, Screen Recording, `system.run`).
- Optionally hosts **PeekabooBridge** for UI automation.
- Installs the global CLI (`clawdbot`) via npm/pnpm/bun on request.
- Installs the global CLI (`clawdbot`) via npm/pnpm on request (bun not recommended for the Gateway runtime).
## Local vs remote mode

View File

@@ -174,6 +174,9 @@ Notes:
- The bot lacks channel permissions (View/Send/Read History), or
- Your config requires mentions and you didn’t mention it, or
- Your guild/channel allowlist denies the channel/user.
- **`requireMention: false` but still no replies**:
- `discord.groupPolicy` defaults to **allowlist**; you must either set it to `"open"` or explicitly list the channel under `discord.guilds.<id>.channels`.
- `requireMention` must live under `discord.guilds` (or a specific channel). `discord.requireMention` at the top level is ignored.
- **Permission audits** (`providers status --probe`) only check numeric channel IDs. If you use slugs/names as `discord.guilds.*.channels` keys, the audit can’t verify permissions.
- **DMs don’t work**: `discord.dm.enabled=false`, `discord.dm.policy="disabled"`, or you haven’t been approved yet (`discord.dm.policy="pairing"`).

View File

@@ -72,6 +72,28 @@ Configure via CLI:
}
```
### MiniMax M2.1 as fallback (Opus primary)
**Best for:** keep Opus 4.5 as primary, fail over to MiniMax M2.1.
```json5
{
env: { MINIMAX_API_KEY: "sk-..." },
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"]
}
}
}
}
```
### Optional: Local via LM Studio (manual)
**Best for:** local inference with LM Studio.
@@ -134,5 +156,27 @@ Use the interactive config wizard to set MiniMax without editing JSON:
- Model refs are `minimax/<model>`.
- Update pricing values in `models.json` if you need exact cost tracking.
- Referral link for MiniMax Coding Plan (10% off): https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link
- See [/concepts/model-providers](/concepts/model-providers) for provider rules.
- Use `clawdbot models list` and `clawdbot models set minimax/MiniMax-M2.1` to switch.
## Troubleshooting
### “Unknown model: minimax/MiniMax-M2.1”
This usually means the **MiniMax provider isn’t configured** (no provider entry
and no MiniMax auth profile/env key found). A fix for this detection is in
**2026.1.12** (unreleased at the time of writing). Fix by:
- Upgrading to **2026.1.12** (or run from source `main`), then restarting the gateway.
- Running `clawdbot configure` and selecting **MiniMax M2.1**, or
- Adding the `models.providers.minimax` block manually, or
- Setting `MINIMAX_API_KEY` (or a MiniMax auth profile) so the provider can be injected.
Make sure the model id is **case‑sensitive**:
- `minimax/MiniMax-M2.1`
- `minimax/MiniMax-M2.1-lightning`
Then recheck with:
```bash
clawdbot models list
```

View File

@@ -31,6 +31,7 @@ model as `provider/model`.
- [Z.AI](/providers/zai)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
- [Amazon Bedrock](/bedrock)
For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration,
see [Model providers](/concepts/model-providers).

View File

@@ -310,4 +310,5 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- Fix: `clawdbot doctor` (or restart the gateway). If it persists, relink via `providers login` and inspect `clawdbot logs --follow`.
**Bun runtime**
- WhatsApp uses Baileys; run the gateway with **Node** for WhatsApp. (See Getting Started runtime note.)
- Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun.
Run the gateway with **Node**. (See Getting Started runtime note.)

View File

@@ -5,6 +5,123 @@ summary: "Frequently asked questions about Clawdbot setup, configuration, and us
Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, multi-agent, OAuth/API keys, model failover). For runtime diagnostics, see [Troubleshooting](/gateway/troubleshooting). For the full config reference, see [Configuration](/gateway/configuration).
## Table of contents
- [What is Clawdbot?](#what-is-clawdbot)
- [What is Clawdbot, in one paragraph?](#what-is-clawdbot-in-one-paragraph)
- [Quick start and first-run setup](#quick-start-and-first-run-setup)
- [What’s the recommended way to install and set up Clawdbot?](#whats-the-recommended-way-to-install-and-set-up-clawdbot)
- [How do I open the dashboard after onboarding?](#how-do-i-open-the-dashboard-after-onboarding)
- [How do I authenticate the dashboard (token) on localhost vs remote?](#how-do-i-authenticate-the-dashboard-token-on-localhost-vs-remote)
- [What runtime do I need?](#what-runtime-do-i-need)
- [What does the onboarding wizard actually do?](#what-does-the-onboarding-wizard-actually-do)
- [How does Anthropic "setup-token" auth work?](#how-does-anthropic-setup-token-auth-work)
- [Do you support Claude subscription auth (Claude Code OAuth)?](#do-you-support-claude-subscription-auth-claude-code-oauth)
- [Is AWS Bedrock supported?](#is-aws-bedrock-supported)
- [How does Codex auth work?](#how-does-codex-auth-work)
- [Is a local model OK for casual chats?](#is-a-local-model-ok-for-casual-chats)
- [How do I keep hosted model traffic in a specific region?](#how-do-i-keep-hosted-model-traffic-in-a-specific-region)
- [Can I use Bun?](#can-i-use-bun)
- [Telegram: what goes in `allowFrom`?](#telegram-what-goes-in-allowfrom)
- [Can multiple people use one WhatsApp number with different Clawdbots?](#can-multiple-people-use-one-whatsapp-number-with-different-clawdbots)
- [Can I run a "fast chat" agent and an "Opus for coding" agent?](#can-i-run-a-fast-chat-agent-and-an-opus-for-coding-agent)
- [Does Homebrew work on Linux?](#does-homebrew-work-on-linux)
- [Can I switch between npm and git installs later?](#can-i-switch-between-npm-and-git-installs-later)
- [Should I run the Gateway on my laptop or a VPS?](#should-i-run-the-gateway-on-my-laptop-or-a-vps)
- [Skills and automation](#skills-and-automation)
- [How do I customize skills without keeping the repo dirty?](#how-do-i-customize-skills-without-keeping-the-repo-dirty)
- [Can I load skills from a custom folder?](#can-i-load-skills-from-a-custom-folder)
- [How can I use different models for different tasks?](#how-can-i-use-different-models-for-different-tasks)
- [How do I install skills on Linux?](#how-do-i-install-skills-on-linux)
- [Do you have a Notion or HeyGen integration?](#do-you-have-a-notion-or-heygen-integration)
- [Sandboxing and memory](#sandboxing-and-memory)
- [Is there a dedicated sandboxing doc?](#is-there-a-dedicated-sandboxing-doc)
- [How do I bind a host folder into the sandbox?](#how-do-i-bind-a-host-folder-into-the-sandbox)
- [How does memory work?](#how-does-memory-work)
- [Does semantic memory search require an OpenAI API key?](#does-semantic-memory-search-require-an-openai-api-key)
- [Where things live on disk](#where-things-live-on-disk)
- [Where does Clawdbot store its data?](#where-does-clawdbot-store-its-data)
- [Where should AGENTS.md / SOUL.md / USER.md / MEMORY.md live?](#where-should-agentsmd--soulmd--usermd--memorymd-live)
- [How do I completely uninstall Clawdbot?](#how-do-i-completely-uninstall-clawdbot)
- [Can agents work outside the workspace?](#can-agents-work-outside-the-workspace)
- [I’m in remote mode — where is the session store?](#im-in-remote-mode-where-is-the-session-store)
- [Config basics](#config-basics)
- [What format is the config? Where is it?](#what-format-is-the-config-where-is-it)
- [I set `gateway.bind: "lan"` (or `"tailnet"`) and now nothing listens / the UI says unauthorized](#i-set-gatewaybind-lan-or-tailnet-and-now-nothing-listens-the-ui-says-unauthorized)
- [Why do I need a token on localhost now?](#why-do-i-need-a-token-on-localhost-now)
- [Do I have to restart after changing config?](#do-i-have-to-restart-after-changing-config)
- [How do I run a central Gateway with specialized workers across devices?](#how-do-i-run-a-central-gateway-with-specialized-workers-across-devices)
- [Can the Clawdbot browser run headless?](#can-the-clawdbot-browser-run-headless)
- [Remote gateways + nodes](#remote-gateways-nodes)
- [How do commands propagate between Telegram, the gateway, and nodes?](#how-do-commands-propagate-between-telegram-the-gateway-and-nodes)
- [Do nodes run a gateway daemon?](#do-nodes-run-a-gateway-daemon)
- [Is there an API / RPC way to apply config?](#is-there-an-api-rpc-way-to-apply-config)
- [What’s a minimal “sane” config for a first install?](#whats-a-minimal-sane-config-for-a-first-install)
- [How do I set up Tailscale on a VPS and connect from my Mac?](#how-do-i-set-up-tailscale-on-a-vps-and-connect-from-my-mac)
- [How do I connect a Mac node to a remote Gateway (Tailscale Serve)?](#how-do-i-connect-a-mac-node-to-a-remote-gateway-tailscale-serve)
- [Env vars and .env loading](#env-vars-and-env-loading)
- [How does Clawdbot load environment variables?](#how-does-clawdbot-load-environment-variables)
- [“I started the Gateway via a daemon and my env vars disappeared.” What now?](#i-started-the-gateway-via-a-daemon-and-my-env-vars-disappeared-what-now)
- [I set `COPILOT_GITHUB_TOKEN`, but models status shows “Shell env: off.” Why?](#i-set-copilot_github_token-but-models-status-shows-shell-env-off-why)
- [Sessions & multiple chats](#sessions-multiple-chats)
- [How do I start a fresh conversation?](#how-do-i-start-a-fresh-conversation)
- [How do I completely reset Clawdbot (but keep it installed)?](#how-do-i-completely-reset-clawdbot-but-keep-it-installed)
- [I’m getting “context too large” errors — how do I reset or compact?](#im-getting-context-too-large-errors-how-do-i-reset-or-compact)
- [Do I need to add a “bot account” to a WhatsApp group?](#do-i-need-to-add-a-bot-account-to-a-whatsapp-group)
- [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group)
- [Do groups/threads share context with DMs?](#do-groupsthreads-share-context-with-dms)
- [Models: defaults, selection, aliases, switching](#models-defaults-selection-aliases-switching)
- [What is the “default model”?](#what-is-the-default-model)
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
- [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
- [Why do I see “Unknown model: minimax/MiniMax-M2.1”?](#why-do-i-see-unknown-model-minimaxminimax-m21)
- [Can I use MiniMax as my default and OpenAI for complex tasks?](#can-i-use-minimax-as-my-default-and-openai-for-complex-tasks)
- [Are opus / sonnet / gpt built‑in shortcuts?](#are-opus-sonnet-gpt-builtin-shortcuts)
- [How do I define/override model shortcuts (aliases)?](#how-do-i-defineoverride-model-shortcuts-aliases)
- [How do I add models from other providers like OpenRouter or Z.AI?](#how-do-i-add-models-from-other-providers-like-openrouter-or-zai)
- [Model failover and “All models failed”](#model-failover-and-all-models-failed)
- [How does failover work?](#how-does-failover-work)
- [What does this error mean?](#what-does-this-error-mean)
- [Fix checklist for `No credentials found for profile "anthropic:default"`](#fix-checklist-for-no-credentials-found-for-profile-anthropicdefault)
- [Why did it also try Google Gemini and fail?](#why-did-it-also-try-google-gemini-and-fail)
- [Auth profiles: what they are and how to manage them](#auth-profiles-what-they-are-and-how-to-manage-them)
- [What is an auth profile?](#what-is-an-auth-profile)
- [What are typical profile IDs?](#what-are-typical-profile-ids)
- [Can I control which auth profile is tried first?](#can-i-control-which-auth-profile-is-tried-first)
- [OAuth vs API key: what’s the difference?](#oauth-vs-api-key-whats-the-difference)
- [Gateway: ports, “already running”, and remote mode](#gateway-ports-already-running-and-remote-mode)
- [What port does the Gateway use?](#what-port-does-the-gateway-use)
- [Why does `clawdbot daemon status` say `Runtime: running` but `RPC probe: failed`?](#why-does-clawdbot-daemon-status-say-runtime-running-but-rpc-probe-failed)
- [Why does `clawdbot daemon status` show `Config (cli)` and `Config (daemon)` different?](#why-does-clawdbot-daemon-status-show-config-cli-and-config-daemon-different)
- [What does “another gateway instance is already listening” mean?](#what-does-another-gateway-instance-is-already-listening-mean)
- [How do I run Clawdbot in remote mode (client connects to a Gateway elsewhere)?](#how-do-i-run-clawdbot-in-remote-mode-client-connects-to-a-gateway-elsewhere)
- [The Control UI says “unauthorized” (or keeps reconnecting). What now?](#the-control-ui-says-unauthorized-or-keeps-reconnecting-what-now)
- [I set `gateway.bind: "tailnet"` but it can’t bind / nothing listens](#i-set-gatewaybind-tailnet-but-it-cant-bind-nothing-listens)
- [Can I run multiple Gateways on the same host?](#can-i-run-multiple-gateways-on-the-same-host)
- [What does “invalid handshake” / code 1008 mean?](#what-does-invalid-handshake--code-1008-mean)
- [Logging and debugging](#logging-and-debugging)
- [Where are logs?](#where-are-logs)
- [How do I start/stop/restart the Gateway daemon?](#how-do-i-startstoprestart-the-gateway-daemon)
- [What’s the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails)
- [Media & attachments](#media-attachments)
- [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent)
- [Security and access control](#security-and-access-control)
- [Is it safe to expose Clawdbot to inbound DMs?](#is-it-safe-to-expose-clawdbot-to-inbound-dms)
- [WhatsApp: will it message my contacts? How does pairing work?](#whatsapp-will-it-message-my-contacts-how-does-pairing-work)
- [Chat commands, aborting tasks, and “it won’t stop”](#chat-commands-aborting-tasks-and-it-wont-stop)
- [How do I stop/cancel a running task?](#how-do-i-stopcancel-a-running-task)
- [Why does it feel like the bot “ignores” rapid‑fire messages?](#why-does-it-feel-like-the-bot-ignores-rapidfire-messages)
- [Common troubleshooting](#common-troubleshooting)
- [“All models failed” — what should I check first?](#all-models-failed-what-should-i-check-first)
- [I’m running on my personal WhatsApp number — why is self-chat weird?](#im-running-on-my-personal-whatsapp-number-why-is-self-chat-weird)
- [WhatsApp logged me out. How do I re‑auth?](#whatsapp-logged-me-out-how-do-i-reauth)
- [Build errors on `main` — what’s the standard fix path?](#build-errors-on-main-whats-the-standard-fix-path)
- [npm install fails (allow-build-scripts / missing tar or yargs). What now?](#npm-install-fails-allow-build-scripts--missing-tar-or-yargs-what-now)
- [How do I switch between git installs and npm installs?](#how-do-i-switch-between-git-installs-and-npm-installs)
- [Telegram block streaming isn’t splitting text between tool calls. Why?](#telegram-block-streaming-isnt-splitting-text-between-tool-calls-why)
- [Discord doesn’t reply in my server even with `requireMention: false`. Why?](#discord-doesnt-reply-in-my-server-even-with-requiremention-false-why)
- [Cloud Code Assist API error: invalid tool schema (400). What now?](#cloud-code-assist-api-error-invalid-tool-schema-400-what-now)
## First 60 seconds if something's broken
1) **Quick status (first check)**
@@ -60,7 +177,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the always‑on control plane; the assistant is the product.
## Quick start and first‑run setup
## Quick start and first-run setup
### What’s the recommended way to install and set up Clawdbot?
@@ -103,7 +220,7 @@ See [Dashboard](/web/dashboard) and [Web surfaces](/web) for bind modes and auth
### What runtime do I need?
Node **>= 22** is required. `pnpm` is recommended; `bun` is optional.
Node **>= 22** is required. `pnpm` is recommended. Bun is **not recommended** for the Gateway.
### What does the onboarding wizard actually do?
@@ -126,9 +243,11 @@ The wizard can run `claude setup-token` on the gateway host (or you run it yours
Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also supports **setup-token**. If you have a Claude subscription, we recommend **setup-token** on the gateway host for the most reliable long‑running setup (requires Claude Pro/Max + the `claude` CLI). OAuth reuse is supported, but avoid logging in separately via Clawdbot and Claude Code to prevent token conflicts. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice.
### Is AWS Bedrock supported?
Not currently. Clawdbot doesn’t ship a Bedrock provider today. If you must use Bedrock, the common workaround is an OpenAI‑compatible proxy in front of Bedrock, then point Clawdbot at that endpoint. See [Model providers](/providers/models) and [Model providers (full list)](/concepts/model-providers).
Yes — via pi‑ai’s **Amazon Bedrock (Converse)** provider with **manual config**. You must supply AWS credentials/region on the gateway host and add a Bedrock provider entry in your models config. See [Amazon Bedrock](/bedrock) and [Model providers](/providers/models). If you prefer a managed key flow, an OpenAI‑compatible proxy in front of Bedrock is still a valid option.
### How does Codex auth work?
@@ -144,7 +263,11 @@ Pick region-pinned endpoints. OpenRouter exposes US-hosted options for MiniMax,
### Can I use Bun?
Bun is supported for faster TypeScript execution, but **WhatsApp requires Node** in this ecosystem. The wizard lets you pick the runtime; choose **Node** if you use WhatsApp.
Bun is **not recommended**. We see runtime bugs, especially with WhatsApp and Telegram.
Use **Node** for stable gateways.
If you still want to experiment with Bun, do it on a non‑production gateway
without WhatsApp/Telegram.
### Telegram: what goes in `allowFrom`?
@@ -196,6 +319,25 @@ clawdbot daemon restart
Doctor detects a gateway service entrypoint mismatch and offers to rewrite the service config to match the current install (use `--repair` in automation).
### Should I run the Gateway on my laptop or a VPS?
Short answer: **if you want 24/7 reliability, use a VPS**. If you want the
lowest friction and you’re okay with sleep/restarts, run it locally.
**Laptop (local Gateway)**
- **Pros:** no server cost, direct access to local files, live browser window.
- **Cons:** sleep/network drops = disconnects, OS updates/reboots interrupt, must stay awake.
**VPS / cloud**
- **Pros:** always‑on, stable network, no laptop sleep issues, easier to keep running.
- **Cons:** often run headless (use screenshots), remote file access only, you must SSH for updates.
**Clawdbot‑specific note:** WhatsApp/Telegram/Slack/Discord all work fine from a VPS. The only real trade‑off is **headless browser** vs a visible window. See [Browser](/tools/browser).
**Recommended default:** VPS if you had gateway disconnects before. Local is great when you’re actively using the Mac and want local file access or UI automation with a visible browser.
## Skills and automation
### How do I customize skills without keeping the repo dirty?
Use managed overrides instead of editing the repo copy. Put your changes in `~/.clawdbot/skills/<name>/SKILL.md` (or add a folder via `skills.load.extraDirs` in `~/.clawdbot/clawdbot.json`). Precedence is `<workspace>/skills` > `~/.clawdbot/skills` > bundled, so managed overrides win without touching git. Only upstream-worthy edits should live in the repo and go out as PRs.
@@ -216,6 +358,7 @@ See [Cron jobs](/automation/cron-jobs), [Multi-Agent Routing](/concepts/multi-ag
### How do I install skills on Linux?
Use **ClawdHub** (CLI) or drop skills into your workspace. The macOS Skills UI isn’t available on Linux.
Browse skills at https://clawdhub.com.
Install the ClawdHub CLI (pick one package manager):
@@ -227,9 +370,20 @@ npm i -g clawdhub
pnpm add -g clawdhub
```
```bash
bun add -g clawdhub
```
### Do you have a Notion or HeyGen integration?
Not built‑in today.
Options:
- **Custom skill / plugin:** best for reliable API access (Notion/HeyGen both have APIs).
- **Browser automation:** works without code but is slower and more fragile.
If you want to keep context per client (agency workflows), a simple pattern is:
- One Notion page per client (context + preferences + active work).
- Ask the agent to fetch that page at the start of a session.
If you want a native integration, open a feature request or build a skill
targeting those APIs.
Install skills:
@@ -240,6 +394,8 @@ clawdhub update --all
ClawdHub installs into `./skills` under your current directory (or falls back to your configured Clawdbot workspace); Clawdbot treats that as `<workspace>/skills` on the next session. For shared skills across agents, place them in `~/.clawdbot/skills/<name>/SKILL.md`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills) and [ClawdHub](/tools/clawdhub).
## Sandboxing and memory
### Is there a dedicated sandboxing doc?
Yes. See [Sandboxing](/gateway/sandboxing). For Docker-specific setup (full gateway in Docker or sandbox images), see [Docker](/install/docker).
@@ -290,6 +446,29 @@ Legacy single‑agent path: `~/.clawdbot/agent/*` (migrated by `clawdbot doctor`
Your **workspace** (AGENTS.md, memory files, skills, etc.) is separate and configured via `agents.defaults.workspace` (default: `~/clawd`).
### Where should AGENTS.md / SOUL.md / USER.md / MEMORY.md live?
These files live in the **agent workspace**, not `~/.clawdbot`.
- **Workspace (per agent)**: `AGENTS.md`, `SOUL.md`, `IDENTITY.md`, `USER.md`,
`MEMORY.md` (or `memory.md`), `memory/YYYY-MM-DD.md`, optional `HEARTBEAT.md`.
- **State dir (`~/.clawdbot`)**: config, credentials, auth profiles, sessions, logs,
and shared skills (`~/.clawdbot/skills`).
Default workspace is `~/clawd`, configurable via:
```json5
{
agents: { defaults: { workspace: "~/clawd" } }
}
```
If the bot “forgets” after a restart, confirm the Gateway is using the same
workspace on every launch (and remember: remote mode uses the **gateway host’s**
workspace, not your local laptop).
See [Agent workspace](/concepts/agent-workspace) and [Memory](/concepts/memory).
### How do I completely uninstall Clawdbot?
See the dedicated guide: [Uninstall](/install/uninstall).
@@ -394,6 +573,11 @@ Yes. It’s a config option:
Default is `false` (headful). Headless is more likely to trigger anti‑bot checks on some sites. See [Browser](/tools/browser).
Headless uses the **same Chromium engine** and works for most automation (forms, clicks, scraping, logins). The main differences:
- No visible browser window (use screenshots if you need visuals).
- Some sites are stricter about automation in headless mode (CAPTCHAs, anti‑bot).
For example, X/Twitter often blocks headless sessions.
## Remote gateways + nodes
### How do commands propagate between Telegram, the gateway, and nodes?
@@ -427,6 +611,52 @@ Yes. `config.apply` validates + writes the full config and restarts the Gateway
This sets your workspace and restricts who can trigger the bot.
### How do I set up Tailscale on a VPS and connect from my Mac?
Minimal steps:
1) **Install + login on the VPS**
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
```
2) **Install + login on your Mac**
- Use the Tailscale app and sign in to the same tailnet.
3) **Enable MagicDNS (recommended)**
- In the Tailscale admin console, enable MagicDNS so the VPS has a stable name.
4) **Use the tailnet hostname**
- SSH: `ssh user@your-vps.tailnet-xxxx.ts.net`
- Gateway WS: `ws://your-vps.tailnet-xxxx.ts.net:18789`
If you want the Control UI without SSH, use Tailscale Serve on the VPS:
```bash
clawdbot gateway --tailscale serve
```
This keeps the gateway bound to loopback and exposes HTTPS via Tailscale. See [Tailscale](/gateway/tailscale).
### How do I connect a Mac node to a remote Gateway (Tailscale Serve)?
Serve only exposes the **Gateway Control UI**. Nodes use the **bridge port**.
Recommended setup:
1) **Enable the bridge on the gateway host**:
```json5
{
bridge: { enabled: true, bind: "auto" }
}
```
`auto` prefers a tailnet IP when Tailscale is present.
2) **Make sure the VPS + Mac are on the same tailnet**.
3) **Use the macOS app in Remote mode** (SSH target can be the tailnet hostname).
The app will tunnel the bridge port and connect as a node.
4) **Approve the node** on the gateway:
```bash
clawdbot nodes pending
clawdbot nodes approve <requestId>
```
Docs: [Bridge protocol](/gateway/bridge-protocol), [Discovery](/gateway/discovery), [macOS remote mode](/platforms/mac/remote).
## Env vars and .env loading
### How does Clawdbot load environment variables?
@@ -472,6 +702,30 @@ Two common fixes:
This runs your login shell and imports only missing expected keys (never overrides). Env var equivalents:
`CLAWDBOT_LOAD_SHELL_ENV=1`, `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`.
### I set `COPILOT_GITHUB_TOKEN`, but models status shows “Shell env: off.” Why?
`clawdbot models status` reports whether **shell env import** is enabled. “Shell env: off”
does **not** mean your env vars are missing — it just means Clawdbot won’t load
your login shell automatically.
If the Gateway runs as a daemon (launchd/systemd), it won’t inherit your shell
environment. Fix by doing one of these:
1) Put the token in `~/.clawdbot/.env`:
```
COPILOT_GITHUB_TOKEN=...
```
2) Or enable shell import (`env.shellEnv.enabled: true`).
3) Or add it to your config `env` block (applies only if missing).
Then restart the gateway and recheck:
```bash
clawdbot models status
```
Copilot tokens are read from `COPILOT_GITHUB_TOKEN` (also `GH_TOKEN` / `GITHUB_TOKEN`).
See [/concepts/model-providers](/concepts/model-providers) and [/environment](/environment).
## Sessions & multiple chats
### How do I start a fresh conversation?
@@ -503,6 +757,28 @@ Notes:
- If you used profiles (`--profile` / `CLAWDBOT_PROFILE`), reset each state dir (defaults are `~/.clawdbot-<profile>`).
- Dev reset: `clawdbot gateway --dev --reset` (dev-only; wipes dev config + credentials + sessions + workspace).
### I’m getting “context too large” errors — how do I reset or compact?
Use one of these:
- **Compact** (keeps the conversation but summarizes older turns):
```
/compact
```
or `/compact <instructions>` to guide the summary.
- **Reset** (fresh session ID for the same chat key):
```
/new
/reset
```
If it keeps happening:
- Enable or tune **session pruning** (`agents.defaults.contextPruning`) to trim old tool output.
- Use a model with a larger context window.
Docs: [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning), [Session management](/concepts/session).
### Do I need to add a “bot account” to a WhatsApp group?
No. Clawdbot runs on **your own account**, so if you’re in the group, Clawdbot can see it.
@@ -587,6 +863,59 @@ Model "provider/model" is not allowed. Use /model to list available models.
That error is returned **instead of** a normal reply. Fix: add the model to
`agents.defaults.models`, remove the allowlist, or pick a model from `/model list`.
### Why do I see “Unknown model: minimax/MiniMax-M2.1”?
This means the **provider isn’t configured** (no MiniMax provider config or auth
profile was found), so the model can’t be resolved. A fix for this detection is
in **2026.1.12** (unreleased at the time of writing).
Fix checklist:
1) Upgrade to **2026.1.12** (or run from source `main`), then restart the gateway.
2) Make sure MiniMax is configured (wizard or JSON), or that a MiniMax API key
exists in env/auth profiles so the provider can be injected.
3) Use the exact model id (case‑sensitive): `minimax/MiniMax-M2.1` or
`minimax/MiniMax-M2.1-lightning`.
4) Run:
```bash
clawdbot models list
```
and pick from the list (or `/model list` in chat).
See [MiniMax](/providers/minimax) and [Models](/concepts/models).
### Can I use MiniMax as my default and OpenAI for complex tasks?
Yes. Use **MiniMax as the default** and switch models **per session** when needed.
Fallbacks are for **errors**, not “hard tasks,” so use `/model` or a separate agent.
**Option A: switch per session**
```json5
{
env: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "minimax/MiniMax-M2.1" },
models: {
"minimax/MiniMax-M2.1": { alias: "minimax" },
"openai/gpt-5.2": { alias: "gpt" }
}
}
}
}
```
Then:
```
/model gpt
```
**Option B: separate agents**
- Agent A default: MiniMax
- Agent B default: OpenAI
- Route by agent or use `/agent` to switch
Docs: [Models](/concepts/models), [Multi-Agent Routing](/concepts/multi-agent), [MiniMax](/providers/minimax), [OpenAI](/providers/openai).
### Are opus / sonnet / gpt built‑in shortcuts?
Yes. Clawdbot ships a few default shorthands (only applied when the model exists in `agents.defaults.models`):
@@ -684,6 +1013,23 @@ It means the system attempted to use the auth profile ID `anthropic:default`, bu
- **Sanity‑check model/auth status**
- Use `clawdbot models status` to see configured models and whether providers are authenticated.
### Fix checklist for `No credentials found for profile "anthropic:claude-cli"`
This means the run is pinned to the **Claude CLI** profile, but the Gateway
can’t find that profile in its auth store.
- **Sync the Claude CLI token on the gateway host**
- Run `clawdbot models status` (it loads + syncs Claude CLI credentials).
- If it still says missing: run `claude setup-token` (or `clawdbot models auth setup-token --provider anthropic`) and retry.
- **If you want to use an API key instead**
- Put `ANTHROPIC_API_KEY` in `~/.clawdbot/.env` on the **gateway host**.
- Clear any pinned order that forces `anthropic:claude-cli`:
```bash
clawdbot models auth order clear --provider anthropic
```
- **Confirm you’re running commands on the gateway host**
- In remote mode, auth profiles live on the gateway machine, not your laptop.
### Why did it also try Google Gemini and fail?
If your model config includes Google Gemini as a fallback (or you switched to a Gemini shorthand), Clawdbot will try it during model fallback. If you haven’t configured Google credentials, you’ll see `No API key found for provider "google"`.
@@ -826,6 +1172,8 @@ Fix:
Fix:
- Start Tailscale on that host (so it has a 100.x address), or
- Switch to `gateway.bind: "loopback"` / `"lan"`.
Note: `tailnet` is legacy and is migrated to `auto` by Doctor. Prefer `gateway.bind: "auto"` when using Tailscale.
### Can I run multiple Gateways on the same host?
@@ -845,6 +1193,29 @@ Quick setup (recommended):
Profiles also suffix service names (`com.clawdbot.<profile>`, `clawdbot-gateway-<profile>.service`, `Clawdbot Gateway (<profile>)`).
### What does “invalid handshake” / code 1008 mean?
The Gateway is a **WebSocket server**, and it expects the very first message to
be a `connect` frame. If it receives anything else, it closes the connection
with **code 1008** (policy violation).
Common causes:
- You opened the **HTTP** URL in a browser (`http://...`) instead of a WS client.
- You used the wrong port or path.
- A proxy or tunnel stripped auth headers or sent a non‑Gateway request.
Quick fixes:
1) Use the WS URL: `ws://<host>:18789` (or `wss://...` if HTTPS).
2) Don’t open the WS port in a normal browser tab.
3) If auth is on, include the token/password in the `connect` frame.
If you’re using the CLI or TUI, the URL should look like:
```
clawdbot tui --url ws://<host>:18789 --token <token>
```
Protocol details: [Gateway protocol](/gateway/protocol).
## Logging and debugging
### Where are logs?
@@ -1009,6 +1380,96 @@ clawdbot providers login
3) Check GitHub issues or Discord
4) Temporary workaround: check out an older commit
### npm install fails (allow-build-scripts / missing tar or yargs). What now?
If you’re running from source, use the repo’s package manager: **pnpm** (preferred).
The repo declares `packageManager: "pnpm@…"`, and pnpm patches are tracked in `pnpm.patchedDependencies`.
Typical recovery:
```bash
git status # ensure you’re in the repo root
pnpm install
pnpm build
pnpm clawdbot doctor
clawdbot daemon restart
```
Why: pnpm is the configured package manager for this repo, and the dependency
patching workflow relies on it.
### How do I switch between git installs and npm installs?
Use the **website installer** and select the install method with a flag. It
upgrades in place and rewrites the gateway service to point at the new install.
Switch **to git install**:
```bash
curl -fsSL https://clawd.bot/install.sh | bash -s -- --install-method git --no-onboard
```
Switch **to npm global**:
```bash
curl -fsSL https://clawd.bot/install.sh | bash
```
Notes:
- The git flow only rebases if the repo is clean. Commit or stash changes first.
- After switching, run:
```bash
clawdbot doctor
clawdbot daemon restart
```
### Telegram block streaming isn’t splitting text between tool calls. Why?
Block streaming only sends **completed text blocks**. Common reasons you see a single message:
- `agents.defaults.blockStreamingDefault` is still `"off"`.
- `telegram.blockStreaming` is set to `false`.
- `telegram.streamMode` is `partial` or `block` **and draft streaming is active**
(private chat + topics). Draft streaming disables block streaming in that case.
- Your `minChars` / coalesce settings are too high, so chunks get merged.
- The model emits one large text block (no mid‑reply flush points).
Fix checklist:
1) Put block streaming settings under `agents.defaults`, not the root.
2) Set `telegram.streamMode: "off"` if you want real multi‑message block replies.
3) Use smaller chunk/coalesce thresholds while debugging.
See [Streaming](/concepts/streaming).
### Discord doesn’t reply in my server even with `requireMention: false`. Why?
`requireMention` only controls mention‑gating **after** the channel passes allowlists.
By default `discord.groupPolicy` is **allowlist**, so guild channels must be explicitly enabled.
Fix checklist:
1) Set `discord.groupPolicy: "open"` **or** add the guild/channel allowlist.
2) Use **numeric channel IDs** in `discord.guilds.<guildId>.channels`.
3) Put `requireMention: false` **under** `discord.guilds` (global or per‑channel).
Top‑level `discord.requireMention` is not a supported key.
4) Ensure the bot has **Message Content Intent** and channel permissions.
5) Run `clawdbot providers status --probe` for audit hints.
Docs: [Discord](/providers/discord), [Providers troubleshooting](/providers/troubleshooting).
### Cloud Code Assist API error: invalid tool schema (400). What now?
This is almost always a **tool schema compatibility** issue. The Cloud Code Assist
endpoint accepts a strict subset of JSON Schema. Clawdbot scrubs/normalizes tool
schemas in current `main`, but the fix is not in the last release yet (as of
January 13, 2026).
Fix checklist:
1) **Update Clawdbot**:
- If you can run from source, pull `main` and restart the gateway.
- Otherwise, wait for the next release that includes the schema scrubber.
2) Avoid unsupported keywords like `anyOf/oneOf/allOf`, `patternProperties`,
`additionalProperties`, `minLength`, `maxLength`, `format`, etc.
3) If you define custom tools, keep the top‑level schema as `type: "object"` with
`properties` and simple enums.
See [Tools](/tools) and [TypeBox schemas](/concepts/typebox).
## Answer the exact question from the screenshot/chat log
**Q: “What’s the default model for Anthropic with an API key?”**

View File

@@ -79,7 +79,7 @@ What you’ll choose:
- **Auth**: OpenAI Code (Codex) subscription (OAuth) or API keys. For Anthropic we recommend an API key; `claude setup-token` is also supported.
- **Providers**: WhatsApp QR login, Telegram/Discord bot tokens, etc.
- **Daemon**: background install (launchd/systemd; WSL2 uses systemd)
- **Runtime**: Node (recommended; required for WhatsApp) or Bun (faster, but incompatible with WhatsApp)
- **Runtime**: Node (recommended; required for WhatsApp/Telegram). Bun is **not recommended**.
- **Gateway token**: the wizard generates one by default (even on loopback) and stores it in `gateway.auth.token`.
Wizard doc: [Wizard](/start/wizard)
@@ -110,11 +110,8 @@ clawdbot gateway --port 18789 --verbose
Dashboard (local loopback): `http://127.0.0.1:18789/`
If a token is configured, paste it into the Control UI settings (stored as `connect.params.auth.token`).
⚠️ **WhatsApp + Bun warning:** Baileys (WhatsApp Web library) uses a WebSocket
path that is currently incompatible with Bun and can cause memory corruption on
reconnect. If you use WhatsApp, run the Gateway with **Node** until this is
resolved. Baileys: https://github.com/WhiskeySockets/Baileys · Bun issue:
https://github.com/oven-sh/bun/issues/5951
⚠️ **Bun warning (WhatsApp + Telegram):** Bun has known issues with these
providers. If you use WhatsApp or Telegram, run the Gateway with **Node**.
## 4) Pair + connect your first chat surface

View File

@@ -27,7 +27,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Docker](/install/docker)
- [Nix](/install/nix)
- [Updating / rollback](/install/updating)
- [Bun workflow](/install/bun)
- [Bun workflow (experimental)](/install/bun)
## Core concepts

View File

@@ -64,7 +64,7 @@ Onboarding requests TCC permissions needed for:
## 5) CLI (optional)
The app can install the global `clawdbot` CLI via npm/pnpm/bun so terminal
The app can install the global `clawdbot` CLI via npm/pnpm so terminal
workflows and launchd tasks work out of the box.
## 6) Onboarding chat (dedicated session)

View File

@@ -9,7 +9,7 @@ summary: "Community-built projects and integrations powered by Clawdbot"
Real projects from the community. See what people are building with Clawdbot.
<Info>
**Want to be featured?** Share your project in [#showcase on Discord](https://discord.gg/clawdbot) or [tag @clawdbot on X](https://x.com/clawdbot).
**Want to be featured?** Share your project in [#showcase on Discord](https://discord.gg/clawd) or [tag @clawdbot on X](https://x.com/clawdbot).
</Info>
## 🎥 Clawdbot in Action
@@ -366,7 +366,7 @@ Have something to share? We'd love to feature it!
<Steps>
<Step title="Share It">
Post in [#showcase on Discord](https://discord.gg/clawdbot) or [tweet @clawdbot](https://x.com/clawdbot)
Post in [#showcase on Discord](https://discord.gg/clawd) or [tweet @clawdbot](https://x.com/clawdbot)
</Step>
<Step title="Include Details">
Tell us what it does, link to the repo/demo, share a screenshot if you have one

View File

@@ -119,7 +119,7 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
- Linux (and Windows via WSL2): systemd user unit
- Wizard attempts to enable lingering via `loginctl enable-linger <user>` so the Gateway stays up after logout.
- May prompt for sudo (writes `/var/lib/systemd/linger`); it tries without sudo first.
- **Runtime selection:** Node (recommended; required for WhatsApp) or Bun (faster, but incompatible with WhatsApp).
- **Runtime selection:** Node (recommended; required for WhatsApp/Telegram). Bun is **not recommended**.
7) **Health check**
- Starts the Gateway (if needed) and runs `clawdbot health`.
@@ -127,7 +127,7 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
8) **Skills (recommended)**
- Reads the available skills and checks requirements.
- Lets you choose a node manager: **npm / pnpm / bun**.
- Lets you choose a node manager: **npm / pnpm** (bun not recommended).
- Installs optional dependencies (some use Homebrew on macOS).
9) **Finish**

View File

@@ -38,7 +38,7 @@ clawdbot agent --to +15555550123 --message "Summon reply" --deliver
- `--local`: run locally (requires provider keys in your shell)
- `--deliver`: send the reply to the chosen provider (requires `--to`)
- `--provider`: `whatsapp|telegram|discord|slack|signal|imessage` (default: `whatsapp`)
- `--thinking <off|minimal|low|medium|high>`: persist thinking level
- `--thinking <off|minimal|low|medium|high|xhigh>`: persist thinking level (GPT-5.2 + Codex models only)
- `--verbose <on|off>`: persist verbose level
- `--timeout <seconds>`: override agent timeout
- `--json`: output structured JSON

View File

@@ -42,10 +42,6 @@ npm i -g clawdhub
pnpm add -g clawdhub
```
```bash
bun add -g clawdhub
```
## How it fits into Clawdbot
By default, the CLI installs skills into `./skills` under your current working directory. If a Clawdbot workspace is configured, `clawdhub` falls back to that workspace unless you override `--workdir` (or `CLAWDHUB_WORKDIR`). Clawdbot loads workspace skills from `<workspace>/skills` and will pick them up in the **next** session. If you already use `~/.clawdbot/skills` or bundled skills, workspace skills take precedence.

View File

@@ -7,6 +7,7 @@ read_when:
## What it does
- Elevated mode allows the exec tool to run with elevated privileges when the feature is available and the sender is approved.
- The bash chat command (`!`; `/bash` alias) uses the same `tools.elevated` allowlists because it always runs on the host.
- **Optional for sandboxed agents**: elevated only changes behavior when the agent is running in a sandbox. If the agent already runs unsandboxed, elevated is effectively a no-op.
- Directive forms: `/elevated on`, `/elevated off`, `/elev on`, `/elev off`.
- Only `on|off` are accepted; anything else returns a hint and does not change state.

View File

@@ -20,7 +20,7 @@ All skills-related configuration lives under `skills` in `~/.clawdbot/clawdbot.j
},
install: {
preferBrew: true,
nodeManager: "npm" // npm | pnpm | yarn | bun
nodeManager: "npm" // npm | pnpm | yarn | bun (Gateway runtime still Node; bun not recommended)
},
entries: {
"nano-banana-pro": {
@@ -44,6 +44,8 @@ All skills-related configuration lives under `skills` in `~/.clawdbot/clawdbot.j
- `load.extraDirs`: additional skill directories to scan (lowest precedence).
- `install.preferBrew`: prefer brew installers when available (default: true).
- `install.nodeManager`: node installer preference (`npm` | `pnpm` | `yarn` | `bun`, default: npm).
This only affects **skill installs**; the Gateway runtime should still be Node
(Bun not recommended for WhatsApp/Telegram).
- `entries.<skillKey>`: per-skill overrides.
Per-skill fields:

View File

@@ -45,8 +45,9 @@ surface those skills teach.
## ClawdHub (install + sync)
ClawdHub is the public skills registry for Clawdbot. Use it to discover,
install, update, and back up skills. Full guide: [ClawdHub](/tools/clawdhub).
ClawdHub is the public skills registry for Clawdbot. Browse at
https://clawdhub.com. Use it to discover, install, update, and back up skills.
Full guide: [ClawdHub](/tools/clawdhub).
Common flows:
@@ -117,6 +118,8 @@ metadata: {"clawdbot":{"emoji":"♊️","requires":{"bins":["gemini"]},"install"
Notes:
- If multiple installers are listed, the gateway picks a **single** preferred option (brew when available, otherwise node).
- Node installs honor `skills.install.nodeManager` in `clawdbot.json` (default: npm; options: npm/pnpm/yarn/bun).
This only affects **skill installs**; the Gateway runtime should still be Node
(Bun is not recommended for WhatsApp/Telegram).
- Go installs: if `go` is missing and `brew` is available, the gateway installs Go via Homebrew first and sets `GOBIN` to Homebrew’s `bin` when possible.
If no `metadata.clawdbot` is present, the skill is always eligible (unless
@@ -201,6 +204,6 @@ See [Skills config](/tools/skills-config) for the full configuration schema.
## Looking for more skills?
Browse [ClawdHub](/tools/clawdhub).
Browse https://clawdhub.com.
---

View File

@@ -7,6 +7,7 @@ read_when:
# Slash commands
Commands are handled by the Gateway. Most commands must be sent as a **standalone** message that starts with `/`.
The host-only bash chat command uses `! <cmd>` (with `/bash <cmd>` as an alias).
There are two related systems:
@@ -26,6 +27,8 @@ They run immediately, are stripped before the model sees the message, and the re
commands: {
native: "auto",
text: true,
bash: false,
bashForegroundMs: 2000,
config: false,
debug: false,
restart: false,
@@ -40,6 +43,8 @@ They run immediately, are stripped before the model sees the message, and the re
- Auto: on for Discord/Telegram; off for Slack (until you add slash commands); ignored for providers without native support.
- Set `discord.commands.native`, `telegram.commands.native`, or `slack.commands.native` to override per provider (bool or `"auto"`).
- `false` clears previously registered commands on Discord/Telegram at startup. Slack commands are managed in the Slack app and are not removed automatically.
- `commands.bash` (default `false`) enables `! <cmd>` to run host shell commands (`/bash <cmd>` is an alias; requires `tools.elevated` allowlists).
- `commands.bashForegroundMs` (default `2000`) controls how long bash waits before switching to background mode (`0` backgrounds immediately).
- `commands.config` (default `false`) enables `/config` (reads/writes `clawdbot.json`).
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
- `commands.useAccessGroups` (default `true`) enforces allowlists/policies for commands.
@@ -60,15 +65,19 @@ Text + native (when enabled):
- `/activation mention|always` (groups only)
- `/send on|off|inherit` (owner-only)
- `/reset` or `/new`
- `/think <level>` (aliases: `/thinking`, `/t`)
- `/think <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only; aliases: `/thinking`, `/t`)
- `/verbose on|off` (alias: `/v`)
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
- `/elevated on|off` (alias: `/elev`)
- `/model <name>` (alias: `/models`; or `/<alias>` from `agents.defaults.models.*.alias`)
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
- `/bash <command>` (host-only; alias for `! <command>`; requires `commands.bash: true` + `tools.elevated` allowlists)
Text-only:
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction))
- `! <command>` (host-only; one at a time; use `!poll` + `!stop` for long-running jobs)
- `!poll` (check output / status; accepts optional `sessionId`; `/bash poll` also works)
- `!stop` (stop the running bash job; accepts optional `sessionId`; `/bash stop` also works)
Notes:
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).

View File

@@ -7,11 +7,12 @@ read_when:
## What it does
- Inline directive in any inbound body: `/t <level>`, `/think:<level>`, or `/thinking <level>`.
- Levels (aliases): `off | minimal | low | medium | high`
- Levels (aliases): `off | minimal | low | medium | high | xhigh` (GPT-5.2 + Codex models only)
- minimal → “think”
- low → “think hard”
- medium → “think harder”
- high → “ultrathink” (max budget)
- xhigh → “ultrathink+” (GPT-5.2 + Codex models only)
- `highest`, `max` map to `high`.
## Resolution order

View File

@@ -50,7 +50,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
- `/agent <id>` (or `/agents`)
- `/session <key>` (or `/sessions`)
- `/model <provider/model>` (or `/model list`, `/models`)
- `/think <off|minimal|low|medium|high>`
- `/think <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
- `/verbose <on|off>`
- `/reasoning <on|off|stream>` (stream = Telegram draft only)
- `/cost <on|off>`
@@ -67,3 +67,6 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
## Notes
- The TUI shows Gateway chat deltas (`event: chat`) and agent tool events.
- It registers as a Gateway client with `mode: "tui"` for presence and debugging.
- The TUI uses the **Gateway’s** model/auth config and environment. If a model
token works in your shell but not in TUI, put it in `~/.clawdbot/.env` or
enable `env.shellEnv.enabled`, then restart the Gateway.

View File

@@ -132,10 +132,10 @@
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "^0.43.0",
"@mariozechner/pi-ai": "^0.43.0",
"@mariozechner/pi-coding-agent": "^0.43.0",
"@mariozechner/pi-tui": "^0.43.0",
"@mariozechner/pi-agent-core": "^0.45.3",
"@mariozechner/pi-ai": "^0.45.3",
"@mariozechner/pi-coding-agent": "^0.45.3",
"@mariozechner/pi-tui": "^0.45.3",
"@microsoft/agents-hosting": "^1.1.1",
"@microsoft/agents-hosting-express": "^1.1.1",
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
@@ -207,7 +207,7 @@
"@sinclair/typebox": "0.34.47"
},
"patchedDependencies": {
"@mariozechner/pi-ai@0.43.0": "patches/@mariozechner__pi-ai@0.43.0.patch"
"@mariozechner/pi-ai@0.45.3": "patches/@mariozechner__pi-ai@0.45.3.patch"
}
},
"vitest": {

View File

@@ -1,8 +1,8 @@
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
index 12540bb1069087a0d0a2967f792008627b9f79d9..f30b525620e6d8e45146b439ec3733e4053c9d2a 100644
index cc9e0cb..2b18ec4 100644
--- a/dist/providers/google-gemini-cli.js
+++ b/dist/providers/google-gemini-cli.js
@@ -248,6 +248,11 @@ export const streamGoogleGeminiCli = (model, context, options) => {
@@ -329,6 +329,11 @@ export const streamGoogleGeminiCli = (model, context, options) => {
break; // Success, exit retry loop
}
const errorText = await response.text();
@@ -14,41 +14,8 @@ index 12540bb1069087a0d0a2967f792008627b9f79d9..f30b525620e6d8e45146b439ec3733e4
// Check if retryable
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
// Use server-provided delay or exponential backoff
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
index ae4710b0f134ac4a48f5b7053f454d1068bee71f..b1b5bd94586f68461ccc44e4a9cdf3acb4e0d084 100644
--- a/dist/providers/google-shared.js
+++ b/dist/providers/google-shared.js
@@ -42,6 +42,8 @@ export function retainThoughtSignature(existing, incoming) {
export function convertMessages(model, context) {
const contents = [];
const transformedMessages = transformMessages(context.messages, model);
+ const shouldStripFunctionId = typeof model.provider === "string" &&
+ model.provider.startsWith("google");
for (const msg of transformedMessages) {
if (msg.role === "user") {
if (typeof msg.content === "string") {
@@ -113,6 +115,9 @@ export function convertMessages(model, context) {
args: block.arguments,
},
};
+ if (shouldStripFunctionId && part?.functionCall?.id) {
+ delete part.functionCall.id; // Google Gemini/Vertex do not support 'id' in functionCall
+ }
if (block.thoughtSignature) {
part.thoughtSignature = block.thoughtSignature;
}
@@ -155,6 +160,9 @@ export function convertMessages(model, context) {
...(hasImages && supportsMultimodalFunctionResponse && { parts: imageParts }),
},
};
+ if (shouldStripFunctionId && functionResponsePart.functionResponse?.id) {
+ delete functionResponsePart.functionResponse.id; // Google Gemini/Vertex do not support 'id' in functionResponse
+ }
// Cloud Code Assist API requires all function responses to be in a single user turn.
// Check if the last content is already a user turn with function responses and merge.
const lastContent = contents[contents.length - 1];
diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js
index ad0a2aabbe10382cee4e463b68a02864dd235e57..8c001acfd0b4e0743181c246f1bedcf8cd2ffb02 100644
index 7488c79..4c34587 100644
--- a/dist/providers/openai-codex-responses.js
+++ b/dist/providers/openai-codex-responses.js
@@ -517,7 +517,7 @@ function convertTools(tools) {
@@ -61,10 +28,10 @@ index ad0a2aabbe10382cee4e463b68a02864dd235e57..8c001acfd0b4e0743181c246f1bedcf8
}
function mapStopReason(status) {
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
index f07085c64390b211340d6a826b28ea9c2e77302f..7f758532246cc7b062df48e9cec4e6c904b76a99 100644
index c4714f4..4d1e6b0 100644
--- a/dist/providers/openai-responses.js
+++ b/dist/providers/openai-responses.js
@@ -396,10 +396,16 @@ function convertMessages(model, context) {
@@ -400,10 +400,16 @@ function convertMessages(model, context) {
}
else if (msg.role === "assistant") {
const output = [];
@@ -81,7 +48,7 @@ index f07085c64390b211340d6a826b28ea9c2e77302f..7f758532246cc7b062df48e9cec4e6c9
const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem);
}
@@ -434,6 +440,16 @@ function convertMessages(model, context) {
@@ -438,6 +444,16 @@ function convertMessages(model, context) {
});
}
}

1573
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,4 +10,4 @@ onlyBuiltDependencies:
- sharp
patchedDependencies:
'@mariozechner/pi-ai@0.43.0': patches/@mariozechner__pi-ai@0.43.0.patch
'@mariozechner/pi-ai@0.45.3': patches/@mariozechner__pi-ai@0.45.3.patch

View File

@@ -4,6 +4,8 @@ RUN corepack enable
WORKDIR /app
ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts ./
COPY patches ./patches
COPY src ./src

View File

@@ -20,6 +20,7 @@ docker run --rm -t \
--entrypoint bash \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e HOME=/home/node \
-e NODE_OPTIONS=--disable-warning=ExperimentalWarning \
-e CLAWDBOT_LIVE_TEST=1 \
-e CLAWDBOT_LIVE_GATEWAY_MODELS="${CLAWDBOT_LIVE_GATEWAY_MODELS:-all}" \
-e CLAWDBOT_LIVE_GATEWAY_PROVIDERS="${CLAWDBOT_LIVE_GATEWAY_PROVIDERS:-}" \

View File

@@ -20,6 +20,7 @@ docker run --rm -t \
--entrypoint bash \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e HOME=/home/node \
-e NODE_OPTIONS=--disable-warning=ExperimentalWarning \
-e CLAWDBOT_LIVE_TEST=1 \
-e CLAWDBOT_LIVE_MODELS="${CLAWDBOT_LIVE_MODELS:-all}" \
-e CLAWDBOT_LIVE_PROVIDERS="${CLAWDBOT_LIVE_PROVIDERS:-}" \

View File

@@ -155,12 +155,3 @@ export function resolveAgentDir(cfg: ClawdbotConfig, agentId: string) {
const root = resolveStateDir(process.env, os.homedir);
return path.join(root, "agents", id, "agent");
}
/**
* Resolve the agent directory for the default agent without requiring config.
* Used by onboarding when writing auth profiles before config is fully set up.
*/
export function resolveDefaultAgentDir(): string {
const root = resolveStateDir(process.env, os.homedir);
return path.join(root, "agents", DEFAULT_AGENT_ID, "agent");
}

View File

@@ -59,6 +59,134 @@ describe("resolveAuthProfileOrder", () => {
expect(order).toContain("anthropic:default");
});
it("drops explicit order entries that are missing from the store", () => {
const order = resolveAuthProfileOrder({
cfg: {
auth: {
order: {
minimax: ["minimax:default", "minimax:prod"],
},
},
},
store: {
version: 1,
profiles: {
"minimax:prod": {
type: "api_key",
provider: "minimax",
key: "sk-prod",
},
},
},
provider: "minimax",
});
expect(order).toEqual(["minimax:prod"]);
});
it("drops explicit order entries that belong to another provider", () => {
const order = resolveAuthProfileOrder({
cfg: {
auth: {
order: {
minimax: ["openai:default", "minimax:prod"],
},
},
},
store: {
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-openai",
},
"minimax:prod": {
type: "api_key",
provider: "minimax",
key: "sk-mini",
},
},
},
provider: "minimax",
});
expect(order).toEqual(["minimax:prod"]);
});
it("drops token profiles with empty credentials", () => {
const order = resolveAuthProfileOrder({
cfg: {
auth: {
order: {
minimax: ["minimax:default"],
},
},
},
store: {
version: 1,
profiles: {
"minimax:default": {
type: "token",
provider: "minimax",
token: " ",
},
},
},
provider: "minimax",
});
expect(order).toEqual([]);
});
it("drops token profiles that are already expired", () => {
const order = resolveAuthProfileOrder({
cfg: {
auth: {
order: {
minimax: ["minimax:default"],
},
},
},
store: {
version: 1,
profiles: {
"minimax:default": {
type: "token",
provider: "minimax",
token: "sk-minimax",
expires: Date.now() - 1000,
},
},
},
provider: "minimax",
});
expect(order).toEqual([]);
});
it("keeps oauth profiles that can refresh", () => {
const order = resolveAuthProfileOrder({
cfg: {
auth: {
order: {
anthropic: ["anthropic:oauth"],
},
},
},
store: {
version: 1,
profiles: {
"anthropic:oauth": {
type: "oauth",
provider: "anthropic",
access: "",
refresh: "refresh-token",
expires: Date.now() - 1000,
},
},
},
provider: "anthropic",
});
expect(order).toEqual(["anthropic:oauth"]);
});
it("does not prioritize lastGood over round-robin ordering", () => {
const order = resolveAuthProfileOrder({
cfg,

View File

@@ -1042,6 +1042,7 @@ export function resolveAuthProfileOrder(params: {
}): string[] {
const { cfg, store, provider, preferredProfile } = params;
const providerKey = normalizeProviderId(provider);
const now = Date.now();
const storedOrder = (() => {
const order = store.order;
if (!order) return undefined;
@@ -1076,7 +1077,36 @@ export function resolveAuthProfileOrder(params: {
const filtered = baseOrder.filter((profileId) => {
const cred = store.profiles[profileId];
return cred ? normalizeProviderId(cred.provider) === providerKey : true;
if (!cred) return false;
if (normalizeProviderId(cred.provider) !== providerKey) return false;
const profileConfig = cfg?.auth?.profiles?.[profileId];
if (profileConfig) {
if (normalizeProviderId(profileConfig.provider) !== providerKey) {
return false;
}
if (profileConfig.mode !== cred.type) {
const oauthCompatible =
profileConfig.mode === "oauth" && cred.type === "token";
if (!oauthCompatible) return false;
}
}
if (cred.type === "api_key") return Boolean(cred.key?.trim());
if (cred.type === "token") {
if (!cred.token?.trim()) return false;
if (
typeof cred.expires === "number" &&
Number.isFinite(cred.expires) &&
cred.expires > 0 &&
now >= cred.expires
) {
return false;
}
return true;
}
if (cred.type === "oauth") {
return Boolean(cred.access?.trim() || cred.refresh?.trim());
}
return false;
});
const deduped: string[] = [];
for (const entry of filtered) {
@@ -1089,7 +1119,6 @@ export function resolveAuthProfileOrder(params: {
if (explicitOrder && explicitOrder.length > 0) {
// ...but still respect cooldown tracking to avoid repeatedly selecting a
// known-bad/rate-limited key as the first candidate.
const now = Date.now();
const available: string[] = [];
const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = [];

View File

@@ -7,7 +7,13 @@ export type ModelRef = {
model: string;
};
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
export type ThinkLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh";
export type ModelAliasIndex = {
byAlias: Map<string, { alias: string; ref: ModelRef }>;

View File

@@ -253,19 +253,17 @@ describeLive("live models (profile keys)", () => {
parameters: Type.Object({}, { additionalProperties: false }),
};
const first = await completeSimpleWithTimeout(
let firstUserContent =
"Call the tool `noop` with {}. Do not write any other text.";
let firstUser = {
role: "user" as const,
content: firstUserContent,
timestamp: Date.now(),
};
let first = await completeSimpleWithTimeout(
model,
{
messages: [
{
role: "user",
content:
"Call the tool `noop` with {}. Do not write any other text.",
timestamp: Date.now(),
},
],
tools: [noopTool],
},
{ messages: [firstUser], tools: [noopTool] },
{
apiKey,
reasoning: resolveTestReasoning(model),
@@ -274,8 +272,49 @@ describeLive("live models (profile keys)", () => {
perModelTimeoutMs,
);
const toolCall = first.content.find((b) => b.type === "toolCall");
let toolCall = first.content.find((b) => b.type === "toolCall");
let firstText = first.content
.filter((b) => b.type === "text")
.map((b) => b.text.trim())
.join(" ")
.trim();
// Occasional flake: model answers in text instead of tool call (or adds text).
// Retry a couple times with a stronger instruction so we still exercise the tool-only replay path.
for (
let i = 0;
i < 2 && (!toolCall || firstText.length > 0);
i += 1
) {
firstUserContent =
"Call the tool `noop` with {}. IMPORTANT: respond ONLY with the tool call; no other text.";
firstUser = {
role: "user" as const,
content: firstUserContent,
timestamp: Date.now(),
};
first = await completeSimpleWithTimeout(
model,
{ messages: [firstUser], tools: [noopTool] },
{
apiKey,
reasoning: resolveTestReasoning(model),
maxTokens: 128,
},
perModelTimeoutMs,
);
toolCall = first.content.find((b) => b.type === "toolCall");
firstText = first.content
.filter((b) => b.type === "text")
.map((b) => b.text.trim())
.join(" ")
.trim();
}
expect(toolCall).toBeTruthy();
expect(firstText.length).toBe(0);
if (!toolCall || toolCall.type !== "toolCall") {
throw new Error("expected tool call");
}
@@ -284,12 +323,7 @@ describeLive("live models (profile keys)", () => {
model,
{
messages: [
{
role: "user",
content:
"Call the tool `noop` with {}. Do not write any other text.",
timestamp: Date.now(),
},
firstUser,
first,
{
role: "toolResult",
@@ -309,7 +343,8 @@ describeLive("live models (profile keys)", () => {
{
apiKey,
reasoning: resolveTestReasoning(model),
maxTokens: 64,
// Headroom: reasoning summary can consume most of the output budget.
maxTokens: 256,
},
perModelTimeoutMs,
);

View File

@@ -209,6 +209,11 @@ describe("classifyFailoverReason", () => {
expect(classifyFailoverReason("resource has been exhausted")).toBe(
"rate_limit",
);
expect(
classifyFailoverReason(
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
),
).toBe("rate_limit");
expect(classifyFailoverReason("invalid request format")).toBe("format");
expect(classifyFailoverReason("credit balance too low")).toBe("billing");
expect(classifyFailoverReason("deadline exceeded")).toBe("timeout");
@@ -265,6 +270,15 @@ describe("formatAssistantErrorText", () => {
"Message ordering conflict",
);
});
it("returns a friendly message for Anthropic overload errors", () => {
const msg = makeAssistantError(
'{"type":"error","error":{"details":null,"type":"overloaded_error","message":"Overloaded"},"request_id":"req_123"}',
);
expect(formatAssistantErrorText(msg)).toBe(
"The AI service is temporarily overloaded. Please try again in a moment.",
);
});
});
describe("sanitizeToolCallId", () => {

View File

@@ -370,8 +370,8 @@ export function formatAssistantErrorText(
// Check for context overflow (413) errors
if (isContextOverflowError(raw)) {
return (
"Context overflow: the conversation history is too large. " +
"Use /new or /reset to start a fresh session."
"Context overflow: prompt too large for the model. " +
"Try again with less input or a larger-context model."
);
}
@@ -392,6 +392,11 @@ export function formatAssistantErrorText(
return `LLM request rejected: ${invalidRequest[1]}`;
}
// Check for overloaded errors (Anthropic API capacity)
if (isOverloadedErrorMessage(raw)) {
return "The AI service is temporarily overloaded. Please try again in a moment.";
}
// Keep it short for WhatsApp.
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
}
@@ -414,6 +419,10 @@ const ERROR_PATTERNS = {
"resource_exhausted",
"usage limit",
],
overloaded: [
/overloaded_error|"type"\s*:\s*"overloaded_error"/i,
"overloaded",
],
timeout: [
"timeout",
"timed out",
@@ -496,6 +505,10 @@ export function isAuthErrorMessage(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.auth);
}
export function isOverloadedErrorMessage(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.overloaded);
}
export function isCloudCodeAssistFormatError(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.format);
}
@@ -517,6 +530,7 @@ export type FailoverReason =
export function classifyFailoverReason(raw: string): FailoverReason | null {
if (isRateLimitErrorMessage(raw)) return "rate_limit";
if (isOverloadedErrorMessage(raw)) return "rate_limit"; // Treat overloaded as rate limit for failover
if (isCloudCodeAssistFormatError(raw)) return "format";
if (isBillingErrorMessage(raw)) return "billing";
if (isTimeoutErrorMessage(raw)) return "timeout";

View File

@@ -362,6 +362,10 @@ export type EmbeddedPiRunMeta = {
durationMs: number;
agentMeta?: EmbeddedPiAgentMeta;
aborted?: boolean;
error?: {
kind: "context_overflow" | "compaction_failure";
message: string;
};
};
function buildModelAliasLines(cfg?: ClawdbotConfig) {
@@ -1046,7 +1050,7 @@ export function resolveEmbeddedSessionLane(key: string) {
}
function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
// pi-agent-core supports "xhigh" too; Clawdbot doesn't surface it for now.
// pi-agent-core supports "xhigh"; Clawdbot enables it for specific models.
if (!level) return "off";
return level;
}
@@ -1976,12 +1980,15 @@ export async function runEmbeddedPiAgent(params: {
if (promptError && !aborted) {
const errorText = describeUnknownError(promptError);
if (isContextOverflowError(errorText)) {
const kind = isCompactionFailureError(errorText)
? "compaction_failure"
: "context_overflow";
return {
payloads: [
{
text:
"Context overflow: the conversation history is too large for the model. " +
"Use /new or /reset to start a fresh session, or try a model with a larger context window.",
"Context overflow: prompt too large for the model. " +
"Try again with less input or a larger-context model.",
isError: true,
},
],
@@ -1992,6 +1999,7 @@ export async function runEmbeddedPiAgent(params: {
provider,
model: model.id,
},
error: { kind, message: errorText },
},
};
}

View File

@@ -251,6 +251,13 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
textAlias: "/queue",
acceptsArgs: true,
}),
defineChatCommand({
key: "bash",
description: "Run host shell commands (host-only).",
textAlias: "/bash",
scope: "text",
acceptsArgs: true,
}),
];
registerAlias(commands, "status", "/usage");
@@ -314,6 +321,7 @@ export function isCommandEnabled(
): boolean {
if (commandKey === "config") return cfg.commands?.config === true;
if (commandKey === "debug") return cfg.commands?.debug === true;
if (commandKey === "bash") return cfg.commands?.bash === true;
return true;
}

View File

@@ -69,6 +69,97 @@ describe("directive behavior", () => {
vi.restoreAllMocks();
});
it("accepts /thinking xhigh for codex models", async () => {
await withTempHome(async (home) => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
},
{},
{
agents: {
defaults: {
model: "openai-codex/gpt-5.2-codex",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
},
);
const texts = (Array.isArray(res) ? res : [res])
.map((entry) => entry?.text)
.filter(Boolean);
expect(texts).toContain("Thinking level set to xhigh.");
});
});
it("accepts /thinking xhigh for openai gpt-5.2", async () => {
await withTempHome(async (home) => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
},
{},
{
agents: {
defaults: {
model: "openai/gpt-5.2",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
},
);
const texts = (Array.isArray(res) ? res : [res])
.map((entry) => entry?.text)
.filter(Boolean);
expect(texts).toContain("Thinking level set to xhigh.");
});
});
it("rejects /thinking xhigh for non-codex models", async () => {
await withTempHome(async (home) => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
},
{},
{
agents: {
defaults: {
model: "openai/gpt-4.1-mini",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
},
);
const texts = (Array.isArray(res) ? res : [res])
.map((entry) => entry?.text)
.filter(Boolean);
expect(texts).toContain(
'Thinking level "xhigh" is only supported for openai/gpt-5.2, openai-codex/gpt-5.2-codex or openai-codex/gpt-5.1-codex.',
);
});
});
it("keeps reserved command aliases from matching after trimming", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();

View File

@@ -1456,7 +1456,7 @@ describe("trigger handling", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe(
"⚠️ Context overflow - conversation too long. Starting fresh might help!",
"⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.",
);
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
});

View File

@@ -29,7 +29,10 @@ import {
type ClawdbotConfig,
loadConfig,
} from "../config/config.js";
import { resolveSessionFilePath } from "../config/sessions.js";
import {
resolveSessionFilePath,
saveSessionStore,
} from "../config/sessions.js";
import { logVerbose } from "../globals.js";
import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
import { getProviderDock } from "../providers/dock.js";
@@ -94,8 +97,10 @@ import {
import type { MsgContext, TemplateContext } from "./templating.js";
import {
type ElevatedLevel,
formatXHighModelHint,
normalizeThinkLevel,
type ReasoningLevel,
supportsXHighThinking,
type ThinkLevel,
type VerboseLevel,
} from "./thinking.js";
@@ -976,6 +981,11 @@ export async function getReplyFromConfig(
command: inlineCommandContext,
agentId,
directives,
elevated: {
enabled: elevatedEnabled,
allowed: elevatedAllowed,
failures: elevatedFailures,
},
sessionEntry,
sessionStore,
sessionKey,
@@ -1033,6 +1043,11 @@ export async function getReplyFromConfig(
command,
agentId,
directives,
elevated: {
enabled: elevatedEnabled,
allowed: elevatedAllowed,
failures: elevatedFailures,
},
sessionEntry,
sessionStore,
sessionKey,
@@ -1187,7 +1202,10 @@ export async function getReplyFromConfig(
if (!resolvedThinkLevel && prefixedCommandBody) {
const parts = prefixedCommandBody.split(/\s+/);
const maybeLevel = normalizeThinkLevel(parts[0]);
if (maybeLevel) {
if (
maybeLevel &&
(maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))
) {
resolvedThinkLevel = maybeLevel;
prefixedCommandBody = parts.slice(1).join(" ").trim();
}
@@ -1195,6 +1213,33 @@ export async function getReplyFromConfig(
if (!resolvedThinkLevel) {
resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel();
}
if (
resolvedThinkLevel === "xhigh" &&
!supportsXHighThinking(provider, model)
) {
const explicitThink =
directives.hasThinkDirective && directives.thinkLevel !== undefined;
if (explicitThink) {
typing.cleanup();
return {
text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}. Use /think high or switch to one of those models.`,
};
}
resolvedThinkLevel = "high";
if (
sessionEntry &&
sessionStore &&
sessionKey &&
sessionEntry.thinkingLevel === "xhigh"
) {
sessionEntry.thinkingLevel = "high";
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
}
}
const sessionIdFinal = sessionId ?? crypto.randomUUID();
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
const queueBodyBase = transcribedText

View File

@@ -525,6 +525,65 @@ describe("runReplyAgent typing (heartbeat)", () => {
}
});
it("retries after context overflow payload by resetting the session", async () => {
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
const stateDir = await fs.mkdtemp(
path.join(tmpdir(), "clawdbot-session-overflow-reset-"),
);
process.env.CLAWDBOT_STATE_DIR = stateDir;
try {
const sessionId = "session";
const storePath = path.join(stateDir, "sessions", "sessions.json");
const sessionEntry = { sessionId, updatedAt: Date.now() };
const sessionStore = { main: sessionEntry };
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8");
runEmbeddedPiAgentMock
.mockImplementationOnce(async () => ({
payloads: [
{ text: "Context overflow: prompt too large", isError: true },
],
meta: {
durationMs: 1,
error: {
kind: "context_overflow",
message:
'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
},
},
}))
.mockImplementationOnce(async () => ({
payloads: [{ text: "ok" }],
meta: { durationMs: 1 },
}));
const callsBefore = runEmbeddedPiAgentMock.mock.calls.length;
const { run } = createMinimalRun({
sessionEntry,
sessionStore,
sessionKey: "main",
storePath,
});
const res = await run();
expect(runEmbeddedPiAgentMock.mock.calls.length - callsBefore).toBe(2);
const payload = Array.isArray(res) ? res[0] : res;
expect(payload).toMatchObject({ text: "ok" });
expect(sessionStore.main.sessionId).not.toBe(sessionId);
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(persisted.main.sessionId).toBe(sessionStore.main.sessionId);
} finally {
if (prevStateDir) {
process.env.CLAWDBOT_STATE_DIR = prevStateDir;
} else {
delete process.env.CLAWDBOT_STATE_DIR;
}
}
});
it("still replies even if session reset fails to persist", async () => {
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
const stateDir = await fs.mkdtemp(

View File

@@ -834,6 +834,20 @@ export async function runReplyAgent(params: {
runResult = fallbackResult.result;
fallbackProvider = fallbackResult.provider;
fallbackModel = fallbackResult.model;
// Some embedded runs surface context overflow as an error payload instead of throwing.
// Treat those as a session-level failure and auto-recover by starting a fresh session.
const embeddedError = runResult.meta?.error;
if (
embeddedError &&
isContextOverflowError(embeddedError.message) &&
!didResetAfterCompactionFailure &&
(await resetSessionAfterCompactionFailure(embeddedError.message))
) {
didResetAfterCompactionFailure = true;
continue;
}
break;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
@@ -894,7 +908,7 @@ export async function runReplyAgent(params: {
defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
return finalizeWithFollowup({
text: isContextOverflow
? "⚠️ Context overflow - conversation too long. Starting fresh might help!"
? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
: `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`,
});
}

View File

@@ -0,0 +1,416 @@
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import {
getFinishedSession,
getSession,
markExited,
} from "../../agents/bash-process-registry.js";
import { createExecTool } from "../../agents/bash-tools.js";
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
import { killProcessTree } from "../../agents/shell-utils.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import type { MsgContext } from "../templating.js";
import type { ReplyPayload } from "../types.js";
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
const CHAT_BASH_SCOPE_KEY = "chat:bash";
const DEFAULT_FOREGROUND_MS = 2000;
const MAX_FOREGROUND_MS = 30_000;
type BashRequest =
| { action: "help" }
| { action: "run"; command: string }
| { action: "poll"; sessionId?: string }
| { action: "stop"; sessionId?: string };
type ActiveBashJob =
| { state: "starting"; startedAt: number; command: string }
| {
state: "running";
sessionId: string;
startedAt: number;
command: string;
watcherAttached: boolean;
};
let activeJob: ActiveBashJob | null = null;
function clampNumber(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
function resolveForegroundMs(cfg: ClawdbotConfig): number {
const raw = cfg.commands?.bashForegroundMs;
if (typeof raw !== "number" || Number.isNaN(raw))
return DEFAULT_FOREGROUND_MS;
return clampNumber(Math.floor(raw), 0, MAX_FOREGROUND_MS);
}
function formatSessionSnippet(sessionId: string) {
const trimmed = sessionId.trim();
if (trimmed.length <= 12) return trimmed;
return `${trimmed.slice(0, 8)}…`;
}
function formatOutputBlock(text: string) {
const trimmed = text.trim();
if (!trimmed) return "(no output)";
return `\`\`\`txt\n${trimmed}\n\`\`\``;
}
function parseBashRequest(raw: string): BashRequest | null {
const trimmed = raw.trimStart();
let restSource = "";
if (trimmed.toLowerCase().startsWith("/bash")) {
const match = trimmed.match(/^\/bash(?:\s*:\s*|\s+|$)([\s\S]*)$/i);
if (!match) return null;
restSource = match[1] ?? "";
} else if (trimmed.startsWith("!")) {
restSource = trimmed.slice(1);
if (restSource.trimStart().startsWith(":")) {
restSource = restSource.trimStart().slice(1);
}
} else {
return null;
}
const rest = restSource.trimStart();
if (!rest) return { action: "help" };
const tokenMatch = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
const token = tokenMatch?.[1]?.trim() ?? "";
const remainder = tokenMatch?.[2]?.trim() ?? "";
const lowered = token.toLowerCase();
if (lowered === "poll") {
return { action: "poll", sessionId: remainder || undefined };
}
if (lowered === "stop") {
return { action: "stop", sessionId: remainder || undefined };
}
if (lowered === "help") {
return { action: "help" };
}
return { action: "run", command: rest };
}
function resolveRawCommandBody(params: {
ctx: MsgContext;
cfg: ClawdbotConfig;
agentId?: string;
isGroup: boolean;
}) {
const source =
params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body ?? "";
const stripped = stripStructuralPrefixes(source);
return params.isGroup
? stripMentions(stripped, params.ctx, params.cfg, params.agentId)
: stripped;
}
function getScopedSession(sessionId: string) {
const running = getSession(sessionId);
if (running && running.scopeKey === CHAT_BASH_SCOPE_KEY) return { running };
const finished = getFinishedSession(sessionId);
if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY)
return { finished };
return {};
}
function ensureActiveJobState() {
if (!activeJob) return null;
if (activeJob.state === "starting") return activeJob;
const { running, finished } = getScopedSession(activeJob.sessionId);
if (running) return activeJob;
if (finished) {
activeJob = null;
return null;
}
activeJob = null;
return null;
}
function attachActiveWatcher(sessionId: string) {
if (!activeJob || activeJob.state !== "running") return;
if (activeJob.sessionId !== sessionId) return;
if (activeJob.watcherAttached) return;
const { running } = getScopedSession(sessionId);
const child = running?.child;
if (!child) return;
activeJob.watcherAttached = true;
child.once("close", () => {
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
activeJob = null;
}
});
}
function buildUsageReply(): ReplyPayload {
return {
text: [
"⚙️ Usage:",
"- ! <command>",
"- !poll | ! poll",
"- !stop | ! stop",
"- /bash ... (alias; same subcommands as !)",
].join("\n"),
};
}
function formatElevatedUnavailableMessage(params: {
runtimeSandboxed: boolean;
failures: Array<{ gate: string; key: string }>;
sessionKey?: string;
}): string {
const lines: string[] = [];
lines.push(
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
);
if (params.failures.length > 0) {
lines.push(
`Failing gates: ${params.failures
.map((f) => `${f.gate} (${f.key})`)
.join(", ")}`,
);
} else {
lines.push(
"Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
);
}
lines.push("Fix-it keys:");
lines.push("- tools.elevated.enabled");
lines.push("- tools.elevated.allowFrom.<provider>");
lines.push("- agents.list[].tools.elevated.enabled");
lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
if (params.sessionKey) {
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
}
return lines.join("\n");
}
export async function handleBashChatCommand(params: {
ctx: MsgContext;
cfg: ClawdbotConfig;
agentId?: string;
sessionKey: string;
isGroup: boolean;
elevated: {
enabled: boolean;
allowed: boolean;
failures: Array<{ gate: string; key: string }>;
};
}): Promise<ReplyPayload> {
if (params.cfg.commands?.bash !== true) {
return {
text: "⚠️ bash is disabled. Set commands.bash=true to enable.",
};
}
const agentId =
params.agentId ??
resolveSessionAgentId({
sessionKey: params.sessionKey,
config: params.cfg,
});
if (!params.elevated.enabled || !params.elevated.allowed) {
const runtimeSandboxed = resolveSandboxRuntimeStatus({
cfg: params.cfg,
sessionKey: params.ctx.SessionKey,
}).sandboxed;
return {
text: formatElevatedUnavailableMessage({
runtimeSandboxed,
failures: params.elevated.failures,
sessionKey: params.ctx.SessionKey,
}),
};
}
const rawBody = resolveRawCommandBody({
ctx: params.ctx,
cfg: params.cfg,
agentId,
isGroup: params.isGroup,
}).trim();
const request = parseBashRequest(rawBody);
if (!request) {
return { text: "⚠️ Unrecognized bash request." };
}
const liveJob = ensureActiveJobState();
if (request.action === "help") {
return buildUsageReply();
}
if (request.action === "poll") {
const sessionId =
request.sessionId?.trim() ||
(liveJob?.state === "running" ? liveJob.sessionId : "");
if (!sessionId) {
return { text: "⚙️ No active bash job." };
}
const { running, finished } = getScopedSession(sessionId);
if (running) {
attachActiveWatcher(sessionId);
const runtimeSec = Math.max(
0,
Math.floor((Date.now() - running.startedAt) / 1000),
);
const tail = running.tail || "(no output yet)";
return {
text: [
`⚙️ bash still running (session ${formatSessionSnippet(sessionId)}, ${runtimeSec}s).`,
formatOutputBlock(tail),
"Hint: !stop (or /bash stop)",
].join("\n"),
};
}
if (finished) {
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
activeJob = null;
}
const exitLabel = finished.exitSignal
? `signal ${String(finished.exitSignal)}`
: `code ${String(finished.exitCode ?? 0)}`;
const prefix = finished.status === "completed" ? "⚙️" : "⚠️";
return {
text: [
`${prefix} bash finished (session ${formatSessionSnippet(sessionId)}).`,
`Exit: ${exitLabel}`,
formatOutputBlock(finished.aggregated || finished.tail),
].join("\n"),
};
}
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
activeJob = null;
}
return {
text: `⚙️ No bash session found for ${formatSessionSnippet(sessionId)}.`,
};
}
if (request.action === "stop") {
const sessionId =
request.sessionId?.trim() ||
(liveJob?.state === "running" ? liveJob.sessionId : "");
if (!sessionId) {
return { text: "⚙️ No active bash job." };
}
const { running } = getScopedSession(sessionId);
if (!running) {
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
activeJob = null;
}
return {
text: `⚙️ No running bash job found for ${formatSessionSnippet(sessionId)}.`,
};
}
if (!running.backgrounded) {
return {
text: `⚠️ Session ${formatSessionSnippet(sessionId)} is not backgrounded.`,
};
}
const pid = running.pid ?? running.child?.pid;
if (pid) {
killProcessTree(pid);
}
markExited(running, null, "SIGKILL", "failed");
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
activeJob = null;
}
return {
text: `⚙️ bash stopped (session ${formatSessionSnippet(sessionId)}).`,
};
}
// request.action === "run"
if (liveJob) {
const label =
liveJob.state === "running"
? formatSessionSnippet(liveJob.sessionId)
: "starting";
return {
text: `⚠️ A bash job is already running (${label}). Use !poll / !stop (or /bash poll / /bash stop).`,
};
}
const commandText = request.command.trim();
if (!commandText) return buildUsageReply();
activeJob = {
state: "starting",
startedAt: Date.now(),
command: commandText,
};
try {
const foregroundMs = resolveForegroundMs(params.cfg);
const shouldBackgroundImmediately = foregroundMs <= 0;
const timeoutSec =
params.cfg.tools?.exec?.timeoutSec ?? params.cfg.tools?.bash?.timeoutSec;
const execTool = createExecTool({
scopeKey: CHAT_BASH_SCOPE_KEY,
allowBackground: true,
timeoutSec,
elevated: {
enabled: params.elevated.enabled,
allowed: params.elevated.allowed,
defaultLevel: "on",
},
});
const result = await execTool.execute("chat-bash", {
command: commandText,
background: shouldBackgroundImmediately,
yieldMs: shouldBackgroundImmediately ? undefined : foregroundMs,
timeout: timeoutSec,
elevated: true,
});
if (result.details?.status === "running") {
const sessionId = result.details.sessionId;
activeJob = {
state: "running",
sessionId,
startedAt: result.details.startedAt,
command: commandText,
watcherAttached: false,
};
attachActiveWatcher(sessionId);
const snippet = formatSessionSnippet(sessionId);
logVerbose(`Started bash session ${snippet}: ${commandText}`);
return {
text: `⚙️ bash started (session ${sessionId}). Still running; use !poll / !stop (or /bash poll / /bash stop).`,
};
}
// Completed in foreground.
activeJob = null;
const exitCode =
result.details?.status === "completed" ? result.details.exitCode : 0;
const output =
result.details?.status === "completed"
? result.details.aggregated
: result.content
.map((chunk) => (chunk.type === "text" ? chunk.text : ""))
.join("\n");
return {
text: [
`⚙️ bash: ${commandText}`,
`Exit: ${exitCode}`,
formatOutputBlock(output || "(no output)"),
].join("\n"),
};
} catch (err) {
activeJob = null;
const message = err instanceof Error ? err.message : String(err);
return {
text: [`⚠️ bash failed: ${commandText}`, formatOutputBlock(message)].join(
"\n",
),
};
}
}
export function resetBashChatCommandForTests() {
activeJob = null;
}

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../../config/config.js";
import type { MsgContext } from "../templating.js";
import { resetBashChatCommandForTests } from "./bash-command.js";
import { buildCommandContext, handleCommands } from "./commands.js";
import { parseInlineDirectives } from "./directive-handling.js";
@@ -33,6 +34,7 @@ function buildParams(
cfg,
command,
directives: parseInlineDirectives(commandBody),
elevated: { enabled: true, allowed: true, failures: [] },
sessionKey: "agent:main:main",
workspaceDir: "/tmp",
defaultGroupActivation: () => "mention",
@@ -47,6 +49,37 @@ function buildParams(
}
describe("handleCommands gating", () => {
it("blocks /bash when disabled", async () => {
resetBashChatCommandForTests();
const cfg = {
commands: { bash: false, text: true },
whatsapp: { allowFrom: ["*"] },
} as ClawdbotConfig;
const params = buildParams("/bash echo hi", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("bash is disabled");
});
it("blocks /bash when elevated is not allowlisted", async () => {
resetBashChatCommandForTests();
const cfg = {
commands: { bash: true, text: true },
whatsapp: { allowFrom: ["*"] },
} as ClawdbotConfig;
const params = buildParams("/bash echo hi", cfg);
params.elevated = {
enabled: true,
allowed: false,
failures: [
{ gate: "allowFrom", key: "tools.elevated.allowFrom.whatsapp" },
],
};
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("elevated is not available");
});
it("blocks /config when disabled", async () => {
const cfg = {
commands: { config: false, debug: false, text: true },
@@ -70,6 +103,32 @@ describe("handleCommands gating", () => {
});
});
describe("handleCommands bash alias", () => {
it("routes !poll through the /bash handler", async () => {
resetBashChatCommandForTests();
const cfg = {
commands: { bash: true, text: true },
whatsapp: { allowFrom: ["*"] },
} as ClawdbotConfig;
const params = buildParams("!poll", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("No active bash job");
});
it("routes !stop through the /bash handler", async () => {
resetBashChatCommandForTests();
const cfg = {
commands: { bash: true, text: true },
whatsapp: { allowFrom: ["*"] },
} as ClawdbotConfig;
const params = buildParams("!stop", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("No active bash job");
});
});
describe("handleCommands identity", () => {
it("returns sender details for /whoami", async () => {
const cfg = {

View File

@@ -83,6 +83,7 @@ import type {
} from "../thinking.js";
import type { ReplyPayload } from "../types.js";
import { isAbortTrigger, setAbortMemory } from "./abort.js";
import { handleBashChatCommand } from "./bash-command.js";
import { parseConfigCommand } from "./config-commands.js";
import { parseDebugCommand } from "./debug-commands.js";
import type { InlineDirectives } from "./directive-handling.js";
@@ -400,6 +401,11 @@ export async function handleCommands(params: {
command: CommandContext;
agentId?: string;
directives: InlineDirectives;
elevated: {
enabled: boolean;
allowed: boolean;
failures: Array<{ gate: string; key: string }>;
};
sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>;
sessionKey: string;
@@ -425,6 +431,7 @@ export async function handleCommands(params: {
cfg,
command,
directives,
elevated,
sessionEntry,
sessionStore,
sessionKey,
@@ -465,6 +472,30 @@ export async function handleCommands(params: {
commandSource: ctx.CommandSource,
});
const bashSlashRequested =
allowTextCommands &&
(command.commandBodyNormalized === "/bash" ||
command.commandBodyNormalized.startsWith("/bash "));
const bashBangRequested =
allowTextCommands && command.commandBodyNormalized.startsWith("!");
if (bashSlashRequested || (bashBangRequested && command.isAuthorizedSender)) {
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /bash from unauthorized sender: ${command.senderId || "<unknown>"}`,
);
return { shouldContinue: false };
}
const reply = await handleBashChatCommand({
ctx,
cfg,
agentId: params.agentId,
sessionKey,
isGroup,
elevated,
});
return { shouldContinue: false, reply };
}
if (allowTextCommands && activationCommand.hasCommand) {
if (!isGroup) {
return {

View File

@@ -37,6 +37,11 @@ import { applyVerboseOverride } from "../../sessions/level-overrides.js";
import { shortenHomePath } from "../../utils.js";
import { extractModelDirective } from "../model.js";
import type { MsgContext } from "../templating.js";
import {
formatThinkingLevels,
formatXHighModelHint,
supportsXHighThinking,
} from "../thinking.js";
import type { ReplyPayload } from "../types.js";
import {
type ElevatedLevel,
@@ -778,6 +783,7 @@ export async function handleDirectiveOnly(params: {
allowedModelCatalog,
resetModelOverride,
provider,
model,
initialModelLabel,
formatModelSwitchEvent,
currentThinkLevel,
@@ -943,6 +949,117 @@ export async function handleDirectiveOnly(params: {
}
}
let modelSelection: ModelDirectiveSelection | undefined;
let profileOverride: string | undefined;
if (directives.hasModelDirective && directives.rawModelDirective) {
const raw = directives.rawModelDirective.trim();
if (/^[0-9]+$/.test(raw)) {
const resolvedDefault = resolveConfiguredModelRef({
cfg: params.cfg,
defaultProvider,
defaultModel,
});
const pickerCatalog: ModelPickerCatalogEntry[] = (() => {
const keys = new Set<string>();
const out: ModelPickerCatalogEntry[] = [];
const push = (entry: ModelPickerCatalogEntry) => {
const provider = normalizeProviderId(entry.provider);
const id = String(entry.id ?? "").trim();
if (!provider || !id) return;
const key = modelKey(provider, id);
if (keys.has(key)) return;
keys.add(key);
out.push({ provider, id, name: entry.name });
};
for (const entry of allowedModelCatalog) push(entry);
for (const rawKey of Object.keys(
params.cfg.agents?.defaults?.models ?? {},
)) {
const resolved = resolveModelRefFromString({
raw: String(rawKey),
defaultProvider,
aliasIndex,
});
if (!resolved) continue;
push({
provider: resolved.ref.provider,
id: resolved.ref.model,
name: resolved.ref.model,
});
}
if (resolvedDefault.model) {
push({
provider: resolvedDefault.provider,
id: resolvedDefault.model,
name: resolvedDefault.model,
});
}
return out;
})();
const items = buildModelPickerItems(pickerCatalog);
const index = Number.parseInt(raw, 10) - 1;
const item = Number.isFinite(index) ? items[index] : undefined;
if (!item) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const picked = pickProviderForModel({
item,
preferredProvider: params.provider,
});
if (!picked) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const key = `${picked.provider}/${picked.model}`;
const aliases = aliasIndex.byKey.get(key);
const alias = aliases && aliases.length > 0 ? aliases[0] : undefined;
modelSelection = {
provider: picked.provider,
model: picked.model,
isDefault:
picked.provider === defaultProvider && picked.model === defaultModel,
...(alias ? { alias } : {}),
};
} else {
const resolved = resolveModelDirectiveSelection({
raw,
defaultProvider,
defaultModel,
aliasIndex,
allowedModelKeys,
});
if (resolved.error) {
return { text: resolved.error };
}
modelSelection = resolved.selection;
}
if (modelSelection && directives.rawModelProfile) {
const profileResolved = resolveProfileOverride({
rawProfile: directives.rawModelProfile,
provider: modelSelection.provider,
cfg: params.cfg,
agentDir,
});
if (profileResolved.error) {
return { text: profileResolved.error };
}
profileOverride = profileResolved.profileId;
}
}
if (directives.rawModelProfile && !modelSelection) {
return { text: "Auth profile override requires a model selection." };
}
const resolvedProvider = modelSelection?.provider ?? provider;
const resolvedModel = modelSelection?.model ?? model;
if (directives.hasThinkDirective && !directives.thinkLevel) {
// If no argument was provided, show the current level
if (!directives.rawThinkLevel) {
@@ -950,12 +1067,12 @@ export async function handleDirectiveOnly(params: {
return {
text: withOptions(
`Current thinking level: ${level}.`,
"off, minimal, low, medium, high",
formatThinkingLevels(resolvedProvider, resolvedModel),
),
};
}
return {
text: `Unrecognized thinking level "${directives.rawThinkLevel}". Valid levels: off, minimal, low, medium, high.`,
text: `Unrecognized thinking level "${directives.rawThinkLevel}". Valid levels: ${formatThinkingLevels(resolvedProvider, resolvedModel)}.`,
};
}
if (directives.hasVerboseDirective && !directives.verboseLevel) {
@@ -1098,126 +1215,25 @@ export async function handleDirectiveOnly(params: {
return { text: errors.join(" ") };
}
let modelSelection: ModelDirectiveSelection | undefined;
let profileOverride: string | undefined;
if (directives.hasModelDirective && directives.rawModelDirective) {
const raw = directives.rawModelDirective.trim();
if (/^[0-9]+$/.test(raw)) {
const resolvedDefault = resolveConfiguredModelRef({
cfg: params.cfg,
defaultProvider,
defaultModel,
});
const pickerCatalog: ModelPickerCatalogEntry[] = (() => {
const keys = new Set<string>();
const out: ModelPickerCatalogEntry[] = [];
const push = (entry: ModelPickerCatalogEntry) => {
const provider = normalizeProviderId(entry.provider);
const id = String(entry.id ?? "").trim();
if (!provider || !id) return;
const key = modelKey(provider, id);
if (keys.has(key)) return;
keys.add(key);
out.push({ provider, id, name: entry.name });
};
for (const entry of allowedModelCatalog) push(entry);
for (const rawKey of Object.keys(
params.cfg.agents?.defaults?.models ?? {},
)) {
const resolved = resolveModelRefFromString({
raw: String(rawKey),
defaultProvider,
aliasIndex,
});
if (!resolved) continue;
push({
provider: resolved.ref.provider,
id: resolved.ref.model,
name: resolved.ref.model,
});
}
if (resolvedDefault.model) {
push({
provider: resolvedDefault.provider,
id: resolvedDefault.model,
name: resolvedDefault.model,
});
}
return out;
})();
const items = buildModelPickerItems(pickerCatalog);
const index = Number.parseInt(raw, 10) - 1;
const item = Number.isFinite(index) ? items[index] : undefined;
if (!item) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const picked = pickProviderForModel({
item,
preferredProvider: params.provider,
});
if (!picked) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const key = `${picked.provider}/${picked.model}`;
const aliases = aliasIndex.byKey.get(key);
const alias = aliases && aliases.length > 0 ? aliases[0] : undefined;
modelSelection = {
provider: picked.provider,
model: picked.model,
isDefault:
picked.provider === defaultProvider && picked.model === defaultModel,
...(alias ? { alias } : {}),
};
} else {
const resolved = resolveModelDirectiveSelection({
raw,
defaultProvider,
defaultModel,
aliasIndex,
allowedModelKeys,
});
if (resolved.error) {
return { text: resolved.error };
}
modelSelection = resolved.selection;
}
if (modelSelection) {
if (directives.rawModelProfile) {
const profileResolved = resolveProfileOverride({
rawProfile: directives.rawModelProfile,
provider: modelSelection.provider,
cfg: params.cfg,
agentDir,
});
if (profileResolved.error) {
return { text: profileResolved.error };
}
profileOverride = profileResolved.profileId;
}
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
if (nextLabel !== initialModelLabel) {
enqueueSystemEvent(
formatModelSwitchEvent(nextLabel, modelSelection.alias),
{
sessionKey,
contextKey: `model:${nextLabel}`,
},
);
}
}
}
if (directives.rawModelProfile && !modelSelection) {
return { text: "Auth profile override requires a model selection." };
if (
directives.hasThinkDirective &&
directives.thinkLevel === "xhigh" &&
!supportsXHighThinking(resolvedProvider, resolvedModel)
) {
return {
text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
};
}
const nextThinkLevel = directives.hasThinkDirective
? directives.thinkLevel
: ((sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
currentThinkLevel);
const shouldDowngradeXHigh =
!directives.hasThinkDirective &&
nextThinkLevel === "xhigh" &&
!supportsXHighThinking(resolvedProvider, resolvedModel);
if (sessionEntry && sessionStore && sessionKey) {
const prevElevatedLevel =
currentElevatedLevel ??
@@ -1239,6 +1255,9 @@ export async function handleDirectiveOnly(params: {
if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel;
else sessionEntry.thinkingLevel = directives.thinkLevel;
}
if (shouldDowngradeXHigh) {
sessionEntry.thinkingLevel = "high";
}
if (directives.hasVerboseDirective && directives.verboseLevel) {
applyVerboseOverride(sessionEntry, directives.verboseLevel);
}
@@ -1295,6 +1314,18 @@ export async function handleDirectiveOnly(params: {
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
if (modelSelection) {
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
if (nextLabel !== initialModelLabel) {
enqueueSystemEvent(
formatModelSwitchEvent(nextLabel, modelSelection.alias),
{
sessionKey,
contextKey: `model:${nextLabel}`,
},
);
}
}
if (elevatedChanged) {
const nextElevated = (sessionEntry.elevatedLevel ??
"off") as ElevatedLevel;
@@ -1345,6 +1376,11 @@ export async function handleDirectiveOnly(params: {
);
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
}
if (shouldDowngradeXHigh) {
parts.push(
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
);
}
if (modelSelection) {
const label = `${modelSelection.provider}/${modelSelection.model}`;
const labelWithAlias = modelSelection.alias

View File

@@ -1,10 +1,34 @@
import { describe, expect, it } from "vitest";
import { normalizeReasoningLevel, normalizeThinkLevel } from "./thinking.js";
import {
listThinkingLevels,
normalizeReasoningLevel,
normalizeThinkLevel,
} from "./thinking.js";
describe("normalizeThinkLevel", () => {
it("accepts mid as medium", () => {
expect(normalizeThinkLevel("mid")).toBe("medium");
});
it("accepts xhigh", () => {
expect(normalizeThinkLevel("xhigh")).toBe("xhigh");
});
});
describe("listThinkingLevels", () => {
it("includes xhigh for codex models", () => {
expect(listThinkingLevels(undefined, "gpt-5.2-codex")).toContain("xhigh");
});
it("includes xhigh for openai gpt-5.2", () => {
expect(listThinkingLevels("openai", "gpt-5.2")).toContain("xhigh");
});
it("excludes xhigh for non-codex models", () => {
expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain(
"xhigh",
);
});
});
describe("normalizeReasoningLevel", () => {

View File

@@ -1,9 +1,30 @@
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
export type ThinkLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh";
export type VerboseLevel = "off" | "on";
export type ElevatedLevel = "off" | "on";
export type ReasoningLevel = "off" | "on" | "stream";
export type UsageDisplayLevel = "off" | "on";
export const XHIGH_MODEL_REFS = [
"openai/gpt-5.2",
"openai-codex/gpt-5.2-codex",
"openai-codex/gpt-5.1-codex",
] as const;
const XHIGH_MODEL_SET = new Set(
XHIGH_MODEL_REFS.map((entry) => entry.toLowerCase()),
);
const XHIGH_MODEL_IDS = new Set(
XHIGH_MODEL_REFS.map((entry) => entry.split("/")[1]?.toLowerCase()).filter(
(entry): entry is string => Boolean(entry),
),
);
// Normalize user-provided thinking level strings to the canonical enum.
export function normalizeThinkLevel(
raw?: string | null,
@@ -32,10 +53,49 @@ export function normalizeThinkLevel(
].includes(key)
)
return "high";
if (["xhigh", "x-high", "x_high"].includes(key)) return "xhigh";
if (["think"].includes(key)) return "minimal";
return undefined;
}
export function supportsXHighThinking(
provider?: string | null,
model?: string | null,
): boolean {
const modelKey = model?.trim().toLowerCase();
if (!modelKey) return false;
const providerKey = provider?.trim().toLowerCase();
if (providerKey) {
return XHIGH_MODEL_SET.has(`${providerKey}/${modelKey}`);
}
return XHIGH_MODEL_IDS.has(modelKey);
}
export function listThinkingLevels(
provider?: string | null,
model?: string | null,
): ThinkLevel[] {
const levels: ThinkLevel[] = ["off", "minimal", "low", "medium", "high"];
if (supportsXHighThinking(provider, model)) levels.push("xhigh");
return levels;
}
export function formatThinkingLevels(
provider?: string | null,
model?: string | null,
separator = ", ",
): string {
return listThinkingLevels(provider, model).join(separator);
}
export function formatXHighModelHint(): string {
const refs = [...XHIGH_MODEL_REFS] as string[];
if (refs.length === 0) return "unknown model";
if (refs.length === 1) return refs[0];
if (refs.length === 2) return `${refs[0]} or ${refs[1]}`;
return `${refs.slice(0, -1).join(", ")} or ${refs[refs.length - 1]}`;
}
// Normalize verbose flags used to toggle agent verbosity.
export function normalizeVerboseLevel(
raw?: string | null,

View File

@@ -30,8 +30,11 @@ import { hasNonzeroUsage } from "../agents/usage.js";
import { ensureAgentWorkspace } from "../agents/workspace.js";
import type { MsgContext } from "../auto-reply/templating.js";
import {
formatThinkingLevels,
formatXHighModelHint,
normalizeThinkLevel,
normalizeVerboseLevel,
supportsXHighThinking,
type ThinkLevel,
type VerboseLevel,
} from "../auto-reply/thinking.js";
@@ -216,17 +219,26 @@ export async function agentCommand(
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
});
const workspaceDir = workspace.dir;
const configuredModel = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const thinkingLevelsHint = formatThinkingLevels(
configuredModel.provider,
configuredModel.model,
);
const thinkOverride = normalizeThinkLevel(opts.thinking);
const thinkOnce = normalizeThinkLevel(opts.thinkingOnce);
if (opts.thinking && !thinkOverride) {
throw new Error(
"Invalid thinking level. Use one of: off, minimal, low, medium, high.",
`Invalid thinking level. Use one of: ${thinkingLevelsHint}.`,
);
}
if (opts.thinkingOnce && !thinkOnce) {
throw new Error(
"Invalid one-shot thinking level. Use one of: off, minimal, low, medium, high.",
`Invalid one-shot thinking level. Use one of: ${thinkingLevelsHint}.`,
);
}
@@ -444,6 +456,29 @@ export async function agentCommand(
catalog: catalogForThinking,
});
}
if (
resolvedThinkLevel === "xhigh" &&
!supportsXHighThinking(provider, model)
) {
const explicitThink = Boolean(thinkOnce || thinkOverride);
if (explicitThink) {
throw new Error(
`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
);
}
resolvedThinkLevel = "high";
if (
sessionEntry &&
sessionStore &&
sessionKey &&
sessionEntry.thinkingLevel === "xhigh"
) {
sessionEntry.thinkingLevel = "high";
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
}
}
const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, {
agentId: sessionAgentId,
});

View File

@@ -18,6 +18,13 @@ vi.mock("../providers/github-copilot-auth.js", () => ({
const noopAsync = async () => {};
const noop = () => {};
const authProfilePathFor = (agentDir: string) =>
path.join(agentDir, "auth-profiles.json");
const requireAgentDir = () => {
const agentDir = process.env.CLAWDBOT_AGENT_DIR;
if (!agentDir) throw new Error("CLAWDBOT_AGENT_DIR not set");
return agentDir;
};
describe("applyAuthChoice", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
@@ -111,13 +118,7 @@ describe("applyAuthChoice", () => {
mode: "api_key",
});
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
@@ -170,13 +171,7 @@ describe("applyAuthChoice", () => {
mode: "api_key",
});
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
@@ -337,13 +332,7 @@ describe("applyAuthChoice", () => {
"openrouter/auto",
);
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
@@ -426,13 +415,7 @@ describe("applyAuthChoice", () => {
mode: "oauth",
});
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<

View File

@@ -0,0 +1,37 @@
import fs from "node:fs";
import path from "node:path";
import { note } from "../terminal/note.js";
export function noteSourceInstallIssues(root: string | null) {
if (!root) return;
const workspaceMarker = path.join(root, "pnpm-workspace.yaml");
if (!fs.existsSync(workspaceMarker)) return;
const warnings: string[] = [];
const nodeModules = path.join(root, "node_modules");
const pnpmStore = path.join(nodeModules, ".pnpm");
const tsxBin = path.join(nodeModules, ".bin", "tsx");
const srcEntry = path.join(root, "src", "entry.ts");
if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) {
warnings.push(
"- node_modules was not installed by pnpm (missing node_modules/.pnpm). Run: pnpm install",
);
}
if (fs.existsSync(path.join(root, "package-lock.json"))) {
warnings.push(
"- package-lock.json present in a pnpm workspace. If you ran npm install, remove it and reinstall with pnpm.",
);
}
if (fs.existsSync(srcEntry) && !fs.existsSync(tsxBin)) {
warnings.push("- tsx binary is missing for source runs. Run: pnpm install");
}
if (warnings.length > 0) {
note(warnings.join("\n"), "Install");
}
}

View File

@@ -23,10 +23,49 @@ export async function maybeRepairUiProtocolFreshness(
try {
const [schemaStats, uiStats] = await Promise.all([
fs.stat(schemaPath),
fs.stat(uiIndexPath),
fs.stat(schemaPath).catch(() => null),
fs.stat(uiIndexPath).catch(() => null),
]);
if (schemaStats && !uiStats) {
note(
["- Control UI assets are missing.", "- Run: pnpm ui:build"].join("\n"),
"UI",
);
const shouldRepair = await prompter.confirmRepair({
message: "Build Control UI assets now?",
initialValue: true,
});
if (shouldRepair) {
note("Building Control UI assets... (this may take a moment)", "UI");
const uiScriptPath = path.join(root, "scripts/ui.js");
const buildResult = await runCommandWithTimeout(
[process.execPath, uiScriptPath, "build"],
{
cwd: root,
timeoutMs: 120_000,
env: { ...process.env, FORCE_COLOR: "1" },
},
);
if (buildResult.code === 0) {
note("UI build complete.", "UI");
} else {
const details = [
`UI build failed (exit ${buildResult.code ?? "unknown"}).`,
buildResult.stderr.trim() ? buildResult.stderr.trim() : null,
]
.filter(Boolean)
.join("\n");
note(details, "UI");
}
}
return;
}
if (!schemaStats || !uiStats) return;
if (schemaStats.mtime > uiStats.mtime) {
const uiMtimeIso = uiStats.mtime.toISOString();
// Find changes since the UI build

View File

@@ -62,6 +62,7 @@ import {
maybeRepairGatewayServiceConfig,
maybeScanExtraGatewayServices,
} from "./doctor-gateway-services.js";
import { noteSourceInstallIssues } from "./doctor-install.js";
import {
maybeMigrateLegacyConfigFile,
normalizeLegacyConfigValues,
@@ -187,6 +188,12 @@ export async function doctorCommand(
printWizardHeader(runtime);
intro("Clawdbot doctor");
const root = await resolveClawdbotPackageRoot({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
const updateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS === "1";
const canOfferUpdate =
!updateInProgress &&
@@ -195,11 +202,6 @@ export async function doctorCommand(
options.repair !== true &&
Boolean(process.stdin.isTTY);
if (canOfferUpdate) {
const root = await resolveClawdbotPackageRoot({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
if (root) {
const git = await detectClawdbotGitCheckout(root);
if (git === "git") {
@@ -250,6 +252,7 @@ export async function doctorCommand(
}
await maybeRepairUiProtocolFreshness(runtime, prompter);
noteSourceInstallIssues(root);
await maybeMigrateLegacyConfigFile(runtime);

View File

@@ -18,9 +18,18 @@ import {
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF,
setMinimaxApiKey,
writeOAuthCredentials,
} from "./onboard-auth.js";
const authProfilePathFor = (agentDir: string) =>
path.join(agentDir, "auth-profiles.json");
const requireAgentDir = () => {
const agentDir = process.env.CLAWDBOT_AGENT_DIR;
if (!agentDir) throw new Error("CLAWDBOT_AGENT_DIR not set");
return agentDir;
};
describe("writeOAuthCredentials", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
@@ -50,10 +59,9 @@ describe("writeOAuthCredentials", () => {
delete process.env.CLAWDBOT_OAUTH_DIR;
});
it("writes auth-profiles.json under CLAWDBOT_STATE_DIR/agents/main/agent", async () => {
it("writes auth-profiles.json under CLAWDBOT_AGENT_DIR when set", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-oauth-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
// Even if legacy env vars are set, onboarding should write to the multi-agent path.
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
@@ -65,14 +73,7 @@ describe("writeOAuthCredentials", () => {
await writeOAuthCredentials("openai-codex", creds);
// Now writes to the multi-agent path: agents/main/agent
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, OAuthCredentials & { type?: string }>;
@@ -85,7 +86,80 @@ describe("writeOAuthCredentials", () => {
await expect(
fs.readFile(
path.join(tempStateDir, "agent", "auth-profiles.json"),
path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
),
"utf8",
),
).rejects.toThrow();
});
});
describe("setMinimaxApiKey", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
let tempStateDir: string | null = null;
afterEach(async () => {
if (tempStateDir) {
await fs.rm(tempStateDir, { recursive: true, force: true });
tempStateDir = null;
}
if (previousStateDir === undefined) {
delete process.env.CLAWDBOT_STATE_DIR;
} else {
process.env.CLAWDBOT_STATE_DIR = previousStateDir;
}
if (previousAgentDir === undefined) {
delete process.env.CLAWDBOT_AGENT_DIR;
} else {
process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
}
if (previousPiAgentDir === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
});
it("writes to CLAWDBOT_AGENT_DIR when set", async () => {
tempStateDir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-minimax-"),
);
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "custom-agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
await setMinimaxApiKey("sk-minimax-test");
const customAuthPath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(customAuthPath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<
string,
{ type?: string; provider?: string; key?: string }
>;
};
expect(parsed.profiles?.["minimax:default"]).toMatchObject({
type: "api_key",
provider: "minimax",
key: "sk-minimax-test",
});
await expect(
fs.readFile(
path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
),
"utf8",
),
).rejects.toThrow();

View File

@@ -1,5 +1,5 @@
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { resolveDefaultAgentDir } from "../agents/agent-scope.js";
import { resolveClawdbotAgentDir } from "../agents/agent-paths.js";
import { upsertAuthProfile } from "../agents/auth-profiles.js";
import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
import {
@@ -24,6 +24,9 @@ const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`;
export { SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF };
const resolveAuthAgentDir = (agentDir?: string) =>
agentDir ?? resolveClawdbotAgentDir();
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
const MINIMAX_API_COST = {
input: 15,
@@ -107,7 +110,7 @@ export async function writeOAuthCredentials(
creds: OAuthCredentials,
agentDir?: string,
): Promise<void> {
// Write to the multi-agent path so gateway finds credentials on startup
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: `${provider}:${creds.email ?? "default"}`,
credential: {
@@ -115,12 +118,12 @@ export async function writeOAuthCredentials(
provider,
...creds,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setAnthropicApiKey(key: string, agentDir?: string) {
// Write to the multi-agent path so gateway finds credentials on startup
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "anthropic:default",
credential: {
@@ -128,12 +131,12 @@ export async function setAnthropicApiKey(key: string, agentDir?: string) {
provider: "anthropic",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setGeminiApiKey(key: string, agentDir?: string) {
// Write to the multi-agent path so gateway finds credentials on startup
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "google:default",
credential: {
@@ -141,12 +144,12 @@ export async function setGeminiApiKey(key: string, agentDir?: string) {
provider: "google",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setMinimaxApiKey(key: string, agentDir?: string) {
// Write to the multi-agent path so gateway finds credentials on startup
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "minimax:default",
credential: {
@@ -154,12 +157,12 @@ export async function setMinimaxApiKey(key: string, agentDir?: string) {
provider: "minimax",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setMoonshotApiKey(key: string, agentDir?: string) {
// Write to the multi-agent path so gateway finds credentials on startup
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "moonshot:default",
credential: {
@@ -167,12 +170,12 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) {
provider: "moonshot",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setSyntheticApiKey(key: string, agentDir?: string) {
// Write to the multi-agent path so gateway finds credentials on startup
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "synthetic:default",
credential: {
@@ -180,7 +183,7 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) {
provider: "synthetic",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
@@ -188,7 +191,7 @@ export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7";
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
export async function setZaiApiKey(key: string, agentDir?: string) {
// Write to the multi-agent path so gateway finds credentials on startup
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "zai:default",
credential: {
@@ -196,7 +199,7 @@ export async function setZaiApiKey(key: string, agentDir?: string) {
provider: "zai",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
@@ -208,7 +211,7 @@ export async function setOpenrouterApiKey(key: string, agentDir?: string) {
provider: "openrouter",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}
@@ -714,7 +717,7 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) {
provider: "opencode",
key,
},
agentDir: agentDir ?? resolveDefaultAgentDir(),
agentDir: resolveAuthAgentDir(agentDir),
});
}

View File

@@ -155,6 +155,8 @@ const FIELD_LABELS: Record<string, string> = {
"agents.defaults.cliBackends": "CLI Backends",
"commands.native": "Native Commands",
"commands.text": "Text Commands",
"commands.bash": "Allow Bash Chat Command",
"commands.bashForegroundMs": "Bash Foreground Window (ms)",
"commands.config": "Allow /config",
"commands.debug": "Allow /debug",
"commands.restart": "Allow Restart",
@@ -289,6 +291,10 @@ const FIELD_HELP: Record<string, string> = {
"commands.native":
"Register native commands with connectors that support it (Discord/Slack/Telegram).",
"commands.text": "Allow text command parsing (slash commands only).",
"commands.bash":
"Allow bash chat command (`!`; `/bash` alias) to run host shell commands (default: false; requires tools.elevated).",
"commands.bashForegroundMs":
"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).",
"commands.config":
"Allow /config chat command to read/write config on disk (default: false).",
"commands.debug":

View File

@@ -1244,6 +1244,10 @@ export type CommandsConfig = {
native?: NativeCommandsSetting;
/** Enable text command parsing (default: true). */
text?: boolean;
/** Allow bash chat command (`!`; `/bash` alias) (default: false). */
bash?: boolean;
/** How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately). */
bashForegroundMs?: number;
/** Allow /config command (default: false). */
config?: boolean;
/** Allow /debug command (default: false). */
@@ -1629,7 +1633,7 @@ export type AgentDefaultsConfig = {
/** Vector memory search configuration (per-agent overrides supported). */
memorySearch?: MemorySearchConfig;
/** Default thinking level when no /think directive is present. */
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high";
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
/** Default verbose level when no /verbose directive is present. */
verboseDefault?: "off" | "on";
/** Default elevated level when no /elevated directive is present. */

View File

@@ -724,6 +724,8 @@ const CommandsSchema = z
.object({
native: NativeCommandsSettingSchema.optional().default("auto"),
text: z.boolean().optional(),
bash: z.boolean().optional(),
bashForegroundMs: z.number().int().min(0).max(30_000).optional(),
config: z.boolean().optional(),
debug: z.boolean().optional(),
restart: z.boolean().optional(),
@@ -1248,6 +1250,7 @@ const AgentDefaultsSchema = z
z.literal("low"),
z.literal("medium"),
z.literal("high"),
z.literal("xhigh"),
])
.optional(),
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),

View File

@@ -32,7 +32,11 @@ import {
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
stripHeartbeatToken,
} from "../auto-reply/heartbeat.js";
import { normalizeThinkLevel } from "../auto-reply/thinking.js";
import {
formatXHighModelHint,
normalizeThinkLevel,
supportsXHighThinking,
} from "../auto-reply/thinking.js";
import type { CliDeps } from "../cli/deps.js";
import type { ClawdbotConfig } from "../config/config.js";
import {
@@ -367,6 +371,11 @@ export async function runCronIsolatedAgentTurn(params: {
catalog: await loadCatalog(),
});
}
if (thinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) {
throw new Error(
`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
);
}
const timeoutMs = resolveAgentTimeoutMs({
cfg: cfgWithAgentDefaults,

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import process from "node:process";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
@@ -8,6 +9,33 @@ if (process.argv.includes("--no-color")) {
process.env.FORCE_COLOR = "0";
}
const EXPERIMENTAL_WARNING_FLAG = "--disable-warning=ExperimentalWarning";
function hasExperimentalWarningSuppressed(nodeOptions: string): boolean {
if (!nodeOptions) return false;
return (
nodeOptions.includes(EXPERIMENTAL_WARNING_FLAG) ||
nodeOptions.includes("--no-warnings")
);
}
function ensureExperimentalWarningSuppressed(): void {
if (process.env.CLAWDBOT_NODE_OPTIONS_READY === "1") return;
const nodeOptions = process.env.NODE_OPTIONS ?? "";
if (hasExperimentalWarningSuppressed(nodeOptions)) return;
process.env.CLAWDBOT_NODE_OPTIONS_READY = "1";
process.env.NODE_OPTIONS =
`${nodeOptions} ${EXPERIMENTAL_WARNING_FLAG}`.trim();
const result = spawnSync(process.execPath, process.argv.slice(1), {
stdio: "inherit",
env: process.env,
});
if (result.signal) process.exit(1);
process.exit(result.status ?? 1);
}
ensureExperimentalWarningSuppressed();
const parsed = parseCliProfileArgs(process.argv);
if (!parsed.ok) {
// Keep it simple; Commander will handle rich help/errors after we strip flags.

View File

@@ -8,10 +8,13 @@ import {
} from "../agents/model-selection.js";
import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
import {
formatThinkingLevels,
formatXHighModelHint,
normalizeElevatedLevel,
normalizeReasoningLevel,
normalizeThinkLevel,
normalizeUsageDisplay,
supportsXHighThinking,
} from "../auto-reply/thinking.js";
import type { ClawdbotConfig } from "../config/config.js";
import type { SessionEntry } from "../config/sessions.js";
@@ -95,8 +98,17 @@ export async function applySessionsPatchToStore(params: {
} else if (raw !== undefined) {
const normalized = normalizeThinkLevel(String(raw));
if (!normalized) {
const resolvedDefault = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const hintProvider =
existing?.providerOverride?.trim() || resolvedDefault.provider;
const hintModel =
existing?.modelOverride?.trim() || resolvedDefault.model;
return invalid(
"invalid thinkingLevel (use off|minimal|low|medium|high)",
`invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`,
);
}
if (normalized === "off") delete next.thinkingLevel;
@@ -196,6 +208,24 @@ export async function applySessionsPatchToStore(params: {
}
}
if (next.thinkingLevel === "xhigh") {
const resolvedDefault = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const effectiveProvider = next.providerOverride ?? resolvedDefault.provider;
const effectiveModel = next.modelOverride ?? resolvedDefault.model;
if (!supportsXHighThinking(effectiveProvider, effectiveModel)) {
if ("thinkingLevel" in patch) {
return invalid(
`thinkingLevel "xhigh" is only supported for ${formatXHighModelHint()}`,
);
}
next.thinkingLevel = "high";
}
}
if ("sendPolicy" in patch) {
const raw = patch.sendPolicy;
if (raw === null) {

View File

@@ -171,7 +171,7 @@ describe("google-shared convertMessages", () => {
{
type: "thinking",
thinking: "hidden",
thinkingSignature: "sig",
thinkingSignature: "c2ln",
},
],
api: "google-generative-ai",
@@ -202,7 +202,7 @@ describe("google-shared convertMessages", () => {
expect(contents[0].role).toBe("model");
expect(contents[0].parts?.[0]).toMatchObject({
thought: true,
thoughtSignature: "sig",
thoughtSignature: "c2ln",
});
});
@@ -216,7 +216,7 @@ describe("google-shared convertMessages", () => {
{
type: "thinking",
thinking: "structured",
thinkingSignature: "sig",
thinkingSignature: "c2ln",
},
],
api: "google-generative-ai",
@@ -247,7 +247,7 @@ describe("google-shared convertMessages", () => {
expect(parts).toHaveLength(1);
expect(parts[0]).toMatchObject({
thought: true,
thoughtSignature: "sig",
thoughtSignature: "c2ln",
});
});

View File

@@ -1,6 +1,9 @@
import type { SlashCommand } from "@mariozechner/pi-tui";
import {
formatThinkingLevels,
listThinkingLevels,
} from "../auto-reply/thinking.js";
const THINK_LEVELS = ["off", "minimal", "low", "medium", "high"];
const VERBOSE_LEVELS = ["on", "off"];
const REASONING_LEVELS = ["on", "off"];
const ELEVATED_LEVELS = ["on", "off"];
@@ -12,6 +15,11 @@ export type ParsedCommand = {
args: string;
};
export type SlashCommandOptions = {
provider?: string;
model?: string;
};
const COMMAND_ALIASES: Record<string, string> = {
elev: "elevated",
};
@@ -27,7 +35,10 @@ export function parseCommand(input: string): ParsedCommand {
};
}
export function getSlashCommands(): SlashCommand[] {
export function getSlashCommands(
options: SlashCommandOptions = {},
): SlashCommand[] {
const thinkLevels = listThinkingLevels(options.provider, options.model);
return [
{ name: "help", description: "Show slash command help" },
{ name: "status", description: "Show gateway status summary" },
@@ -44,9 +55,9 @@ export function getSlashCommands(): SlashCommand[] {
name: "think",
description: "Set thinking level",
getArgumentCompletions: (prefix) =>
THINK_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map(
(value) => ({ value, label: value }),
),
thinkLevels
.filter((v) => v.startsWith(prefix.toLowerCase()))
.map((value) => ({ value, label: value })),
},
{
name: "verbose",
@@ -105,7 +116,12 @@ export function getSlashCommands(): SlashCommand[] {
];
}
export function helpText(): string {
export function helpText(options: SlashCommandOptions = {}): string {
const thinkLevels = formatThinkingLevels(
options.provider,
options.model,
"|",
);
return [
"Slash commands:",
"/help",
@@ -113,7 +129,7 @@ export function helpText(): string {
"/agent <id> (or /agents)",
"/session <key> (or /sessions)",
"/model <provider/model> (or /models)",
"/think <off|minimal|low|medium|high>",
`/think <${thinkLevels}>`,
"/verbose <on|off>",
"/reasoning <on|off>",
"/cost <on|off>",

View File

@@ -7,7 +7,10 @@ import {
TUI,
} from "@mariozechner/pi-tui";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { normalizeUsageDisplay } from "../auto-reply/thinking.js";
import {
formatThinkingLevels,
normalizeUsageDisplay,
} from "../auto-reply/thinking.js";
import { loadConfig } from "../config/config.js";
import { formatAge } from "../infra/provider-summary.js";
import {
@@ -239,6 +242,18 @@ export async function runTui(opts: TuiOptions) {
root.addChild(footer);
root.addChild(editor);
const updateAutocompleteProvider = () => {
editor.setAutocompleteProvider(
new CombinedAutocompleteProvider(
getSlashCommands({
provider: sessionInfo.modelProvider,
model: sessionInfo.model,
}),
process.cwd(),
),
);
};
const tui = new TUI(new ProcessTerminal());
tui.addChild(root);
tui.setFocus(editor);
@@ -524,6 +539,7 @@ export async function runTui(opts: TuiOptions) {
} catch (err) {
chatLog.addSystem(`sessions list failed: ${String(err)}`);
}
updateAutocompleteProvider();
updateFooter();
tui.requestRender();
};
@@ -861,7 +877,12 @@ export async function runTui(opts: TuiOptions) {
if (!name) return;
switch (name) {
case "help":
chatLog.addSystem(helpText());
chatLog.addSystem(
helpText({
provider: sessionInfo.modelProvider,
model: sessionInfo.model,
}),
);
break;
case "status":
try {
@@ -921,7 +942,12 @@ export async function runTui(opts: TuiOptions) {
break;
case "think":
if (!args) {
chatLog.addSystem("usage: /think <off|minimal|low|medium|high>");
const levels = formatThinkingLevels(
sessionInfo.modelProvider,
sessionInfo.model,
"|",
);
chatLog.addSystem(`usage: /think <${levels}>`);
break;
}
try {
@@ -1071,9 +1097,7 @@ export async function runTui(opts: TuiOptions) {
tui.requestRender();
};
editor.setAutocompleteProvider(
new CombinedAutocompleteProvider(getSlashCommands(), process.cwd()),
);
updateAutocompleteProvider();
editor.onSubmit = (text) => {
const value = text.trim();
editor.setText("");

View File

@@ -786,9 +786,15 @@ export async function runOnboardingWizard(
}
}
const controlUiAssets = await ensureControlUiAssetsBuilt(runtime);
if (!controlUiAssets.ok && controlUiAssets.message) {
runtime.error(controlUiAssets.message);
const controlUiEnabled =
nextConfig.gateway?.controlUi?.enabled ??
baseConfig.gateway?.controlUi?.enabled ??
true;
if (!opts.skipUi && controlUiEnabled) {
const controlUiAssets = await ensureControlUiAssetsBuilt(runtime);
if (!controlUiAssets.ok && controlUiAssets.message) {
runtime.error(controlUiAssets.message);
}
}
await prompter.note(

View File

@@ -122,9 +122,9 @@ const spawnGatewayInstance = async (name: string): Promise<GatewayInstance> => {
try {
child = spawn(
"bun",
"node",
[
"src/index.ts",
"dist/index.js",
"gateway",
"--port",
String(port),
@@ -222,7 +222,7 @@ const runCliJson = async (
): Promise<unknown> => {
const stdout: string[] = [];
const stderr: string[] = [];
const child = spawn("bun", ["src/index.ts", ...args], {
const child = spawn("node", ["dist/index.js", ...args], {
cwd: process.cwd(),
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],

View File

@@ -15,10 +15,10 @@
"vite": "7.3.1"
},
"devDependencies": {
"@vitest/browser-playwright": "4.0.16",
"@vitest/browser-playwright": "4.0.17",
"playwright": "^1.57.0",
"typescript": "^5.9.3",
"vitest": "4.0.16"
"vitest": "4.0.17"
},
"pnpm": {
"minimumReleaseAge": 2880