Compare 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

No files matched your search

+45 -110
View File
@@ -1,120 +1,55 @@
# Changelog # Changelog
## 2026.1.13-1 ## 2025.1.12 (Unreleased)
### 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)
### Highlights ### 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 ### New & Improved
- 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). - Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI.
- Browser: add `scrollintoview` action to scroll refs into view before click/type. - 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.
- Memory: embedding providers support OpenAI or local `node-llama-cpp`; config adds defaults + per-agent overrides, provider/fallback metadata surfaced in tools/CLI. - Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config.
- CLI/Tools: new `clawdbot memory` commands plus `memory_search`/`memory_get` tools returning snippets + line ranges and provider info. - 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`.
- 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. - Tools: add browser `scrollintoview` action; allow Claude/Gemini tool param aliases; allow thinking `xhigh` for GPT-5.2/Codex with safe downgrades.
- Doctor: rebuild Control UI assets when protocol schema is newer to avoid stale UI connect errors. (#786) — thanks @meaningfool. - 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.
- Cron: allow jobs to target a specific agent and expose agent selection in the macOS app + Control UI. - Auth/Onboarding: add Chutes OAuth (PKCE + refresh + onboarding choice); normalize API key inputs; default TUI onboarding to `deliver: false`.
- Discord: add `discord.allowBots` to permit bot-authored messages (still ignores its own messages) with docs warning about bot loops. (#802) — thanks @zknicker. - 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).
- 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. - Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer.
- 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. - Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
- 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) ### Installer
- Install: run `clawdbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
### Fixes ### Fixes
- Auto-reply: inline `/status` now honors allowlists (authorized stripped + replied inline; unauthorized leaves text for the agent) to match command gating tests. - Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
- 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/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.
- Models/Tools: include `MiniMax-VL-01` in implicit MiniMax provider so image pairing uses a real vision model. - Auth: drop invalid auth profiles from ordering so environment keys can still be used for providers like MiniMax.
- Telegram: show typing indicator in General forum topics. (#779) — thanks @azade-c. - 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.
- Discord: keep reasoning italics intact when messages are chunked, so reasoning stays italic across multi-part sends. - Agents: auto-recover from compaction context overflow by resetting the session and retrying; propagate overflow details from embedded runs so callers can recover.
- Models: keep explicit GitHub Copilot provider config and honor agent-dir auth profiles for auto-injection. (#705) — thanks @TAGOOZ. - MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing.
- Auto-reply: restore 300-char heartbeat ack limit and keep >300 char replies instead of dropping them; adjust long heartbeat test content accordingly. - Onboarding/Auth: honor `CLAWDBOT_AGENT_DIR` / `PI_CODING_AGENT_DIR` when writing auth profiles (MiniMax). (#829) — thanks @roshanasingh4.
- 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. - Anthropic: handle `overloaded_error` with a friendly message and failover classification. (#832) — thanks @danielz1z.
- Google: apply patched pi-ai `google-gemini-cli` function call handling (strips ids) after upgrading to pi-ai 0.43.0. - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid incorrect role errors.
- 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. - Messaging: enforce context isolation for message tool sends; keep typing indicators alive during tool execution.
- Auto-reply: elevated/reasoning toggles now enqueue system events so the model sees the mode change immediately. - Auto-reply: `/status` allowlist behavior, reasoning-tag enforcement on fallback, and system-event enqueueing for elevated/reasoning toggles.
- Tools: keep `image` available in sandbox and fail over when image models return empty output (fixes “(no text returned)”). - Auto-reply: resolve ambiguous `/model` matches; fix streaming block reply media handling; keep >300 char heartbeat replies instead of dropping.
- Discord: add per-channel `autoThread` to auto-create threads for top-level messages. (#800) — thanks @davidguttman. - 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.
- Discord: fix autoThread routing so replies stay in the created thread and avoid reply references. (#807) — thanks @davidguttman. - Telegram: preserve forum topic thread ids, persist polling offsets, respect account bindings in webhook mode, and show typing indicator in General topics.
- Onboarding: TUI defaults to `deliver: false` to avoid cross-provider auto-delivery leaks; onboarding spawns the TUI with explicit `deliver: false`. (#791 — thanks @roshanasingh4) - 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 ## 2026.1.11
+16 -16
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) - `/status` — compact session status (model + tokens, cost when available)
- `/new` or `/reset` — reset the session - `/new` or `/reset` — reset the session
- `/compact` — compact session context (summary) - `/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` - `/verbose on|off`
- `/cost on|off` — append per-response token/cost usage lines - `/cost on|off` — append per-response token/cost usage lines
- `/restart` — restart the gateway (owner-only in groups) - `/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: Thanks to all clawtributors:
<p align="left"> <p align="left">
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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=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=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=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> </p>
+1 -1
View File
@@ -105,7 +105,7 @@ Isolation options (only for `session=isolated`):
### Model and thinking overrides ### Model and thinking overrides
Isolated jobs (`agentTurn`) can override the model and thinking level: 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`) - `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 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 session model. We recommend model overrides only for isolated jobs to avoid
+71
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.
+4 -4
View File
@@ -257,12 +257,12 @@ Options:
- `--tailscale-reset-on-exit` - `--tailscale-reset-on-exit`
- `--install-daemon` - `--install-daemon`
- `--no-install-daemon` (alias: `--skip-daemon`) - `--no-install-daemon` (alias: `--skip-daemon`)
- `--daemon-runtime <node|bun>` - `--daemon-runtime <node|bun>` (bun not recommended for WhatsApp/Telegram)
- `--skip-providers` - `--skip-providers`
- `--skip-skills` - `--skip-skills`
- `--skip-health` - `--skip-health`
- `--skip-ui` - `--skip-ui`
- `--node-manager <npm|pnpm|bun>` - `--node-manager <npm|pnpm|bun>` (pnpm recommended; bun not recommended for Gateway runtime)
- `--json` - `--json`
### `configure` / `config` ### `configure` / `config`
@@ -398,7 +398,7 @@ Required:
Options: Options:
- `--to <dest>` (for session key and optional delivery) - `--to <dest>` (for session key and optional delivery)
- `--session-id <id>` - `--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>` - `--verbose <on|off>`
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>` - `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
- `--local` - `--local`
@@ -549,7 +549,7 @@ Notes:
- `daemon status` supports `--no-probe`, `--deep`, and `--json` for scripting. - `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` 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 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`. - `daemon install` options: `--port`, `--runtime`, `--token`, `--force`.
### `logs` ### `logs`
+2 -2
View File
@@ -9,7 +9,7 @@ read_when:
Safely update a **source checkout** (git install) of Clawdbot. 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 ## Usage
@@ -32,7 +32,7 @@ High-level:
1. Requires a clean worktree (no uncommitted changes). 1. Requires a clean worktree (no uncommitted changes).
2. Fetches and rebases against `@{upstream}`. 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. 4. Builds + builds the Control UI.
5. Runs `clawdbot doctor` as the final “safe update” check. 5. Runs `clawdbot doctor` as the final “safe update” check.
+3
View File
@@ -92,6 +92,9 @@ This maps to:
`*.blockStreaming` is explicitly set to `true`. Telegram can stream drafts `*.blockStreaming` is explicitly set to `true`. Telegram can stream drafts
(`telegram.streamMode`) without block replies. (`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 draft streaming (token-ish)
Telegram is the only provider with draft streaming: Telegram is the only provider with draft streaming:
@@ -39,7 +39,7 @@ Add `hooks.gmail.model` config option to specify an optional cheaper model for G
| Field | Type | Default | Description | | 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.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 ### Alias Support
+26 -1
View File
@@ -746,8 +746,10 @@ Controls how chat commands are enabled across connectors.
```json5 ```json5
{ {
commands: { commands: {
native: false, // register native commands when supported native: "auto", // register native commands when supported (auto)
text: true, // parse slash commands in chat messages 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) config: false, // allow /config (writes to disk)
debug: false, // allow /debug (runtime-only overrides) debug: false, // allow /debug (runtime-only overrides)
restart: false, // allow /restart + gateway restart tool restart: false, // allow /restart + gateway restart tool
@@ -761,6 +763,8 @@ Notes:
- `commands.text: false` disables parsing chat messages for commands. - `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. - `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. - 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.config: true` enables `/config` (reads/writes `clawdbot.json`).
- `commands.debug: true` enables `/debug` (runtime-only overrides). - `commands.debug: true` enables `/debug` (runtime-only overrides).
- `commands.restart: true` enables `/restart` and the gateway tool restart action. - `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). 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) #### `agents.defaults.cliBackends` (CLI fallback)
Optional CLI backends for text-only fallback runs (no tool calls). These are useful as a Optional CLI backends for text-only fallback runs (no tool calls). These are useful as a
+1
View File
@@ -76,6 +76,7 @@ cat ~/.clawdbot/clawdbot.json
- Security warnings for open DM policies. - Security warnings for open DM policies.
- Gateway auth warnings when no `gateway.auth.token` is set (local mode; offers token generation). - Gateway auth warnings when no `gateway.auth.token` is set (local mode; offers token generation).
- systemd linger check on Linux. - systemd linger check on Linux.
- Source install checks (pnpm workspace mismatch, missing UI assets, missing tsx binary).
- Writes updated config + wizard metadata. - Writes updated config + wizard metadata.
## Detailed behavior and rationale ## Detailed behavior and rationale
+2
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. - `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` - Set `gateway.tailscale.resetOnExit` if you want Clawdbot to undo `tailscale serve`
or `tailscale funnel` configuration on shutdown. 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 ## Tailscale prerequisites + limits
+6 -3
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: read_when:
- You want the fastest local dev loop (bun + watch) - You want the fastest local dev loop (bun + watch)
- You hit Bun install/patch/lifecycle script issues - 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 ## Status
+1
View File
@@ -30,6 +30,7 @@ What it does (high level):
- `git`: clone/build a source checkout and install a wrapper script - `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. - 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). - 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). - 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: If you *want* `sharp` to link against a globally-installed libvips (or you’re debugging), set:
+23 -11
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. 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 ## 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). - Know how your Gateway is running: **foreground terminal** vs **supervised service** (launchd/systemd).
- Snapshot your tailoring: - Snapshot your tailoring:
- Config: `~/.clawdbot/clawdbot.json` - Config: `~/.clawdbot/clawdbot.json`
@@ -29,10 +48,7 @@ npm i -g clawdbot@latest
```bash ```bash
pnpm add -g clawdbot@latest pnpm add -g clawdbot@latest
``` ```
We do **not** recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs).
```bash
bun add -g clawdbot@latest
```
Then: Then:
@@ -59,7 +75,7 @@ It runs a safe-ish update flow:
- Fetches + rebases against the configured upstream. - Fetches + rebases against the configured upstream.
- Installs deps, builds, builds the Control UI, and runs `clawdbot doctor`. - 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) ## Update (Control UI / RPC)
@@ -93,7 +109,7 @@ pnpm clawdbot health
Notes: 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/`. - `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. - 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` ## Always run: `clawdbot doctor`
@@ -145,10 +161,6 @@ npm i -g clawdbot@<version>
pnpm add -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`. Tip: to see the current published version, run `npm view clawdbot version`.
Then restart + re-run doctor: Then restart + re-run doctor:
+2 -1
View File
@@ -6,7 +6,8 @@ read_when:
--- ---
# Platforms # 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 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. Linux companion apps are planned, but the Gateway is fully supported today.
+3 -2
View File
@@ -6,7 +6,8 @@ read_when:
--- ---
# Linux App # 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. 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 ## Install
- [Getting Started](/start/getting-started) - [Getting Started](/start/getting-started)
- [Install & updates](/install/updating) - [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
- [Gateway runbook](/gateway) - [Gateway runbook](/gateway)
+1 -1
View File
@@ -20,7 +20,7 @@ You need Node 22+ on the Mac, then install `clawdbot` globally:
npm install -g clawdbot@<version> 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) ## Launchd (Gateway as LaunchAgent)
+1 -1
View File
@@ -18,7 +18,7 @@ node.
- Runs or connects to the Gateway (local or remote). - Runs or connects to the Gateway (local or remote).
- Exposes macOS‑only tools (Canvas, Camera, Screen Recording, `system.run`). - Exposes macOS‑only tools (Canvas, Camera, Screen Recording, `system.run`).
- Optionally hosts **PeekabooBridge** for UI automation. - 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 ## Local vs remote mode
+3
View File
@@ -174,6 +174,9 @@ Notes:
- The bot lacks channel permissions (View/Send/Read History), or - The bot lacks channel permissions (View/Send/Read History), or
- Your config requires mentions and you didn’t mention it, or - Your config requires mentions and you didn’t mention it, or
- Your guild/channel allowlist denies the channel/user. - 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. - **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"`). - **DMs don’t work**: `discord.dm.enabled=false`, `discord.dm.policy="disabled"`, or you haven’t been approved yet (`discord.dm.policy="pairing"`).
+44
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) ### Optional: Local via LM Studio (manual)
**Best for:** local inference with LM Studio. **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>`. - Model refs are `minimax/<model>`.
- Update pricing values in `models.json` if you need exact cost tracking. - 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. - See [/concepts/model-providers](/concepts/model-providers) for provider rules.
- Use `clawdbot models list` and `clawdbot models set minimax/MiniMax-M2.1` to switch. - 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
```
+1
View File
@@ -31,6 +31,7 @@ model as `provider/model`.
- [Z.AI](/providers/zai) - [Z.AI](/providers/zai)
- [GLM models](/providers/glm) - [GLM models](/providers/glm)
- [MiniMax](/providers/minimax) - [MiniMax](/providers/minimax)
- [Amazon Bedrock](/bedrock)
For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration, For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration,
see [Model providers](/concepts/model-providers). see [Model providers](/concepts/model-providers).
+2 -1
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`. - Fix: `clawdbot doctor` (or restart the gateway). If it persists, relink via `providers login` and inspect `clawdbot logs --follow`.
**Bun runtime** **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.)
+468 -7
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). 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 ## First 60 seconds if something's broken
1) **Quick status (first check)** 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. 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? ### 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? ### 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? ### 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). 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? ### 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? ### How does Codex auth work?
@@ -144,7 +263,11 @@ Pick region-pinned endpoints. OpenRouter exposes US-hosted options for MiniMax,
### Can I use Bun? ### 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`? ### 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). 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? ### 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. 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? ### 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. 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): Install the ClawdHub CLI (pick one package manager):
@@ -227,9 +370,20 @@ npm i -g clawdhub
pnpm add -g clawdhub pnpm add -g clawdhub
``` ```
```bash ### Do you have a Notion or HeyGen integration?
bun add -g clawdhub
``` 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: 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). 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? ### 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). 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`). 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? ### How do I completely uninstall Clawdbot?
See the dedicated guide: [Uninstall](/install/uninstall). 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). 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 ## Remote gateways + nodes
### How do commands propagate between Telegram, the gateway, and 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. 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 ## Env vars and .env loading
### How does Clawdbot load environment variables? ### 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: 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`. `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 ## Sessions & multiple chats
### How do I start a fresh conversation? ### 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>`). - 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). - 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? ### 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. 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 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`. `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? ### Are opus / sonnet / gpt built‑in shortcuts?
Yes. Clawdbot ships a few default shorthands (only applied when the model exists in `agents.defaults.models`): 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** - **Sanity‑check model/auth status**
- Use `clawdbot models status` to see configured models and whether providers are authenticated. - 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? ### 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"`. 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: Fix:
- Start Tailscale on that host (so it has a 100.x address), or - Start Tailscale on that host (so it has a 100.x address), or
- Switch to `gateway.bind: "loopback"` / `"lan"`. - 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? ### 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>)`). 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 ## Logging and debugging
### Where are logs? ### Where are logs?
@@ -1009,6 +1380,96 @@ clawdbot providers login
3) Check GitHub issues or Discord 3) Check GitHub issues or Discord
4) Temporary workaround: check out an older commit 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 ## Answer the exact question from the screenshot/chat log
**Q: “What’s the default model for Anthropic with an API key?”** **Q: “What’s the default model for Anthropic with an API key?”**
+3 -6
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. - **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. - **Providers**: WhatsApp QR login, Telegram/Discord bot tokens, etc.
- **Daemon**: background install (launchd/systemd; WSL2 uses systemd) - **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`. - **Gateway token**: the wizard generates one by default (even on loopback) and stores it in `gateway.auth.token`.
Wizard doc: [Wizard](/start/wizard) Wizard doc: [Wizard](/start/wizard)
@@ -110,11 +110,8 @@ clawdbot gateway --port 18789 --verbose
Dashboard (local loopback): `http://127.0.0.1:18789/` 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`). 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 ⚠️ **Bun warning (WhatsApp + Telegram):** Bun has known issues with these
path that is currently incompatible with Bun and can cause memory corruption on providers. If you use WhatsApp or Telegram, run the Gateway with **Node**.
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
## 4) Pair + connect your first chat surface ## 4) Pair + connect your first chat surface
+1 -1
View File
@@ -27,7 +27,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Docker](/install/docker) - [Docker](/install/docker)
- [Nix](/install/nix) - [Nix](/install/nix)
- [Updating / rollback](/install/updating) - [Updating / rollback](/install/updating)
- [Bun workflow](/install/bun) - [Bun workflow (experimental)](/install/bun)
## Core concepts ## Core concepts
+1 -1
View File
@@ -64,7 +64,7 @@ Onboarding requests TCC permissions needed for:
## 5) CLI (optional) ## 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. workflows and launchd tasks work out of the box.
## 6) Onboarding chat (dedicated session) ## 6) Onboarding chat (dedicated session)
+2 -2
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. Real projects from the community. See what people are building with Clawdbot.
<Info> <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> </Info>
## 🎥 Clawdbot in Action ## 🎥 Clawdbot in Action
@@ -366,7 +366,7 @@ Have something to share? We'd love to feature it!
<Steps> <Steps>
<Step title="Share It"> <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>
<Step title="Include Details"> <Step title="Include Details">
Tell us what it does, link to the repo/demo, share a screenshot if you have one Tell us what it does, link to the repo/demo, share a screenshot if you have one
+2 -2
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 - 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. - 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. - 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** 7) **Health check**
- Starts the Gateway (if needed) and runs `clawdbot health`. - 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)** 8) **Skills (recommended)**
- Reads the available skills and checks requirements. - 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). - Installs optional dependencies (some use Homebrew on macOS).
9) **Finish** 9) **Finish**
+1 -1
View File
@@ -38,7 +38,7 @@ clawdbot agent --to +15555550123 --message "Summon reply" --deliver
- `--local`: run locally (requires provider keys in your shell) - `--local`: run locally (requires provider keys in your shell)
- `--deliver`: send the reply to the chosen provider (requires `--to`) - `--deliver`: send the reply to the chosen provider (requires `--to`)
- `--provider`: `whatsapp|telegram|discord|slack|signal|imessage` (default: `whatsapp`) - `--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 - `--verbose <on|off>`: persist verbose level
- `--timeout <seconds>`: override agent timeout - `--timeout <seconds>`: override agent timeout
- `--json`: output structured JSON - `--json`: output structured JSON
-4
View File
@@ -42,10 +42,6 @@ npm i -g clawdhub
pnpm add -g clawdhub pnpm add -g clawdhub
``` ```
```bash
bun add -g clawdhub
```
## How it fits into Clawdbot ## 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. 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.
+1
View File
@@ -7,6 +7,7 @@ read_when:
## What it does ## What it does
- Elevated mode allows the exec tool to run with elevated privileges when the feature is available and the sender is approved. - 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. - **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`. - 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. - Only `on|off` are accepted; anything else returns a hint and does not change state.
+3 -1
View File
@@ -20,7 +20,7 @@ All skills-related configuration lives under `skills` in `~/.clawdbot/clawdbot.j
}, },
install: { install: {
preferBrew: true, preferBrew: true,
nodeManager: "npm" // npm | pnpm | yarn | bun nodeManager: "npm" // npm | pnpm | yarn | bun (Gateway runtime still Node; bun not recommended)
}, },
entries: { entries: {
"nano-banana-pro": { "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). - `load.extraDirs`: additional skill directories to scan (lowest precedence).
- `install.preferBrew`: prefer brew installers when available (default: true). - `install.preferBrew`: prefer brew installers when available (default: true).
- `install.nodeManager`: node installer preference (`npm` | `pnpm` | `yarn` | `bun`, default: npm). - `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. - `entries.<skillKey>`: per-skill overrides.
Per-skill fields: Per-skill fields:
+6 -3
View File
@@ -45,8 +45,9 @@ surface those skills teach.
## ClawdHub (install + sync) ## ClawdHub (install + sync)
ClawdHub is the public skills registry for Clawdbot. Use it to discover, ClawdHub is the public skills registry for Clawdbot. Browse at
install, update, and back up skills. Full guide: [ClawdHub](/tools/clawdhub). https://clawdhub.com. Use it to discover, install, update, and back up skills.
Full guide: [ClawdHub](/tools/clawdhub).
Common flows: Common flows:
@@ -117,6 +118,8 @@ metadata: {"clawdbot":{"emoji":"♊️","requires":{"bins":["gemini"]},"install"
Notes: Notes:
- If multiple installers are listed, the gateway picks a **single** preferred option (brew when available, otherwise node). - 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). - 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. - 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 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? ## Looking for more skills?
Browse [ClawdHub](/tools/clawdhub). Browse https://clawdhub.com.
--- ---
+10 -1
View File
@@ -7,6 +7,7 @@ read_when:
# Slash commands # Slash commands
Commands are handled by the Gateway. Most commands must be sent as a **standalone** message that starts with `/`. 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: There are two related systems:
@@ -26,6 +27,8 @@ They run immediately, are stripped before the model sees the message, and the re
commands: { commands: {
native: "auto", native: "auto",
text: true, text: true,
bash: false,
bashForegroundMs: 2000,
config: false, config: false,
debug: false, debug: false,
restart: 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. - 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"`). - 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. - `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.config` (default `false`) enables `/config` (reads/writes `clawdbot.json`).
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides). - `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
- `commands.useAccessGroups` (default `true`) enforces allowlists/policies for commands. - `commands.useAccessGroups` (default `true`) enforces allowlists/policies for commands.
@@ -60,15 +65,19 @@ Text + native (when enabled):
- `/activation mention|always` (groups only) - `/activation mention|always` (groups only)
- `/send on|off|inherit` (owner-only) - `/send on|off|inherit` (owner-only)
- `/reset` or `/new` - `/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`) - `/verbose on|off` (alias: `/v`)
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only) - `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
- `/elevated on|off` (alias: `/elev`) - `/elevated on|off` (alias: `/elev`)
- `/model <name>` (alias: `/models`; or `/<alias>` from `agents.defaults.models.*.alias`) - `/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) - `/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: Text-only:
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction)) - `/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: Notes:
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`). - Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
+2 -1
View File
@@ -7,11 +7,12 @@ read_when:
## What it does ## What it does
- Inline directive in any inbound body: `/t <level>`, `/think:<level>`, or `/thinking <level>`. - 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” - minimal → “think”
- low → “think hard” - low → “think hard”
- medium → “think harder” - medium → “think harder”
- high → “ultrathink” (max budget) - high → “ultrathink” (max budget)
- xhigh → “ultrathink+” (GPT-5.2 + Codex models only)
- `highest`, `max` map to `high`. - `highest`, `max` map to `high`.
## Resolution order ## Resolution order
+4 -1
View File
@@ -50,7 +50,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
- `/agent <id>` (or `/agents`) - `/agent <id>` (or `/agents`)
- `/session <key>` (or `/sessions`) - `/session <key>` (or `/sessions`)
- `/model <provider/model>` (or `/model list`, `/models`) - `/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>` - `/verbose <on|off>`
- `/reasoning <on|off|stream>` (stream = Telegram draft only) - `/reasoning <on|off|stream>` (stream = Telegram draft only)
- `/cost <on|off>` - `/cost <on|off>`
@@ -67,3 +67,6 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
## Notes ## Notes
- The TUI shows Gateway chat deltas (`event: chat`) and agent tool events. - 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. - 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.
+5 -5
View File
@@ -132,10 +132,10 @@
"@grammyjs/runner": "^2.0.3", "@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1", "@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4", "@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "^0.43.0", "@mariozechner/pi-agent-core": "^0.45.3",
"@mariozechner/pi-ai": "^0.43.0", "@mariozechner/pi-ai": "^0.45.3",
"@mariozechner/pi-coding-agent": "^0.43.0", "@mariozechner/pi-coding-agent": "^0.45.3",
"@mariozechner/pi-tui": "^0.43.0", "@mariozechner/pi-tui": "^0.45.3",
"@microsoft/agents-hosting": "^1.1.1", "@microsoft/agents-hosting": "^1.1.1",
"@microsoft/agents-hosting-express": "^1.1.1", "@microsoft/agents-hosting-express": "^1.1.1",
"@microsoft/agents-hosting-extensions-teams": "^1.1.1", "@microsoft/agents-hosting-extensions-teams": "^1.1.1",
@@ -207,7 +207,7 @@
"@sinclair/typebox": "0.34.47" "@sinclair/typebox": "0.34.47"
}, },
"patchedDependencies": { "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": { "vitest": {
@@ -1,8 +1,8 @@
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js 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 --- a/dist/providers/google-gemini-cli.js
+++ b/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 break; // Success, exit retry loop
} }
const errorText = await response.text(); const errorText = await response.text();
@@ -14,41 +14,8 @@ index 12540bb1069087a0d0a2967f792008627b9f79d9..f30b525620e6d8e45146b439ec3733e4
// Check if retryable // Check if retryable
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
// Use server-provided delay or exponential backoff // 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 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 --- a/dist/providers/openai-codex-responses.js
+++ b/dist/providers/openai-codex-responses.js +++ b/dist/providers/openai-codex-responses.js
@@ -517,7 +517,7 @@ function convertTools(tools) { @@ -517,7 +517,7 @@ function convertTools(tools) {
@@ -61,10 +28,10 @@ index ad0a2aabbe10382cee4e463b68a02864dd235e57..8c001acfd0b4e0743181c246f1bedcf8
} }
function mapStopReason(status) { function mapStopReason(status) {
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js 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 --- a/dist/providers/openai-responses.js
+++ b/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") { else if (msg.role === "assistant") {
const output = []; const output = [];
@@ -81,7 +48,7 @@ index f07085c64390b211340d6a826b28ea9c2e77302f..7f758532246cc7b062df48e9cec4e6c9
const reasoningItem = JSON.parse(block.thinkingSignature); const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem); output.push(reasoningItem);
} }
@@ -434,6 +440,16 @@ function convertMessages(model, context) { @@ -438,6 +444,16 @@ function convertMessages(model, context) {
}); });
} }
} }
+1258 -315
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -10,4 +10,4 @@ onlyBuiltDependencies:
- sharp - sharp
patchedDependencies: 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
+2
View File
@@ -4,6 +4,8 @@ RUN corepack enable
WORKDIR /app WORKDIR /app
ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts ./
COPY patches ./patches COPY patches ./patches
COPY src ./src COPY src ./src
@@ -20,6 +20,7 @@ docker run --rm -t \
--entrypoint bash \ --entrypoint bash \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e HOME=/home/node \ -e HOME=/home/node \
-e NODE_OPTIONS=--disable-warning=ExperimentalWarning \
-e CLAWDBOT_LIVE_TEST=1 \ -e CLAWDBOT_LIVE_TEST=1 \
-e CLAWDBOT_LIVE_GATEWAY_MODELS="${CLAWDBOT_LIVE_GATEWAY_MODELS:-all}" \ -e CLAWDBOT_LIVE_GATEWAY_MODELS="${CLAWDBOT_LIVE_GATEWAY_MODELS:-all}" \
-e CLAWDBOT_LIVE_GATEWAY_PROVIDERS="${CLAWDBOT_LIVE_GATEWAY_PROVIDERS:-}" \ -e CLAWDBOT_LIVE_GATEWAY_PROVIDERS="${CLAWDBOT_LIVE_GATEWAY_PROVIDERS:-}" \
+1
View File
@@ -20,6 +20,7 @@ docker run --rm -t \
--entrypoint bash \ --entrypoint bash \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e HOME=/home/node \ -e HOME=/home/node \
-e NODE_OPTIONS=--disable-warning=ExperimentalWarning \
-e CLAWDBOT_LIVE_TEST=1 \ -e CLAWDBOT_LIVE_TEST=1 \
-e CLAWDBOT_LIVE_MODELS="${CLAWDBOT_LIVE_MODELS:-all}" \ -e CLAWDBOT_LIVE_MODELS="${CLAWDBOT_LIVE_MODELS:-all}" \
-e CLAWDBOT_LIVE_PROVIDERS="${CLAWDBOT_LIVE_PROVIDERS:-}" \ -e CLAWDBOT_LIVE_PROVIDERS="${CLAWDBOT_LIVE_PROVIDERS:-}" \
-9
View File
@@ -155,12 +155,3 @@ export function resolveAgentDir(cfg: ClawdbotConfig, agentId: string) {
const root = resolveStateDir(process.env, os.homedir); const root = resolveStateDir(process.env, os.homedir);
return path.join(root, "agents", id, "agent"); 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");
}
+128
View File
@@ -59,6 +59,134 @@ describe("resolveAuthProfileOrder", () => {
expect(order).toContain("anthropic:default"); 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", () => { it("does not prioritize lastGood over round-robin ordering", () => {
const order = resolveAuthProfileOrder({ const order = resolveAuthProfileOrder({
cfg, cfg,
+31 -2
View File
@@ -1042,6 +1042,7 @@ export function resolveAuthProfileOrder(params: {
}): string[] { }): string[] {
const { cfg, store, provider, preferredProfile } = params; const { cfg, store, provider, preferredProfile } = params;
const providerKey = normalizeProviderId(provider); const providerKey = normalizeProviderId(provider);
const now = Date.now();
const storedOrder = (() => { const storedOrder = (() => {
const order = store.order; const order = store.order;
if (!order) return undefined; if (!order) return undefined;
@@ -1076,7 +1077,36 @@ export function resolveAuthProfileOrder(params: {
const filtered = baseOrder.filter((profileId) => { const filtered = baseOrder.filter((profileId) => {
const cred = store.profiles[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[] = []; const deduped: string[] = [];
for (const entry of filtered) { for (const entry of filtered) {
@@ -1089,7 +1119,6 @@ export function resolveAuthProfileOrder(params: {
if (explicitOrder && explicitOrder.length > 0) { if (explicitOrder && explicitOrder.length > 0) {
// ...but still respect cooldown tracking to avoid repeatedly selecting a // ...but still respect cooldown tracking to avoid repeatedly selecting a
// known-bad/rate-limited key as the first candidate. // known-bad/rate-limited key as the first candidate.
const now = Date.now();
const available: string[] = []; const available: string[] = [];
const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = []; const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = [];
+7 -1
View File
@@ -7,7 +7,13 @@ export type ModelRef = {
model: string; model: string;
}; };
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high"; export type ThinkLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh";
export type ModelAliasIndex = { export type ModelAliasIndex = {
byAlias: Map<string, { alias: string; ref: ModelRef }>; byAlias: Map<string, { alias: string; ref: ModelRef }>;
+55 -20
View File
@@ -253,19 +253,17 @@ describeLive("live models (profile keys)", () => {
parameters: Type.Object({}, { additionalProperties: false }), 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, model,
{ { messages: [firstUser], tools: [noopTool] },
messages: [
{
role: "user",
content:
"Call the tool `noop` with {}. Do not write any other text.",
timestamp: Date.now(),
},
],
tools: [noopTool],
},
{ {
apiKey, apiKey,
reasoning: resolveTestReasoning(model), reasoning: resolveTestReasoning(model),
@@ -274,8 +272,49 @@ describeLive("live models (profile keys)", () => {
perModelTimeoutMs, 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(toolCall).toBeTruthy();
expect(firstText.length).toBe(0);
if (!toolCall || toolCall.type !== "toolCall") { if (!toolCall || toolCall.type !== "toolCall") {
throw new Error("expected tool call"); throw new Error("expected tool call");
} }
@@ -284,12 +323,7 @@ describeLive("live models (profile keys)", () => {
model, model,
{ {
messages: [ messages: [
{ firstUser,
role: "user",
content:
"Call the tool `noop` with {}. Do not write any other text.",
timestamp: Date.now(),
},
first, first,
{ {
role: "toolResult", role: "toolResult",
@@ -309,7 +343,8 @@ describeLive("live models (profile keys)", () => {
{ {
apiKey, apiKey,
reasoning: resolveTestReasoning(model), reasoning: resolveTestReasoning(model),
maxTokens: 64, // Headroom: reasoning summary can consume most of the output budget.
maxTokens: 256,
}, },
perModelTimeoutMs, perModelTimeoutMs,
); );
+14
View File
@@ -209,6 +209,11 @@ describe("classifyFailoverReason", () => {
expect(classifyFailoverReason("resource has been exhausted")).toBe( expect(classifyFailoverReason("resource has been exhausted")).toBe(
"rate_limit", "rate_limit",
); );
expect(
classifyFailoverReason(
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
),
).toBe("rate_limit");
expect(classifyFailoverReason("invalid request format")).toBe("format"); expect(classifyFailoverReason("invalid request format")).toBe("format");
expect(classifyFailoverReason("credit balance too low")).toBe("billing"); expect(classifyFailoverReason("credit balance too low")).toBe("billing");
expect(classifyFailoverReason("deadline exceeded")).toBe("timeout"); expect(classifyFailoverReason("deadline exceeded")).toBe("timeout");
@@ -265,6 +270,15 @@ describe("formatAssistantErrorText", () => {
"Message ordering conflict", "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", () => { describe("sanitizeToolCallId", () => {
+16 -2
View File
@@ -370,8 +370,8 @@ export function formatAssistantErrorText(
// Check for context overflow (413) errors // Check for context overflow (413) errors
if (isContextOverflowError(raw)) { if (isContextOverflowError(raw)) {
return ( return (
"Context overflow: the conversation history is too large. " + "Context overflow: prompt too large for the model. " +
"Use /new or /reset to start a fresh session." "Try again with less input or a larger-context model."
); );
} }
@@ -392,6 +392,11 @@ export function formatAssistantErrorText(
return `LLM request rejected: ${invalidRequest[1]}`; 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. // Keep it short for WhatsApp.
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw; return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
} }
@@ -414,6 +419,10 @@ const ERROR_PATTERNS = {
"resource_exhausted", "resource_exhausted",
"usage limit", "usage limit",
], ],
overloaded: [
/overloaded_error|"type"\s*:\s*"overloaded_error"/i,
"overloaded",
],
timeout: [ timeout: [
"timeout", "timeout",
"timed out", "timed out",
@@ -496,6 +505,10 @@ export function isAuthErrorMessage(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.auth); return matchesErrorPatterns(raw, ERROR_PATTERNS.auth);
} }
export function isOverloadedErrorMessage(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.overloaded);
}
export function isCloudCodeAssistFormatError(raw: string): boolean { export function isCloudCodeAssistFormatError(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.format); return matchesErrorPatterns(raw, ERROR_PATTERNS.format);
} }
@@ -517,6 +530,7 @@ export type FailoverReason =
export function classifyFailoverReason(raw: string): FailoverReason | null { export function classifyFailoverReason(raw: string): FailoverReason | null {
if (isRateLimitErrorMessage(raw)) return "rate_limit"; 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 (isCloudCodeAssistFormatError(raw)) return "format";
if (isBillingErrorMessage(raw)) return "billing"; if (isBillingErrorMessage(raw)) return "billing";
if (isTimeoutErrorMessage(raw)) return "timeout"; if (isTimeoutErrorMessage(raw)) return "timeout";
+11 -3
View File
@@ -362,6 +362,10 @@ export type EmbeddedPiRunMeta = {
durationMs: number; durationMs: number;
agentMeta?: EmbeddedPiAgentMeta; agentMeta?: EmbeddedPiAgentMeta;
aborted?: boolean; aborted?: boolean;
error?: {
kind: "context_overflow" | "compaction_failure";
message: string;
};
}; };
function buildModelAliasLines(cfg?: ClawdbotConfig) { function buildModelAliasLines(cfg?: ClawdbotConfig) {
@@ -1046,7 +1050,7 @@ export function resolveEmbeddedSessionLane(key: string) {
} }
function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { 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"; if (!level) return "off";
return level; return level;
} }
@@ -1976,12 +1980,15 @@ export async function runEmbeddedPiAgent(params: {
if (promptError && !aborted) { if (promptError && !aborted) {
const errorText = describeUnknownError(promptError); const errorText = describeUnknownError(promptError);
if (isContextOverflowError(errorText)) { if (isContextOverflowError(errorText)) {
const kind = isCompactionFailureError(errorText)
? "compaction_failure"
: "context_overflow";
return { return {
payloads: [ payloads: [
{ {
text: text:
"Context overflow: the conversation history is too large for the model. " + "Context overflow: prompt too large for the model. " +
"Use /new or /reset to start a fresh session, or try a model with a larger context window.", "Try again with less input or a larger-context model.",
isError: true, isError: true,
}, },
], ],
@@ -1992,6 +1999,7 @@ export async function runEmbeddedPiAgent(params: {
provider, provider,
model: model.id, model: model.id,
}, },
error: { kind, message: errorText },
}, },
}; };
} }
+8
View File
@@ -251,6 +251,13 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
textAlias: "/queue", textAlias: "/queue",
acceptsArgs: true, acceptsArgs: true,
}), }),
defineChatCommand({
key: "bash",
description: "Run host shell commands (host-only).",
textAlias: "/bash",
scope: "text",
acceptsArgs: true,
}),
]; ];
registerAlias(commands, "status", "/usage"); registerAlias(commands, "status", "/usage");
@@ -314,6 +321,7 @@ export function isCommandEnabled(
): boolean { ): boolean {
if (commandKey === "config") return cfg.commands?.config === true; if (commandKey === "config") return cfg.commands?.config === true;
if (commandKey === "debug") return cfg.commands?.debug === true; if (commandKey === "debug") return cfg.commands?.debug === true;
if (commandKey === "bash") return cfg.commands?.bash === true;
return true; return true;
} }
+91
View File
@@ -69,6 +69,97 @@ describe("directive behavior", () => {
vi.restoreAllMocks(); 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 () => { it("keeps reserved command aliases from matching after trimming", async () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(runEmbeddedPiAgent).mockReset();
+1 -1
View File
@@ -1456,7 +1456,7 @@ describe("trigger handling", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe( 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(); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
}); });
+47 -2
View File
@@ -29,7 +29,10 @@ import {
type ClawdbotConfig, type ClawdbotConfig,
loadConfig, loadConfig,
} from "../config/config.js"; } from "../config/config.js";
import { resolveSessionFilePath } from "../config/sessions.js"; import {
resolveSessionFilePath,
saveSessionStore,
} from "../config/sessions.js";
import { logVerbose } from "../globals.js"; import { logVerbose } from "../globals.js";
import { clearCommandLane, getQueueSize } from "../process/command-queue.js"; import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
import { getProviderDock } from "../providers/dock.js"; import { getProviderDock } from "../providers/dock.js";
@@ -94,8 +97,10 @@ import {
import type { MsgContext, TemplateContext } from "./templating.js"; import type { MsgContext, TemplateContext } from "./templating.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
formatXHighModelHint,
normalizeThinkLevel, normalizeThinkLevel,
type ReasoningLevel, type ReasoningLevel,
supportsXHighThinking,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "./thinking.js"; } from "./thinking.js";
@@ -976,6 +981,11 @@ export async function getReplyFromConfig(
command: inlineCommandContext, command: inlineCommandContext,
agentId, agentId,
directives, directives,
elevated: {
enabled: elevatedEnabled,
allowed: elevatedAllowed,
failures: elevatedFailures,
},
sessionEntry, sessionEntry,
sessionStore, sessionStore,
sessionKey, sessionKey,
@@ -1033,6 +1043,11 @@ export async function getReplyFromConfig(
command, command,
agentId, agentId,
directives, directives,
elevated: {
enabled: elevatedEnabled,
allowed: elevatedAllowed,
failures: elevatedFailures,
},
sessionEntry, sessionEntry,
sessionStore, sessionStore,
sessionKey, sessionKey,
@@ -1187,7 +1202,10 @@ export async function getReplyFromConfig(
if (!resolvedThinkLevel && prefixedCommandBody) { if (!resolvedThinkLevel && prefixedCommandBody) {
const parts = prefixedCommandBody.split(/\s+/); const parts = prefixedCommandBody.split(/\s+/);
const maybeLevel = normalizeThinkLevel(parts[0]); const maybeLevel = normalizeThinkLevel(parts[0]);
if (maybeLevel) { if (
maybeLevel &&
(maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))
) {
resolvedThinkLevel = maybeLevel; resolvedThinkLevel = maybeLevel;
prefixedCommandBody = parts.slice(1).join(" ").trim(); prefixedCommandBody = parts.slice(1).join(" ").trim();
} }
@@ -1195,6 +1213,33 @@ export async function getReplyFromConfig(
if (!resolvedThinkLevel) { if (!resolvedThinkLevel) {
resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel(); 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 sessionIdFinal = sessionId ?? crypto.randomUUID();
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry); const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
const queueBodyBase = transcribedText const queueBodyBase = transcribedText
@@ -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 () => { it("still replies even if session reset fails to persist", async () => {
const prevStateDir = process.env.CLAWDBOT_STATE_DIR; const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
const stateDir = await fs.mkdtemp( const stateDir = await fs.mkdtemp(
+15 -1
View File
@@ -834,6 +834,20 @@ export async function runReplyAgent(params: {
runResult = fallbackResult.result; runResult = fallbackResult.result;
fallbackProvider = fallbackResult.provider; fallbackProvider = fallbackResult.provider;
fallbackModel = fallbackResult.model; 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; break;
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(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}`); defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
return finalizeWithFollowup({ return finalizeWithFollowup({
text: isContextOverflow 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.`, : `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`,
}); });
} }
+416
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;
}
+59
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
import { resetBashChatCommandForTests } from "./bash-command.js";
import { buildCommandContext, handleCommands } from "./commands.js"; import { buildCommandContext, handleCommands } from "./commands.js";
import { parseInlineDirectives } from "./directive-handling.js"; import { parseInlineDirectives } from "./directive-handling.js";
@@ -33,6 +34,7 @@ function buildParams(
cfg, cfg,
command, command,
directives: parseInlineDirectives(commandBody), directives: parseInlineDirectives(commandBody),
elevated: { enabled: true, allowed: true, failures: [] },
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
workspaceDir: "/tmp", workspaceDir: "/tmp",
defaultGroupActivation: () => "mention", defaultGroupActivation: () => "mention",
@@ -47,6 +49,37 @@ function buildParams(
} }
describe("handleCommands gating", () => { 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 () => { it("blocks /config when disabled", async () => {
const cfg = { const cfg = {
commands: { config: false, debug: false, text: true }, 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", () => { describe("handleCommands identity", () => {
it("returns sender details for /whoami", async () => { it("returns sender details for /whoami", async () => {
const cfg = { const cfg = {
+31
View File
@@ -83,6 +83,7 @@ import type {
} from "../thinking.js"; } from "../thinking.js";
import type { ReplyPayload } from "../types.js"; import type { ReplyPayload } from "../types.js";
import { isAbortTrigger, setAbortMemory } from "./abort.js"; import { isAbortTrigger, setAbortMemory } from "./abort.js";
import { handleBashChatCommand } from "./bash-command.js";
import { parseConfigCommand } from "./config-commands.js"; import { parseConfigCommand } from "./config-commands.js";
import { parseDebugCommand } from "./debug-commands.js"; import { parseDebugCommand } from "./debug-commands.js";
import type { InlineDirectives } from "./directive-handling.js"; import type { InlineDirectives } from "./directive-handling.js";
@@ -400,6 +401,11 @@ export async function handleCommands(params: {
command: CommandContext; command: CommandContext;
agentId?: string; agentId?: string;
directives: InlineDirectives; directives: InlineDirectives;
elevated: {
enabled: boolean;
allowed: boolean;
failures: Array<{ gate: string; key: string }>;
};
sessionEntry?: SessionEntry; sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>; sessionStore?: Record<string, SessionEntry>;
sessionKey: string; sessionKey: string;
@@ -425,6 +431,7 @@ export async function handleCommands(params: {
cfg, cfg,
command, command,
directives, directives,
elevated,
sessionEntry, sessionEntry,
sessionStore, sessionStore,
sessionKey, sessionKey,
@@ -465,6 +472,30 @@ export async function handleCommands(params: {
commandSource: ctx.CommandSource, 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 (allowTextCommands && activationCommand.hasCommand) {
if (!isGroup) { if (!isGroup) {
return { return {
+156 -120
View File
@@ -37,6 +37,11 @@ import { applyVerboseOverride } from "../../sessions/level-overrides.js";
import { shortenHomePath } from "../../utils.js"; import { shortenHomePath } from "../../utils.js";
import { extractModelDirective } from "../model.js"; import { extractModelDirective } from "../model.js";
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
import {
formatThinkingLevels,
formatXHighModelHint,
supportsXHighThinking,
} from "../thinking.js";
import type { ReplyPayload } from "../types.js"; import type { ReplyPayload } from "../types.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
@@ -778,6 +783,7 @@ export async function handleDirectiveOnly(params: {
allowedModelCatalog, allowedModelCatalog,
resetModelOverride, resetModelOverride,
provider, provider,
model,
initialModelLabel, initialModelLabel,
formatModelSwitchEvent, formatModelSwitchEvent,
currentThinkLevel, 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 (directives.hasThinkDirective && !directives.thinkLevel) {
// If no argument was provided, show the current level // If no argument was provided, show the current level
if (!directives.rawThinkLevel) { if (!directives.rawThinkLevel) {
@@ -950,12 +1067,12 @@ export async function handleDirectiveOnly(params: {
return { return {
text: withOptions( text: withOptions(
`Current thinking level: ${level}.`, `Current thinking level: ${level}.`,
"off, minimal, low, medium, high", formatThinkingLevels(resolvedProvider, resolvedModel),
), ),
}; };
} }
return { 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) { if (directives.hasVerboseDirective && !directives.verboseLevel) {
@@ -1098,126 +1215,25 @@ export async function handleDirectiveOnly(params: {
return { text: errors.join(" ") }; return { text: errors.join(" ") };
} }
let modelSelection: ModelDirectiveSelection | undefined; if (
let profileOverride: string | undefined; directives.hasThinkDirective &&
if (directives.hasModelDirective && directives.rawModelDirective) { directives.thinkLevel === "xhigh" &&
const raw = directives.rawModelDirective.trim(); !supportsXHighThinking(resolvedProvider, resolvedModel)
if (/^[0-9]+$/.test(raw)) { ) {
const resolvedDefault = resolveConfiguredModelRef({ return {
cfg: params.cfg, text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
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." };
} }
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) { if (sessionEntry && sessionStore && sessionKey) {
const prevElevatedLevel = const prevElevatedLevel =
currentElevatedLevel ?? currentElevatedLevel ??
@@ -1239,6 +1255,9 @@ export async function handleDirectiveOnly(params: {
if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel; if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel;
else sessionEntry.thinkingLevel = directives.thinkLevel; else sessionEntry.thinkingLevel = directives.thinkLevel;
} }
if (shouldDowngradeXHigh) {
sessionEntry.thinkingLevel = "high";
}
if (directives.hasVerboseDirective && directives.verboseLevel) { if (directives.hasVerboseDirective && directives.verboseLevel) {
applyVerboseOverride(sessionEntry, directives.verboseLevel); applyVerboseOverride(sessionEntry, directives.verboseLevel);
} }
@@ -1295,6 +1314,18 @@ export async function handleDirectiveOnly(params: {
if (storePath) { if (storePath) {
await saveSessionStore(storePath, sessionStore); 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) { if (elevatedChanged) {
const nextElevated = (sessionEntry.elevatedLevel ?? const nextElevated = (sessionEntry.elevatedLevel ??
"off") as ElevatedLevel; "off") as ElevatedLevel;
@@ -1345,6 +1376,11 @@ export async function handleDirectiveOnly(params: {
); );
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint()); if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
} }
if (shouldDowngradeXHigh) {
parts.push(
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
);
}
if (modelSelection) { if (modelSelection) {
const label = `${modelSelection.provider}/${modelSelection.model}`; const label = `${modelSelection.provider}/${modelSelection.model}`;
const labelWithAlias = modelSelection.alias const labelWithAlias = modelSelection.alias
+25 -1
View File
@@ -1,10 +1,34 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { normalizeReasoningLevel, normalizeThinkLevel } from "./thinking.js"; import {
listThinkingLevels,
normalizeReasoningLevel,
normalizeThinkLevel,
} from "./thinking.js";
describe("normalizeThinkLevel", () => { describe("normalizeThinkLevel", () => {
it("accepts mid as medium", () => { it("accepts mid as medium", () => {
expect(normalizeThinkLevel("mid")).toBe("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", () => { describe("normalizeReasoningLevel", () => {
+61 -1
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 VerboseLevel = "off" | "on";
export type ElevatedLevel = "off" | "on"; export type ElevatedLevel = "off" | "on";
export type ReasoningLevel = "off" | "on" | "stream"; export type ReasoningLevel = "off" | "on" | "stream";
export type UsageDisplayLevel = "off" | "on"; 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. // Normalize user-provided thinking level strings to the canonical enum.
export function normalizeThinkLevel( export function normalizeThinkLevel(
raw?: string | null, raw?: string | null,
@@ -32,10 +53,49 @@ export function normalizeThinkLevel(
].includes(key) ].includes(key)
) )
return "high"; return "high";
if (["xhigh", "x-high", "x_high"].includes(key)) return "xhigh";
if (["think"].includes(key)) return "minimal"; if (["think"].includes(key)) return "minimal";
return undefined; 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. // Normalize verbose flags used to toggle agent verbosity.
export function normalizeVerboseLevel( export function normalizeVerboseLevel(
raw?: string | null, raw?: string | null,
+37 -2
View File
@@ -30,8 +30,11 @@ import { hasNonzeroUsage } from "../agents/usage.js";
import { ensureAgentWorkspace } from "../agents/workspace.js"; import { ensureAgentWorkspace } from "../agents/workspace.js";
import type { MsgContext } from "../auto-reply/templating.js"; import type { MsgContext } from "../auto-reply/templating.js";
import { import {
formatThinkingLevels,
formatXHighModelHint,
normalizeThinkLevel, normalizeThinkLevel,
normalizeVerboseLevel, normalizeVerboseLevel,
supportsXHighThinking,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "../auto-reply/thinking.js"; } from "../auto-reply/thinking.js";
@@ -216,17 +219,26 @@ export async function agentCommand(
ensureBootstrapFiles: !agentCfg?.skipBootstrap, ensureBootstrapFiles: !agentCfg?.skipBootstrap,
}); });
const workspaceDir = workspace.dir; 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 thinkOverride = normalizeThinkLevel(opts.thinking);
const thinkOnce = normalizeThinkLevel(opts.thinkingOnce); const thinkOnce = normalizeThinkLevel(opts.thinkingOnce);
if (opts.thinking && !thinkOverride) { if (opts.thinking && !thinkOverride) {
throw new Error( 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) { if (opts.thinkingOnce && !thinkOnce) {
throw new Error( 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, 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, { const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, {
agentId: sessionAgentId, agentId: sessionAgentId,
}); });
+11 -28
View File
@@ -18,6 +18,13 @@ vi.mock("../providers/github-copilot-auth.js", () => ({
const noopAsync = async () => {}; const noopAsync = async () => {};
const noop = () => {}; 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", () => { describe("applyAuthChoice", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
@@ -111,13 +118,7 @@ describe("applyAuthChoice", () => {
mode: "api_key", mode: "api_key",
}); });
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(requireAgentDir());
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>; profiles?: Record<string, { key?: string }>;
@@ -170,13 +171,7 @@ describe("applyAuthChoice", () => {
mode: "api_key", mode: "api_key",
}); });
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(requireAgentDir());
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>; profiles?: Record<string, { key?: string }>;
@@ -337,13 +332,7 @@ describe("applyAuthChoice", () => {
"openrouter/auto", "openrouter/auto",
); );
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(requireAgentDir());
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>; profiles?: Record<string, { key?: string }>;
@@ -426,13 +415,7 @@ describe("applyAuthChoice", () => {
mode: "oauth", mode: "oauth",
}); });
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(requireAgentDir());
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
profiles?: Record< profiles?: Record<
+37
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");
}
}
+41 -2
View File
@@ -23,10 +23,49 @@ export async function maybeRepairUiProtocolFreshness(
try { try {
const [schemaStats, uiStats] = await Promise.all([ const [schemaStats, uiStats] = await Promise.all([
fs.stat(schemaPath), fs.stat(schemaPath).catch(() => null),
fs.stat(uiIndexPath), 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) { if (schemaStats.mtime > uiStats.mtime) {
const uiMtimeIso = uiStats.mtime.toISOString(); const uiMtimeIso = uiStats.mtime.toISOString();
// Find changes since the UI build // Find changes since the UI build
+8 -5
View File
@@ -62,6 +62,7 @@ import {
maybeRepairGatewayServiceConfig, maybeRepairGatewayServiceConfig,
maybeScanExtraGatewayServices, maybeScanExtraGatewayServices,
} from "./doctor-gateway-services.js"; } from "./doctor-gateway-services.js";
import { noteSourceInstallIssues } from "./doctor-install.js";
import { import {
maybeMigrateLegacyConfigFile, maybeMigrateLegacyConfigFile,
normalizeLegacyConfigValues, normalizeLegacyConfigValues,
@@ -187,6 +188,12 @@ export async function doctorCommand(
printWizardHeader(runtime); printWizardHeader(runtime);
intro("Clawdbot doctor"); 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 updateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS === "1";
const canOfferUpdate = const canOfferUpdate =
!updateInProgress && !updateInProgress &&
@@ -195,11 +202,6 @@ export async function doctorCommand(
options.repair !== true && options.repair !== true &&
Boolean(process.stdin.isTTY); Boolean(process.stdin.isTTY);
if (canOfferUpdate) { if (canOfferUpdate) {
const root = await resolveClawdbotPackageRoot({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
if (root) { if (root) {
const git = await detectClawdbotGitCheckout(root); const git = await detectClawdbotGitCheckout(root);
if (git === "git") { if (git === "git") {
@@ -250,6 +252,7 @@ export async function doctorCommand(
} }
await maybeRepairUiProtocolFreshness(runtime, prompter); await maybeRepairUiProtocolFreshness(runtime, prompter);
noteSourceInstallIssues(root);
await maybeMigrateLegacyConfigFile(runtime); await maybeMigrateLegacyConfigFile(runtime);
+85 -11
View File
@@ -18,9 +18,18 @@ import {
OPENROUTER_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF,
setMinimaxApiKey,
writeOAuthCredentials, writeOAuthCredentials,
} from "./onboard-auth.js"; } 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", () => { describe("writeOAuthCredentials", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
@@ -50,10 +59,9 @@ describe("writeOAuthCredentials", () => {
delete process.env.CLAWDBOT_OAUTH_DIR; 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-")); tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-oauth-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir; 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.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
@@ -65,14 +73,7 @@ describe("writeOAuthCredentials", () => {
await writeOAuthCredentials("openai-codex", creds); await writeOAuthCredentials("openai-codex", creds);
// Now writes to the multi-agent path: agents/main/agent const authProfilePath = authProfilePathFor(requireAgentDir());
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
profiles?: Record<string, OAuthCredentials & { type?: string }>; profiles?: Record<string, OAuthCredentials & { type?: string }>;
@@ -85,7 +86,80 @@ describe("writeOAuthCredentials", () => {
await expect( await expect(
fs.readFile( 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", "utf8",
), ),
).rejects.toThrow(); ).rejects.toThrow();
+20 -17
View File
@@ -1,5 +1,5 @@
import type { OAuthCredentials } from "@mariozechner/pi-ai"; 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 { upsertAuthProfile } from "../agents/auth-profiles.js";
import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js"; import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
import { import {
@@ -24,6 +24,9 @@ const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
const MOONSHOT_DEFAULT_MAX_TOKENS = 8192; const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`; export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`;
export { SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF }; 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. // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
const MINIMAX_API_COST = { const MINIMAX_API_COST = {
input: 15, input: 15,
@@ -107,7 +110,7 @@ export async function writeOAuthCredentials(
creds: OAuthCredentials, creds: OAuthCredentials,
agentDir?: string, agentDir?: string,
): Promise<void> { ): 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({ upsertAuthProfile({
profileId: `${provider}:${creds.email ?? "default"}`, profileId: `${provider}:${creds.email ?? "default"}`,
credential: { credential: {
@@ -115,12 +118,12 @@ export async function writeOAuthCredentials(
provider, provider,
...creds, ...creds,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setAnthropicApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "anthropic:default", profileId: "anthropic:default",
credential: { credential: {
@@ -128,12 +131,12 @@ export async function setAnthropicApiKey(key: string, agentDir?: string) {
provider: "anthropic", provider: "anthropic",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setGeminiApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "google:default", profileId: "google:default",
credential: { credential: {
@@ -141,12 +144,12 @@ export async function setGeminiApiKey(key: string, agentDir?: string) {
provider: "google", provider: "google",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setMinimaxApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "minimax:default", profileId: "minimax:default",
credential: { credential: {
@@ -154,12 +157,12 @@ export async function setMinimaxApiKey(key: string, agentDir?: string) {
provider: "minimax", provider: "minimax",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setMoonshotApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "moonshot:default", profileId: "moonshot:default",
credential: { credential: {
@@ -167,12 +170,12 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) {
provider: "moonshot", provider: "moonshot",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setSyntheticApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "synthetic:default", profileId: "synthetic:default",
credential: { credential: {
@@ -180,7 +183,7 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) {
provider: "synthetic", provider: "synthetic",
key, 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 const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
export async function setZaiApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "zai:default", profileId: "zai:default",
credential: { credential: {
@@ -196,7 +199,7 @@ export async function setZaiApiKey(key: string, agentDir?: string) {
provider: "zai", provider: "zai",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
@@ -208,7 +211,7 @@ export async function setOpenrouterApiKey(key: string, agentDir?: string) {
provider: "openrouter", provider: "openrouter",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
@@ -714,7 +717,7 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) {
provider: "opencode", provider: "opencode",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
+6
View File
@@ -155,6 +155,8 @@ const FIELD_LABELS: Record<string, string> = {
"agents.defaults.cliBackends": "CLI Backends", "agents.defaults.cliBackends": "CLI Backends",
"commands.native": "Native Commands", "commands.native": "Native Commands",
"commands.text": "Text Commands", "commands.text": "Text Commands",
"commands.bash": "Allow Bash Chat Command",
"commands.bashForegroundMs": "Bash Foreground Window (ms)",
"commands.config": "Allow /config", "commands.config": "Allow /config",
"commands.debug": "Allow /debug", "commands.debug": "Allow /debug",
"commands.restart": "Allow Restart", "commands.restart": "Allow Restart",
@@ -289,6 +291,10 @@ const FIELD_HELP: Record<string, string> = {
"commands.native": "commands.native":
"Register native commands with connectors that support it (Discord/Slack/Telegram).", "Register native commands with connectors that support it (Discord/Slack/Telegram).",
"commands.text": "Allow text command parsing (slash commands only).", "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": "commands.config":
"Allow /config chat command to read/write config on disk (default: false).", "Allow /config chat command to read/write config on disk (default: false).",
"commands.debug": "commands.debug":
+5 -1
View File
@@ -1244,6 +1244,10 @@ export type CommandsConfig = {
native?: NativeCommandsSetting; native?: NativeCommandsSetting;
/** Enable text command parsing (default: true). */ /** Enable text command parsing (default: true). */
text?: boolean; 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). */ /** Allow /config command (default: false). */
config?: boolean; config?: boolean;
/** Allow /debug command (default: false). */ /** Allow /debug command (default: false). */
@@ -1629,7 +1633,7 @@ export type AgentDefaultsConfig = {
/** Vector memory search configuration (per-agent overrides supported). */ /** Vector memory search configuration (per-agent overrides supported). */
memorySearch?: MemorySearchConfig; memorySearch?: MemorySearchConfig;
/** Default thinking level when no /think directive is present. */ /** 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. */ /** Default verbose level when no /verbose directive is present. */
verboseDefault?: "off" | "on"; verboseDefault?: "off" | "on";
/** Default elevated level when no /elevated directive is present. */ /** Default elevated level when no /elevated directive is present. */
+3
View File
@@ -724,6 +724,8 @@ const CommandsSchema = z
.object({ .object({
native: NativeCommandsSettingSchema.optional().default("auto"), native: NativeCommandsSettingSchema.optional().default("auto"),
text: z.boolean().optional(), text: z.boolean().optional(),
bash: z.boolean().optional(),
bashForegroundMs: z.number().int().min(0).max(30_000).optional(),
config: z.boolean().optional(), config: z.boolean().optional(),
debug: z.boolean().optional(), debug: z.boolean().optional(),
restart: z.boolean().optional(), restart: z.boolean().optional(),
@@ -1248,6 +1250,7 @@ const AgentDefaultsSchema = z
z.literal("low"), z.literal("low"),
z.literal("medium"), z.literal("medium"),
z.literal("high"), z.literal("high"),
z.literal("xhigh"),
]) ])
.optional(), .optional(),
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(), verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
+10 -1
View File
@@ -32,7 +32,11 @@ import {
DEFAULT_HEARTBEAT_ACK_MAX_CHARS, DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
stripHeartbeatToken, stripHeartbeatToken,
} from "../auto-reply/heartbeat.js"; } 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 { CliDeps } from "../cli/deps.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { import {
@@ -367,6 +371,11 @@ export async function runCronIsolatedAgentTurn(params: {
catalog: await loadCatalog(), catalog: await loadCatalog(),
}); });
} }
if (thinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) {
throw new Error(
`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
);
}
const timeoutMs = resolveAgentTimeoutMs({ const timeoutMs = resolveAgentTimeoutMs({
cfg: cfgWithAgentDefaults, cfg: cfgWithAgentDefaults,
+28
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from "node:child_process";
import process from "node:process"; import process from "node:process";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js"; import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
@@ -8,6 +9,33 @@ if (process.argv.includes("--no-color")) {
process.env.FORCE_COLOR = "0"; 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); const parsed = parseCliProfileArgs(process.argv);
if (!parsed.ok) { if (!parsed.ok) {
// Keep it simple; Commander will handle rich help/errors after we strip flags. // Keep it simple; Commander will handle rich help/errors after we strip flags.
+31 -1
View File
@@ -8,10 +8,13 @@ import {
} from "../agents/model-selection.js"; } from "../agents/model-selection.js";
import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
import { import {
formatThinkingLevels,
formatXHighModelHint,
normalizeElevatedLevel, normalizeElevatedLevel,
normalizeReasoningLevel, normalizeReasoningLevel,
normalizeThinkLevel, normalizeThinkLevel,
normalizeUsageDisplay, normalizeUsageDisplay,
supportsXHighThinking,
} from "../auto-reply/thinking.js"; } from "../auto-reply/thinking.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { SessionEntry } from "../config/sessions.js"; import type { SessionEntry } from "../config/sessions.js";
@@ -95,8 +98,17 @@ export async function applySessionsPatchToStore(params: {
} else if (raw !== undefined) { } else if (raw !== undefined) {
const normalized = normalizeThinkLevel(String(raw)); const normalized = normalizeThinkLevel(String(raw));
if (!normalized) { 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( return invalid(
"invalid thinkingLevel (use off|minimal|low|medium|high)", `invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`,
); );
} }
if (normalized === "off") delete next.thinkingLevel; 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) { if ("sendPolicy" in patch) {
const raw = patch.sendPolicy; const raw = patch.sendPolicy;
if (raw === null) { if (raw === null) {
+4 -4
View File
@@ -171,7 +171,7 @@ describe("google-shared convertMessages", () => {
{ {
type: "thinking", type: "thinking",
thinking: "hidden", thinking: "hidden",
thinkingSignature: "sig", thinkingSignature: "c2ln",
}, },
], ],
api: "google-generative-ai", api: "google-generative-ai",
@@ -202,7 +202,7 @@ describe("google-shared convertMessages", () => {
expect(contents[0].role).toBe("model"); expect(contents[0].role).toBe("model");
expect(contents[0].parts?.[0]).toMatchObject({ expect(contents[0].parts?.[0]).toMatchObject({
thought: true, thought: true,
thoughtSignature: "sig", thoughtSignature: "c2ln",
}); });
}); });
@@ -216,7 +216,7 @@ describe("google-shared convertMessages", () => {
{ {
type: "thinking", type: "thinking",
thinking: "structured", thinking: "structured",
thinkingSignature: "sig", thinkingSignature: "c2ln",
}, },
], ],
api: "google-generative-ai", api: "google-generative-ai",
@@ -247,7 +247,7 @@ describe("google-shared convertMessages", () => {
expect(parts).toHaveLength(1); expect(parts).toHaveLength(1);
expect(parts[0]).toMatchObject({ expect(parts[0]).toMatchObject({
thought: true, thought: true,
thoughtSignature: "sig", thoughtSignature: "c2ln",
}); });
}); });
+23 -7
View File
@@ -1,6 +1,9 @@
import type { SlashCommand } from "@mariozechner/pi-tui"; 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 VERBOSE_LEVELS = ["on", "off"];
const REASONING_LEVELS = ["on", "off"]; const REASONING_LEVELS = ["on", "off"];
const ELEVATED_LEVELS = ["on", "off"]; const ELEVATED_LEVELS = ["on", "off"];
@@ -12,6 +15,11 @@ export type ParsedCommand = {
args: string; args: string;
}; };
export type SlashCommandOptions = {
provider?: string;
model?: string;
};
const COMMAND_ALIASES: Record<string, string> = { const COMMAND_ALIASES: Record<string, string> = {
elev: "elevated", 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 [ return [
{ name: "help", description: "Show slash command help" }, { name: "help", description: "Show slash command help" },
{ name: "status", description: "Show gateway status summary" }, { name: "status", description: "Show gateway status summary" },
@@ -44,9 +55,9 @@ export function getSlashCommands(): SlashCommand[] {
name: "think", name: "think",
description: "Set thinking level", description: "Set thinking level",
getArgumentCompletions: (prefix) => getArgumentCompletions: (prefix) =>
THINK_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map( thinkLevels
(value) => ({ value, label: value }), .filter((v) => v.startsWith(prefix.toLowerCase()))
), .map((value) => ({ value, label: value })),
}, },
{ {
name: "verbose", 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 [ return [
"Slash commands:", "Slash commands:",
"/help", "/help",
@@ -113,7 +129,7 @@ export function helpText(): string {
"/agent <id> (or /agents)", "/agent <id> (or /agents)",
"/session <key> (or /sessions)", "/session <key> (or /sessions)",
"/model <provider/model> (or /models)", "/model <provider/model> (or /models)",
"/think <off|minimal|low|medium|high>", `/think <${thinkLevels}>`,
"/verbose <on|off>", "/verbose <on|off>",
"/reasoning <on|off>", "/reasoning <on|off>",
"/cost <on|off>", "/cost <on|off>",
+30 -6
View File
@@ -7,7 +7,10 @@ import {
TUI, TUI,
} from "@mariozechner/pi-tui"; } from "@mariozechner/pi-tui";
import { resolveDefaultAgentId } from "../agents/agent-scope.js"; 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 { loadConfig } from "../config/config.js";
import { formatAge } from "../infra/provider-summary.js"; import { formatAge } from "../infra/provider-summary.js";
import { import {
@@ -239,6 +242,18 @@ export async function runTui(opts: TuiOptions) {
root.addChild(footer); root.addChild(footer);
root.addChild(editor); root.addChild(editor);
const updateAutocompleteProvider = () => {
editor.setAutocompleteProvider(
new CombinedAutocompleteProvider(
getSlashCommands({
provider: sessionInfo.modelProvider,
model: sessionInfo.model,
}),
process.cwd(),
),
);
};
const tui = new TUI(new ProcessTerminal()); const tui = new TUI(new ProcessTerminal());
tui.addChild(root); tui.addChild(root);
tui.setFocus(editor); tui.setFocus(editor);
@@ -524,6 +539,7 @@ export async function runTui(opts: TuiOptions) {
} catch (err) { } catch (err) {
chatLog.addSystem(`sessions list failed: ${String(err)}`); chatLog.addSystem(`sessions list failed: ${String(err)}`);
} }
updateAutocompleteProvider();
updateFooter(); updateFooter();
tui.requestRender(); tui.requestRender();
}; };
@@ -861,7 +877,12 @@ export async function runTui(opts: TuiOptions) {
if (!name) return; if (!name) return;
switch (name) { switch (name) {
case "help": case "help":
chatLog.addSystem(helpText()); chatLog.addSystem(
helpText({
provider: sessionInfo.modelProvider,
model: sessionInfo.model,
}),
);
break; break;
case "status": case "status":
try { try {
@@ -921,7 +942,12 @@ export async function runTui(opts: TuiOptions) {
break; break;
case "think": case "think":
if (!args) { if (!args) {
chatLog.addSystem("usage: /think <off|minimal|low|medium|high>"); const levels = formatThinkingLevels(
sessionInfo.modelProvider,
sessionInfo.model,
"|",
);
chatLog.addSystem(`usage: /think <${levels}>`);
break; break;
} }
try { try {
@@ -1071,9 +1097,7 @@ export async function runTui(opts: TuiOptions) {
tui.requestRender(); tui.requestRender();
}; };
editor.setAutocompleteProvider( updateAutocompleteProvider();
new CombinedAutocompleteProvider(getSlashCommands(), process.cwd()),
);
editor.onSubmit = (text) => { editor.onSubmit = (text) => {
const value = text.trim(); const value = text.trim();
editor.setText(""); editor.setText("");
+9 -3
View File
@@ -786,9 +786,15 @@ export async function runOnboardingWizard(
} }
} }
const controlUiAssets = await ensureControlUiAssetsBuilt(runtime); const controlUiEnabled =
if (!controlUiAssets.ok && controlUiAssets.message) { nextConfig.gateway?.controlUi?.enabled ??
runtime.error(controlUiAssets.message); 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( await prompter.note(
+3 -3
View File
@@ -122,9 +122,9 @@ const spawnGatewayInstance = async (name: string): Promise<GatewayInstance> => {
try { try {
child = spawn( child = spawn(
"bun", "node",
[ [
"src/index.ts", "dist/index.js",
"gateway", "gateway",
"--port", "--port",
String(port), String(port),
@@ -222,7 +222,7 @@ const runCliJson = async (
): Promise<unknown> => { ): Promise<unknown> => {
const stdout: string[] = []; const stdout: string[] = [];
const stderr: string[] = []; const stderr: string[] = [];
const child = spawn("bun", ["src/index.ts", ...args], { const child = spawn("node", ["dist/index.js", ...args], {
cwd: process.cwd(), cwd: process.cwd(),
env: { ...process.env, ...env }, env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
+2 -2
View File
@@ -15,10 +15,10 @@
"vite": "7.3.1" "vite": "7.3.1"
}, },
"devDependencies": { "devDependencies": {
"@vitest/browser-playwright": "4.0.16", "@vitest/browser-playwright": "4.0.17",
"playwright": "^1.57.0", "playwright": "^1.57.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "4.0.16" "vitest": "4.0.17"
}, },
"pnpm": { "pnpm": {
"minimumReleaseAge": 2880 "minimumReleaseAge": 2880