Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0a79c19f9 | ||
|
|
cb0f6cefa4 | ||
|
|
25ef01b74a | ||
|
|
6db0201fcd | ||
|
|
40e508823f | ||
|
|
3368284b2a | ||
|
|
ece01d89fe | ||
|
|
9c0c4f50ec | ||
|
|
6729637f61 | ||
|
|
18d22aa426 | ||
|
|
a3641526ab | ||
|
|
f50e06a1b6 | ||
|
|
2ae3b45ac1 | ||
|
|
780a43711f | ||
|
|
d682b604de |
No files matched your search
+119
-39
@@ -1,49 +1,129 @@
|
||||
# Changelog
|
||||
|
||||
## 2025.1.12 (Unreleased)
|
||||
## 2026.1.13-2
|
||||
|
||||
### Highlights
|
||||
- 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.
|
||||
|
||||
### New & Improved
|
||||
- Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI.
|
||||
- Memory: new `clawdbot memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default.
|
||||
- Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config.
|
||||
- Tools/Sandbox: add tool profiles + group shorthands; support tool-policy groups in `tools.sandbox.tools`; drop legacy `memory` shorthand; allow Docker bind mounts via `docker.binds`.
|
||||
- Tools: add browser `scrollintoview` action; allow Claude/Gemini tool param aliases; allow thinking `xhigh` for GPT-5.2/Codex with safe downgrades.
|
||||
- Gateway/CLI: add Tailscale binary discovery, custom bind mode, and probe auth retry; add `clawdbot dashboard` auto-open flow; default native slash commands to `"auto"` with per-provider overrides.
|
||||
- Auth/Onboarding: add Chutes OAuth (PKCE + refresh + onboarding choice); normalize API key inputs; default TUI onboarding to `deliver: false`.
|
||||
- Providers: add `discord.allowBots`; trim legacy MiniMax M2 from default catalogs; route MiniMax vision to the Coding Plan VLM endpoint (also accepts `@/path/to/file.png` inputs).
|
||||
- Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer.
|
||||
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
||||
|
||||
### Fixes
|
||||
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles.
|
||||
- Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors.
|
||||
- MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing.
|
||||
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid incorrect role errors.
|
||||
- Messaging: enforce context isolation for message tool sends; keep typing indicators alive during tool execution.
|
||||
- Auto-reply: `/status` allowlist behavior, reasoning-tag enforcement on fallback, and system-event enqueueing for elevated/reasoning toggles.
|
||||
- Auto-reply: explain how to enable `/bash` when it’s disabled; add security notes + FAQ. (#722) — thanks @vrknetha.
|
||||
- Auto-reply: resolve ambiguous `/model` matches; fix streaming block reply media handling; keep >300 char heartbeat replies instead of dropping.
|
||||
- Discord/Slack: centralize reply-thread planning; fix autoThread routing + add per-channel autoThread; avoid duplicate listeners; keep reasoning italics intact; allow clearing channel parents via message tool.
|
||||
- Telegram: preserve forum topic thread ids, persist polling offsets, respect account bindings in webhook mode, and show typing indicator in General topics.
|
||||
- Slack: accept slash commands with or without leading `/` for custom command configs.
|
||||
- Cron: persist disabled jobs correctly; accept `jobId` aliases for update/run/remove params.
|
||||
- Gateway/CLI: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides; `agents.list` respects explicit config; reduce noisy loopback WS logs during tests; run `clawdbot doctor --non-interactive` during updates.
|
||||
- Onboarding/Control UI: refuse invalid configs (run doctor first); quote Windows browser URLs for OAuth; keep chat scroll position unless the user is near the bottom.
|
||||
- Tools/UI: harden tool input schemas for strict providers; drop null-only union variants for Gemini schema cleanup; treat `maxChars: 0` as unlimited; keep TUI last streamed response instead of "(no output)".
|
||||
- Connections UI: polish multi-account account cards.
|
||||
|
||||
### Maintenance
|
||||
### Changes
|
||||
- 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.13-1
|
||||
|
||||
### Changes
|
||||
- Cron: accept ISO timestamps for one-shot schedules (UTC) and allow optional delete-after-run; wired into CLI + macOS editor.
|
||||
- Gateway: add Tailscale binary discovery, custom bind mode, and probe auth retry for password changes. (#740 — thanks @jeffersonwarrior)
|
||||
- Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow)
|
||||
- Agents: support per-agent model fallbacks via `agents.list[].model`. (#583 — thanks @mitschabaude-bot)
|
||||
- Tools: add tool profiles plus group shorthands for tool policy allow/deny (global, per-agent, sandbox).
|
||||
- Thinking: allow xhigh for GPT-5.2 + Codex models and downgrade on unsupported switches. (#444 — thanks @grp06)
|
||||
|
||||
### 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
|
||||
- Memory: add vector search for agent memories (Markdown-only scope) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback.
|
||||
|
||||
### Changes
|
||||
- Agents: strengthen memory recall guidance (memory_search mandatory for past work/preferences; system prompt injects conditional recall section; memory_get now described as safe snippet fetch).
|
||||
- Browser: add `scrollintoview` action to scroll refs into view before click/type.
|
||||
- Memory: embedding providers support OpenAI or local `node-llama-cpp`; config adds defaults + per-agent overrides, provider/fallback metadata surfaced in tools/CLI.
|
||||
- CLI/Tools: new `clawdbot memory` commands plus `memory_search`/`memory_get` tools returning snippets + line ranges and provider info.
|
||||
- Runtime: memory index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default; inline status replies now stay auth-gated while inline prompts continue to the agent.
|
||||
- Doctor: rebuild Control UI assets when protocol schema is newer to avoid stale UI connect errors. (#786) — thanks @meaningfool.
|
||||
- Cron: allow jobs to target a specific agent and expose agent selection in the macOS app + Control UI.
|
||||
- Discord: add `discord.allowBots` to permit bot-authored messages (still ignores its own messages) with docs warning about bot loops. (#802) — thanks @zknicker.
|
||||
- CLI/Onboarding: `clawdbot dashboard` prints/copies the tokenized Control UI link and opens it; onboarding now auto-opens the dashboard with your token and keeps the link in the summary.
|
||||
- Commands: native slash commands now default to `"auto"` (on for Discord/Telegram, off for Slack) with per-provider overrides (`discord/telegram/slack.commands.native`) and docs updated.
|
||||
- Sandbox: allow Docker bind mounts via `docker.binds`; merges global + per-agent binds (per-agent ignored under shared scope) for custom host paths. (#790 — thanks @akonyer)
|
||||
- Models: add Synthetic provider (Anthropic-compatible) and trim legacy MiniMax M2 from default catalogs. (#811 — thanks @siraht)
|
||||
|
||||
### Fixes
|
||||
- Auto-reply: inline `/status` now honors allowlists (authorized stripped + replied inline; unauthorized leaves text for the agent) to match command gating tests.
|
||||
- Models: normalize `${ENV_VAR}` apiKey config values and auto-fill missing provider `apiKey` from env/auth when custom provider models are configured (fixes MiniMax “Unknown model” on fresh installs).
|
||||
- Models/Tools: include `MiniMax-VL-01` in implicit MiniMax provider so image pairing uses a real vision model.
|
||||
- Telegram: show typing indicator in General forum topics. (#779) — thanks @azade-c.
|
||||
- Discord: keep reasoning italics intact when messages are chunked, so reasoning stays italic across multi-part sends.
|
||||
- Models: keep explicit GitHub Copilot provider config and honor agent-dir auth profiles for auto-injection. (#705) — thanks @TAGOOZ.
|
||||
- Auto-reply: restore 300-char heartbeat ack limit and keep >300 char replies instead of dropping them; adjust long heartbeat test content accordingly.
|
||||
- Gateway: `agents.list` now honors explicit `agents.list` config without pulling stray agents from disk; GitHub Copilot CLI auth path uses the updated provider build.
|
||||
- Google: apply patched pi-ai `google-gemini-cli` function call handling (strips ids) after upgrading to pi-ai 0.43.0.
|
||||
- Tools: allow Gemini/Claude-style aliases (`file_path`, `old_string`, `new_string`) in read/write/edit schemas while still enforcing required params at runtime to avoid validation loops.
|
||||
- Auto-reply: elevated/reasoning toggles now enqueue system events so the model sees the mode change immediately.
|
||||
- Tools: keep `image` available in sandbox and fail over when image models return empty output (fixes “(no text returned)”).
|
||||
- Discord: add per-channel `autoThread` to auto-create threads for top-level messages. (#800) — thanks @davidguttman.
|
||||
- Discord: fix autoThread routing so replies stay in the created thread and avoid reply references. (#807) — thanks @davidguttman.
|
||||
- Onboarding: TUI defaults to `deliver: false` to avoid cross-provider auto-delivery leaks; onboarding spawns the TUI with explicit `deliver: false`. (#791 — thanks @roshanasingh4)
|
||||
|
||||
## 2026.1.11
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -746,10 +746,8 @@ Controls how chat commands are enabled across connectors.
|
||||
```json5
|
||||
{
|
||||
commands: {
|
||||
native: "auto", // register native commands when supported (auto)
|
||||
native: false, // register native commands when supported
|
||||
text: true, // parse slash commands in chat messages
|
||||
bash: false, // allow ! (alias: /bash) (host-only; requires tools.elevated allowlists)
|
||||
bashForegroundMs: 2000, // bash foreground window (0 backgrounds immediately)
|
||||
config: false, // allow /config (writes to disk)
|
||||
debug: false, // allow /debug (runtime-only overrides)
|
||||
restart: false, // allow /restart + gateway restart tool
|
||||
@@ -763,8 +761,6 @@ Notes:
|
||||
- `commands.text: false` disables parsing chat messages for commands.
|
||||
- `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported providers stay text-only.
|
||||
- Set `commands.native: true|false` to force all, or override per provider with `discord.commands.native`, `telegram.commands.native`, `slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app.
|
||||
- `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<provider>`.
|
||||
- `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! <cmd>` requests are rejected (one at a time).
|
||||
- `commands.config: true` enables `/config` (reads/writes `clawdbot.json`).
|
||||
- `commands.debug: true` enables `/debug` (runtime-only overrides).
|
||||
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
|
||||
|
||||
+80
-123
@@ -7,106 +7,87 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
|
||||
## 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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [What is Clawdbot, in one paragraph?](#what-is-clawdbot-in-one-paragraph)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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 does Clawdbot store its data?](#where-does-clawdbot-store-its-data)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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 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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent)
|
||||
- [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)
|
||||
- [How do I stop/cancel a running task?](#how-do-i-stopcancel-a-running-task)
|
||||
- [Why does `/bash` say it’s disabled?](#why-does-bash-say-its-disabled)
|
||||
- [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)
|
||||
- [“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)
|
||||
|
||||
## First 60 seconds if something's broken
|
||||
|
||||
@@ -163,7 +144,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
|
||||
Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the always‑on control plane; the assistant is the product.
|
||||
|
||||
## Quick start and first-run setup
|
||||
## Quick start and first‑run setup
|
||||
|
||||
### What’s the recommended way to install and set up Clawdbot?
|
||||
|
||||
@@ -299,8 +280,6 @@ 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).
|
||||
|
||||
## Skills and automation
|
||||
|
||||
### How do I customize skills without keeping the repo dirty?
|
||||
|
||||
Use managed overrides instead of editing the repo copy. Put your changes in `~/.clawdbot/skills/<name>/SKILL.md` (or add a folder via `skills.load.extraDirs` in `~/.clawdbot/clawdbot.json`). Precedence is `<workspace>/skills` > `~/.clawdbot/skills` > bundled, so managed overrides win without touching git. Only upstream-worthy edits should live in the repo and go out as PRs.
|
||||
@@ -345,8 +324,6 @@ clawdhub update --all
|
||||
|
||||
ClawdHub installs into `./skills` under your current directory (or falls back to your configured Clawdbot workspace); Clawdbot treats that as `<workspace>/skills` on the next session. For shared skills across agents, place them in `~/.clawdbot/skills/<name>/SKILL.md`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills) and [ClawdHub](/tools/clawdhub).
|
||||
|
||||
## Sandboxing and memory
|
||||
|
||||
### Is there a dedicated sandboxing doc?
|
||||
|
||||
Yes. See [Sandboxing](/gateway/sandboxing). For Docker-specific setup (full gateway in Docker or sandbox images), see [Docker](/install/docker).
|
||||
@@ -1064,26 +1041,6 @@ Slash commands overview: see [Slash commands](/tools/slash-commands).
|
||||
|
||||
Most commands must be sent as a **standalone** message that starts with `/`, but a few shortcuts (like `/status`) also work inline for allowlisted senders.
|
||||
|
||||
### Why does `/bash` say it’s disabled?
|
||||
|
||||
The host shell command (`! <cmd>` or `/bash <cmd>`) is **disabled by default** because it runs directly on the gateway host. To enable it, update your config and restart the Gateway:
|
||||
|
||||
```json5
|
||||
{
|
||||
commands: { bash: true },
|
||||
tools: {
|
||||
elevated: {
|
||||
enabled: true,
|
||||
allowFrom: {
|
||||
"<provider>": ["<sender-id>"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep the allowlist tight and avoid enabling it in public rooms. See [Slash commands](/tools/slash-commands) and [Security](/gateway/security).
|
||||
|
||||
### Why does it feel like the bot “ignores” rapid‑fire messages?
|
||||
|
||||
Queue mode controls how new messages interact with an in‑flight run. Use `/queue` to change modes:
|
||||
|
||||
@@ -7,13 +7,10 @@ read_when:
|
||||
|
||||
## What it does
|
||||
- Elevated mode allows the exec tool to run with elevated privileges when the feature is available and the sender is approved.
|
||||
- The bash chat command (`!`; `/bash` alias) uses the same `tools.elevated` allowlists because it always runs on the host.
|
||||
- **Optional for sandboxed agents**: elevated only changes behavior when the agent is running in a sandbox. If the agent already runs unsandboxed, elevated is effectively a no-op.
|
||||
- Directive forms: `/elevated on`, `/elevated off`, `/elev on`, `/elev off`.
|
||||
- Only `on|off` are accepted; anything else returns a hint and does not change state.
|
||||
|
||||
Security note: enabling elevated access or `/bash` effectively grants host shell access. Keep allowlists tight and avoid enabling it in public rooms.
|
||||
|
||||
## What it controls (and what it doesn’t)
|
||||
- **Availability gates**: `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can further restrict elevated per agent (both must allow).
|
||||
- **Per-session state**: `/elevated on|off` sets the elevated level for the current session key.
|
||||
|
||||
@@ -7,7 +7,6 @@ read_when:
|
||||
# Slash commands
|
||||
|
||||
Commands are handled by the Gateway. Most commands must be sent as a **standalone** message that starts with `/`.
|
||||
The host-only bash chat command uses `! <cmd>` (with `/bash <cmd>` as an alias).
|
||||
|
||||
There are two related systems:
|
||||
|
||||
@@ -27,8 +26,6 @@ They run immediately, are stripped before the model sees the message, and the re
|
||||
commands: {
|
||||
native: "auto",
|
||||
text: true,
|
||||
bash: false,
|
||||
bashForegroundMs: 2000,
|
||||
config: false,
|
||||
debug: false,
|
||||
restart: false,
|
||||
@@ -43,9 +40,6 @@ They run immediately, are stripped before the model sees the message, and the re
|
||||
- Auto: on for Discord/Telegram; off for Slack (until you add slash commands); ignored for providers without native support.
|
||||
- Set `discord.commands.native`, `telegram.commands.native`, or `slack.commands.native` to override per provider (bool or `"auto"`).
|
||||
- `false` clears previously registered commands on Discord/Telegram at startup. Slack commands are managed in the Slack app and are not removed automatically.
|
||||
- `commands.bash` (default `false`) enables `! <cmd>` to run host shell commands (`/bash <cmd>` is an alias; requires `tools.elevated` allowlists).
|
||||
- When `commands.bash` is `false`, `/bash` replies with a short enablement hint (config keys + allowlist).
|
||||
- `commands.bashForegroundMs` (default `2000`) controls how long bash waits before switching to background mode (`0` backgrounds immediately).
|
||||
- `commands.config` (default `false`) enables `/config` (reads/writes `clawdbot.json`).
|
||||
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
|
||||
- `commands.useAccessGroups` (default `true`) enforces allowlists/policies for commands.
|
||||
@@ -72,13 +66,9 @@ Text + native (when enabled):
|
||||
- `/elevated on|off` (alias: `/elev`)
|
||||
- `/model <name>` (alias: `/models`; or `/<alias>` from `agents.defaults.models.*.alias`)
|
||||
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
|
||||
- `/bash <command>` (host-only; alias for `! <command>`; requires `commands.bash: true` + `tools.elevated` allowlists)
|
||||
|
||||
Text-only:
|
||||
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction))
|
||||
- `! <command>` (host-only; one at a time; use `!poll` + `!stop` for long-running jobs)
|
||||
- `!poll` (check output / status; accepts optional `sessionId`; `/bash poll` also works)
|
||||
- `!stop` (stop the running bash job; accepts optional `sessionId`; `/bash stop` also works)
|
||||
|
||||
Notes:
|
||||
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
||||
@@ -91,10 +81,6 @@ Notes:
|
||||
- **Inline shortcuts (allowlisted senders only):** `/help`, `/commands`, `/status` (`/usage`), `/whoami` (`/id`) also work when embedded in text.
|
||||
- Unauthorized command-only messages are silently ignored, and inline `/...` tokens are treated as plain text.
|
||||
|
||||
## Security note: `!` / `/bash`
|
||||
|
||||
`! <cmd>` and `/bash <cmd>` run **directly on the gateway host** as the gateway user. Keep this disabled unless you fully trust the sender and have locked down allowlists. Treat it like giving someone SSH access to the host.
|
||||
|
||||
## Usage vs cost (what shows where)
|
||||
|
||||
- **Provider usage/quota** (example: “Claude 80% left”) shows up in `/status` when provider usage tracking is enabled.
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("resolveAgentConfig", () => {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4",
|
||||
fallbacks: ["openai/gpt-4.1"],
|
||||
fallbacks: ["openai/gpt-5-nano"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
|
||||
@@ -8,7 +8,7 @@ function makeCfg(overrides: Partial<ClawdbotConfig> = {}): ClawdbotConfig {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
primary: "openai/gpt-5-nano",
|
||||
fallbacks: ["anthropic/claude-haiku-3-5"],
|
||||
},
|
||||
},
|
||||
@@ -29,7 +29,7 @@ describe("runWithModelFallback", () => {
|
||||
runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
}),
|
||||
).rejects.toThrow("bad request");
|
||||
@@ -46,7 +46,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -92,7 +92,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("runWithModelFallback", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
primary: "openai/gpt-5-nano",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -173,13 +173,13 @@ describe("runWithModelFallback", () => {
|
||||
cfg,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
fallbacksOverride: ["openai/gpt-4.1"],
|
||||
fallbacksOverride: ["openai/gpt-5-nano"],
|
||||
run: async (provider, model) => {
|
||||
calls.push({ provider, model });
|
||||
if (provider === "anthropic") {
|
||||
throw Object.assign(new Error("nope"), { status: 401 });
|
||||
}
|
||||
if (provider === "openai" && model === "gpt-4.1") {
|
||||
if (provider === "openai" && model === "gpt-5-nano") {
|
||||
return "ok";
|
||||
}
|
||||
throw new Error(`unexpected candidate: ${provider}/${model}`);
|
||||
@@ -189,7 +189,7 @@ describe("runWithModelFallback", () => {
|
||||
expect(res.result).toBe("ok");
|
||||
expect(calls).toEqual([
|
||||
{ provider: "anthropic", model: "claude-opus-4-5" },
|
||||
{ provider: "openai", model: "gpt-4.1" },
|
||||
{ provider: "openai", model: "gpt-5-nano" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -234,7 +234,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -254,7 +254,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -269,7 +269,7 @@ describe("runWithModelFallback", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
primary: "openai/gpt-5-nano",
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
@@ -292,6 +292,6 @@ describe("runWithModelFallback", () => {
|
||||
expect(result.result).toBe("ok");
|
||||
expect(run).toHaveBeenCalledTimes(2);
|
||||
expect(result.provider).toBe("openai");
|
||||
expect(result.model).toBe("gpt-4.1-mini");
|
||||
expect(result.model).toBe("gpt-5-nano");
|
||||
});
|
||||
});
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
const catalog = [
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4",
|
||||
name: "GPT-4",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("buildAllowedModelSet", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4": { alias: "gpt4" },
|
||||
"openai/gpt-5-nano": { alias: "gpt5nano" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -38,7 +38,9 @@ describe("buildAllowedModelSet", () => {
|
||||
});
|
||||
|
||||
expect(allowed.allowAny).toBe(false);
|
||||
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
|
||||
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-5-nano"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -57,7 +59,9 @@ describe("buildAllowedModelSet", () => {
|
||||
});
|
||||
|
||||
expect(allowed.allowAny).toBe(true);
|
||||
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
|
||||
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-5-nano"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -236,7 +240,7 @@ describe("resolveAllowedModelRef", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4": { alias: "GPT4" },
|
||||
"openai/gpt-5-nano": { alias: "GPT5NANO" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -244,12 +248,12 @@ describe("resolveAllowedModelRef", () => {
|
||||
const resolved = resolveAllowedModelRef({
|
||||
cfg,
|
||||
catalog: [
|
||||
{ provider: "openai", id: "gpt-4", name: "GPT-4" },
|
||||
{ provider: "openai", id: "gpt-5-nano", name: "GPT-5 Nano" },
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-1", name: "Sonnet" },
|
||||
],
|
||||
raw: "anthropic/claude-sonnet-4-1",
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4",
|
||||
defaultModel: "gpt-5-nano",
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
error: "model not allowed: anthropic/claude-sonnet-4-1",
|
||||
|
||||
@@ -207,11 +207,11 @@ describe("resolveExtraParams", () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for openai/gpt-4 with no config", () => {
|
||||
it("returns undefined for openai/gpt-5-nano with no config", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: undefined,
|
||||
provider: "openai",
|
||||
modelId: "gpt-4",
|
||||
modelId: "gpt-5-nano",
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -223,7 +223,7 @@ describe("resolveExtraParams", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4": {
|
||||
"openai/gpt-5-nano": {
|
||||
params: {
|
||||
logprobs: true,
|
||||
top_logprobs: 5,
|
||||
@@ -234,7 +234,7 @@ describe("resolveExtraParams", () => {
|
||||
},
|
||||
},
|
||||
provider: "openai",
|
||||
modelId: "gpt-4",
|
||||
modelId: "gpt-5-nano",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
|
||||
@@ -251,13 +251,6 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
||||
textAlias: "/queue",
|
||||
acceptsArgs: true,
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "bash",
|
||||
description: "Run host shell commands (host-only).",
|
||||
textAlias: "/bash",
|
||||
scope: "text",
|
||||
acceptsArgs: true,
|
||||
}),
|
||||
];
|
||||
|
||||
registerAlias(commands, "status", "/usage");
|
||||
@@ -321,7 +314,6 @@ export function isCommandEnabled(
|
||||
): boolean {
|
||||
if (commandKey === "config") return cfg.commands?.config === true;
|
||||
if (commandKey === "debug") return cfg.commands?.debug === true;
|
||||
if (commandKey === "bash") return cfg.commands?.bash === true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("block streaming", () => {
|
||||
piEmbeddedMock.runEmbeddedPiAgent.mockReset();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([
|
||||
{ id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
|
||||
{ id: "gpt-5-nano", name: "GPT-5 Nano", provider: "openai" },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("directive behavior", () => {
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([
|
||||
{ id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" },
|
||||
{ id: "claude-sonnet-4-1", name: "Sonnet 4.1", provider: "anthropic" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
|
||||
{ id: "gpt-5-nano", name: "GPT-5 Nano", provider: "openai" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: "openai/gpt-5-nano",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
@@ -1655,7 +1655,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1666,7 +1666,7 @@ describe("directive behavior", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
||||
expect(text).toContain("gpt-4.1-mini — openai");
|
||||
expect(text).toContain("gpt-5-nano — openai");
|
||||
expect(text).not.toContain("claude-sonnet-4-1");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1687,7 +1687,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1697,7 +1697,7 @@ describe("directive behavior", () => {
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
||||
expect(text).toContain("openai/gpt-4.1-mini");
|
||||
expect(text).toContain("openai/gpt-5-nano");
|
||||
expect(text).not.toContain("claude-sonnet-4-1");
|
||||
expect(text).toContain("auth:");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
@@ -1719,7 +1719,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1730,7 +1730,7 @@ describe("directive behavior", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
||||
expect(text).toContain("claude-opus-4-5 — anthropic");
|
||||
expect(text).toContain("gpt-4.1-mini — openai");
|
||||
expect(text).toContain("gpt-5-nano — openai");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1751,7 +1751,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1762,7 +1762,7 @@ describe("directive behavior", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
||||
expect(text).toContain("claude-opus-4-5 — anthropic");
|
||||
expect(text).toContain("gpt-4.1-mini — openai");
|
||||
expect(text).toContain("gpt-5-nano — openai");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1778,7 +1778,7 @@ describe("directive behavior", () => {
|
||||
id: "claude-opus-4-5",
|
||||
name: "Claude Opus 4.5",
|
||||
},
|
||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
||||
{ provider: "openai", id: "gpt-5-nano", name: "GPT-5 Nano" },
|
||||
]);
|
||||
const storePath = path.join(home, "sessions.json");
|
||||
|
||||
@@ -1792,7 +1792,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"minimax/MiniMax-M2.1": { alias: "minimax" },
|
||||
},
|
||||
},
|
||||
@@ -1813,7 +1813,7 @@ describe("directive behavior", () => {
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("claude-opus-4-5 — anthropic");
|
||||
expect(text).toContain("gpt-4.1-mini — openai");
|
||||
expect(text).toContain("gpt-5-nano — openai");
|
||||
expect(text).toContain("MiniMax-M2.1 — minimax");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1853,7 +1853,7 @@ describe("directive behavior", () => {
|
||||
const storePath = path.join(home, "sessions.json");
|
||||
|
||||
await getReplyFromConfig(
|
||||
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" },
|
||||
{ Body: "/model openai/gpt-5-nano", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agents: {
|
||||
@@ -1862,7 +1862,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1871,7 +1871,7 @@ describe("directive behavior", () => {
|
||||
);
|
||||
|
||||
assertModelSelection(storePath, {
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
provider: "openai",
|
||||
});
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
@@ -1889,10 +1889,10 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
@@ -2209,10 +2209,10 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
@@ -2242,10 +2242,10 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
@@ -2278,7 +2278,7 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
@@ -2309,7 +2309,7 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
@@ -2336,7 +2336,7 @@ describe("directive behavior", () => {
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "please sync /model openai/gpt-4.1-mini now",
|
||||
Body: "please sync /model openai/gpt-5-nano now",
|
||||
From: "+1004",
|
||||
To: "+2000",
|
||||
},
|
||||
@@ -2348,7 +2348,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
name: "Claude Opus 4.5 (OpenRouter)",
|
||||
contextWindow: 200000,
|
||||
},
|
||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
||||
{ provider: "openai", id: "gpt-5-nano", name: "GPT-5 Nano" },
|
||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
||||
@@ -320,7 +320,7 @@ describe("trigger handling", () => {
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/model openai/gpt-4.1-mini",
|
||||
Body: "/model openai/gpt-5-nano",
|
||||
From: "telegram:111",
|
||||
To: "telegram:111",
|
||||
ChatType: "direct",
|
||||
@@ -336,11 +336,11 @@ describe("trigger handling", () => {
|
||||
);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Model set to openai/gpt-4.1-mini");
|
||||
expect(text).toContain("Model set to openai/gpt-5-nano");
|
||||
|
||||
const store = loadSessionStore(cfg.session.store);
|
||||
expect(store[targetSessionKey]?.providerOverride).toBe("openai");
|
||||
expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini");
|
||||
expect(store[targetSessionKey]?.modelOverride).toBe("gpt-5-nano");
|
||||
expect(store[slashSessionKey]).toBeUndefined();
|
||||
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||
@@ -368,7 +368,7 @@ describe("trigger handling", () => {
|
||||
expect(vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -981,11 +981,6 @@ export async function getReplyFromConfig(
|
||||
command: inlineCommandContext,
|
||||
agentId,
|
||||
directives,
|
||||
elevated: {
|
||||
enabled: elevatedEnabled,
|
||||
allowed: elevatedAllowed,
|
||||
failures: elevatedFailures,
|
||||
},
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -1043,11 +1038,6 @@ export async function getReplyFromConfig(
|
||||
command,
|
||||
agentId,
|
||||
directives,
|
||||
elevated: {
|
||||
enabled: elevatedEnabled,
|
||||
allowed: elevatedAllowed,
|
||||
failures: elevatedFailures,
|
||||
},
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
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 buildBashDisabledReply(provider?: string): ReplyPayload {
|
||||
const providerKey = provider?.trim().toLowerCase() || "<provider>";
|
||||
return {
|
||||
text: [
|
||||
"⚠️ /bash is disabled (default).",
|
||||
"Enable it in your config (host-only; trusted senders only):",
|
||||
"```json5",
|
||||
"{",
|
||||
" commands: {",
|
||||
" bash: true",
|
||||
" },",
|
||||
" tools: {",
|
||||
" elevated: {",
|
||||
" enabled: true,",
|
||||
` allowFrom: { "${providerKey}": ["<sender-id>"] }`,
|
||||
" }",
|
||||
" }",
|
||||
"}",
|
||||
"```",
|
||||
"Restart the Gateway after updating config.",
|
||||
].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) {
|
||||
logVerbose("Blocked /bash: commands.bash is disabled.");
|
||||
const provider =
|
||||
params.ctx.Provider ??
|
||||
params.ctx.Surface ??
|
||||
params.ctx.OriginatingChannel ??
|
||||
undefined;
|
||||
return buildBashDisabledReply(provider ?? undefined);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { resetBashChatCommandForTests } from "./bash-command.js";
|
||||
import { buildCommandContext, handleCommands } from "./commands.js";
|
||||
import { parseInlineDirectives } from "./directive-handling.js";
|
||||
|
||||
@@ -34,7 +33,6 @@ function buildParams(
|
||||
cfg,
|
||||
command,
|
||||
directives: parseInlineDirectives(commandBody),
|
||||
elevated: { enabled: true, allowed: true, failures: [] },
|
||||
sessionKey: "agent:main:main",
|
||||
workspaceDir: "/tmp",
|
||||
defaultGroupActivation: () => "mention",
|
||||
@@ -49,37 +47,6 @@ function buildParams(
|
||||
}
|
||||
|
||||
describe("handleCommands gating", () => {
|
||||
it("blocks /bash when disabled", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: false, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/bash echo hi", cfg);
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("bash is disabled");
|
||||
});
|
||||
|
||||
it("blocks /bash when elevated is not allowlisted", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: true, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/bash echo hi", cfg);
|
||||
params.elevated = {
|
||||
enabled: true,
|
||||
allowed: false,
|
||||
failures: [
|
||||
{ gate: "allowFrom", key: "tools.elevated.allowFrom.whatsapp" },
|
||||
],
|
||||
};
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("elevated is not available");
|
||||
});
|
||||
|
||||
it("blocks /config when disabled", async () => {
|
||||
const cfg = {
|
||||
commands: { config: false, debug: false, text: true },
|
||||
@@ -103,32 +70,6 @@ describe("handleCommands gating", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleCommands bash alias", () => {
|
||||
it("routes !poll through the /bash handler", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: true, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("!poll", cfg);
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("No active bash job");
|
||||
});
|
||||
|
||||
it("routes !stop through the /bash handler", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: true, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("!stop", cfg);
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("No active bash job");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleCommands identity", () => {
|
||||
it("returns sender details for /whoami", async () => {
|
||||
const cfg = {
|
||||
|
||||
@@ -83,7 +83,6 @@ import type {
|
||||
} from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { isAbortTrigger, setAbortMemory } from "./abort.js";
|
||||
import { handleBashChatCommand } from "./bash-command.js";
|
||||
import { parseConfigCommand } from "./config-commands.js";
|
||||
import { parseDebugCommand } from "./debug-commands.js";
|
||||
import type { InlineDirectives } from "./directive-handling.js";
|
||||
@@ -401,11 +400,6 @@ export async function handleCommands(params: {
|
||||
command: CommandContext;
|
||||
agentId?: string;
|
||||
directives: InlineDirectives;
|
||||
elevated: {
|
||||
enabled: boolean;
|
||||
allowed: boolean;
|
||||
failures: Array<{ gate: string; key: string }>;
|
||||
};
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
@@ -431,7 +425,6 @@ export async function handleCommands(params: {
|
||||
cfg,
|
||||
command,
|
||||
directives,
|
||||
elevated,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -472,30 +465,6 @@ export async function handleCommands(params: {
|
||||
commandSource: ctx.CommandSource,
|
||||
});
|
||||
|
||||
const bashSlashRequested =
|
||||
allowTextCommands &&
|
||||
(command.commandBodyNormalized === "/bash" ||
|
||||
command.commandBodyNormalized.startsWith("/bash "));
|
||||
const bashBangRequested =
|
||||
allowTextCommands && command.commandBodyNormalized.startsWith("!");
|
||||
if (bashSlashRequested || (bashBangRequested && command.isAuthorizedSender)) {
|
||||
if (!command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
`Ignoring /bash from unauthorized sender: ${command.senderId || "<unknown>"}`,
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
const reply = await handleBashChatCommand({
|
||||
ctx,
|
||||
cfg,
|
||||
agentId: params.agentId,
|
||||
sessionKey,
|
||||
isGroup,
|
||||
elevated,
|
||||
});
|
||||
return { shouldContinue: false, reply };
|
||||
}
|
||||
|
||||
if (allowTextCommands && activationCommand.hasCommand) {
|
||||
if (!isGroup) {
|
||||
return {
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("buildStatusMessage", () => {
|
||||
sessionId: "override-1",
|
||||
updatedAt: 0,
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-4.1-mini",
|
||||
modelOverride: "gpt-5-nano",
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-haiku-4-5",
|
||||
contextTokens: 32_000,
|
||||
@@ -133,7 +133,7 @@ describe("buildStatusMessage", () => {
|
||||
modelAuth: "api-key",
|
||||
});
|
||||
|
||||
expect(normalizeTestText(text)).toContain("Model: openai/gpt-4.1-mini");
|
||||
expect(normalizeTestText(text)).toContain("Model: openai/gpt-5-nano");
|
||||
});
|
||||
|
||||
it("keeps provider prefix from configured model", () => {
|
||||
|
||||
@@ -25,9 +25,7 @@ describe("listThinkingLevels", () => {
|
||||
});
|
||||
|
||||
it("excludes xhigh for non-codex models", () => {
|
||||
expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain(
|
||||
"xhigh",
|
||||
);
|
||||
expect(listThinkingLevels(undefined, "gpt-5-nano")).not.toContain("xhigh");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -152,10 +152,10 @@ describe("agentCommand", () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe("agentCommand", () => {
|
||||
|
||||
const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
|
||||
expect(callArgs?.provider).toBe("openai");
|
||||
expect(callArgs?.model).toBe("gpt-4.1-mini");
|
||||
expect(callArgs?.model).toBe("gpt-5-nano");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("agents helpers", () => {
|
||||
name: "Work",
|
||||
workspace: "/work-ws",
|
||||
agentDir: "/state/agents/work/agent",
|
||||
model: "openai/gpt-4.1",
|
||||
model: "openai/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -157,8 +157,8 @@ describe("models list/status", () => {
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
@@ -199,8 +199,8 @@ describe("models list/status", () => {
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
@@ -241,8 +241,8 @@ describe("models list/status", () => {
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
|
||||
@@ -1603,10 +1603,10 @@ describe("legacy config detection", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
modelFallbacks: ["openai/gpt-4.1-mini"],
|
||||
imageModel: "openai/gpt-4.1-mini",
|
||||
modelFallbacks: ["openai/gpt-5-nano"],
|
||||
imageModel: "openai/gpt-5-nano",
|
||||
imageModelFallbacks: ["anthropic/claude-opus-4-5"],
|
||||
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
|
||||
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-5-nano"],
|
||||
modelAliases: { Opus: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
});
|
||||
@@ -1615,10 +1615,10 @@ describe("legacy config detection", () => {
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
expect(res.config?.agents?.defaults?.model?.fallbacks).toEqual([
|
||||
"openai/gpt-4.1-mini",
|
||||
"openai/gpt-5-nano",
|
||||
]);
|
||||
expect(res.config?.agents?.defaults?.imageModel?.primary).toBe(
|
||||
"openai/gpt-4.1-mini",
|
||||
"openai/gpt-5-nano",
|
||||
);
|
||||
expect(res.config?.agents?.defaults?.imageModel?.fallbacks).toEqual([
|
||||
"anthropic/claude-opus-4-5",
|
||||
@@ -1627,7 +1627,7 @@ describe("legacy config detection", () => {
|
||||
res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"],
|
||||
).toMatchObject({ alias: "Opus" });
|
||||
expect(
|
||||
res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"],
|
||||
res.config?.agents?.defaults?.models?.["openai/gpt-5-nano"],
|
||||
).toBeTruthy();
|
||||
expect(res.config?.agent).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -155,8 +155,6 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.cliBackends": "CLI Backends",
|
||||
"commands.native": "Native Commands",
|
||||
"commands.text": "Text Commands",
|
||||
"commands.bash": "Allow Bash Chat Command",
|
||||
"commands.bashForegroundMs": "Bash Foreground Window (ms)",
|
||||
"commands.config": "Allow /config",
|
||||
"commands.debug": "Allow /debug",
|
||||
"commands.restart": "Allow Restart",
|
||||
@@ -291,10 +289,6 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"commands.native":
|
||||
"Register native commands with connectors that support it (Discord/Slack/Telegram).",
|
||||
"commands.text": "Allow text command parsing (slash commands only).",
|
||||
"commands.bash":
|
||||
"Allow bash chat command (`!`; `/bash` alias) to run host shell commands (default: false; requires tools.elevated).",
|
||||
"commands.bashForegroundMs":
|
||||
"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).",
|
||||
"commands.config":
|
||||
"Allow /config chat command to read/write config on disk (default: false).",
|
||||
"commands.debug":
|
||||
|
||||
@@ -1244,10 +1244,6 @@ export type CommandsConfig = {
|
||||
native?: NativeCommandsSetting;
|
||||
/** Enable text command parsing (default: true). */
|
||||
text?: boolean;
|
||||
/** Allow bash chat command (`!`; `/bash` alias) (default: false). */
|
||||
bash?: boolean;
|
||||
/** How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately). */
|
||||
bashForegroundMs?: number;
|
||||
/** Allow /config command (default: false). */
|
||||
config?: boolean;
|
||||
/** Allow /debug command (default: false). */
|
||||
|
||||
@@ -724,8 +724,6 @@ const CommandsSchema = z
|
||||
.object({
|
||||
native: NativeCommandsSettingSchema.optional().default("auto"),
|
||||
text: z.boolean().optional(),
|
||||
bash: z.boolean().optional(),
|
||||
bashForegroundMs: z.number().int().min(0).max(30_000).optional(),
|
||||
config: z.boolean().optional(),
|
||||
debug: z.boolean().optional(),
|
||||
restart: z.boolean().optional(),
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
||||
job: makeJob({
|
||||
kind: "agentTurn",
|
||||
message: "do it",
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: "openai/gpt-5-nano",
|
||||
}),
|
||||
message: "do it",
|
||||
sessionKey: "cron:job-1",
|
||||
@@ -226,7 +226,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
||||
model?: string;
|
||||
};
|
||||
expect(call?.provider).toBe("openai");
|
||||
expect(call?.model).toBe("gpt-4.1-mini");
|
||||
expect(call?.model).toBe("gpt-5-nano");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("hooks mapping", () => {
|
||||
match: { path: "gmail" },
|
||||
action: "agent",
|
||||
messageTemplate: "Subject: {{messages[0].subject}}",
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: "openai/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -58,7 +58,7 @@ describe("hooks mapping", () => {
|
||||
});
|
||||
expect(result?.ok).toBe(true);
|
||||
if (result?.ok && result.action.kind === "agent") {
|
||||
expect(result.action.model).toBe("openai/gpt-4.1-mini");
|
||||
expect(result.action.model).toBe("openai/gpt-5-nano");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("gateway server hooks", () => {
|
||||
body: JSON.stringify({
|
||||
message: "Do it",
|
||||
name: "Email",
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: "openai/gpt-5-nano",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(202);
|
||||
@@ -96,7 +96,7 @@ describe("gateway server hooks", () => {
|
||||
const call = cronIsolatedRun.mock.calls[0]?.[0] as {
|
||||
job?: { payload?: { model?: string } };
|
||||
};
|
||||
expect(call?.job?.payload?.model).toBe("openai/gpt-4.1-mini");
|
||||
expect(call?.job?.payload?.model).toBe("openai/gpt-5-nano");
|
||||
drainSystemEvents(resolveMainKey());
|
||||
await server.close();
|
||||
});
|
||||
|
||||
@@ -122,9 +122,9 @@ const spawnGatewayInstance = async (name: string): Promise<GatewayInstance> => {
|
||||
|
||||
try {
|
||||
child = spawn(
|
||||
"node",
|
||||
"bun",
|
||||
[
|
||||
"dist/index.js",
|
||||
"src/index.ts",
|
||||
"gateway",
|
||||
"--port",
|
||||
String(port),
|
||||
@@ -222,7 +222,7 @@ const runCliJson = async (
|
||||
): Promise<unknown> => {
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
const child = spawn("node", ["dist/index.js", ...args], {
|
||||
const child = spawn("bun", ["src/index.ts", ...args], {
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, ...env },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
|
||||
Reference in new issue
Block a user