An open API service indexing awesome lists of open source software.

https://github.com/opencrabs/opencrabs

The all-in-one AI agent. Build mobile apps, frontend, backend, manages files, deep research, schedule tasks and events. Self-improving, self-healing, fully autonomous. TUI, CLI and Daemon. Connect to your favorite channels. Zero Telemetry.
https://github.com/opencrabs/opencrabs

agent-orchestration agentic-ai agentic-framework autonomous-agents harness harness-framework open-source opencrabs orchestration-framework recursive-self-improvement rsi self-healing self-learning

Last synced: 16 days ago
JSON representation

The all-in-one AI agent. Build mobile apps, frontend, backend, manages files, deep research, schedule tasks and events. Self-improving, self-healing, fully autonomous. TUI, CLI and Daemon. Connect to your favorite channels. Zero Telemetry.

Awesome Lists containing this project

README

          

[![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)](https://www.rust-lang.org)
[![Rust Edition](https://img.shields.io/badge/rust-2024_edition-orange.svg)](https://www.rust-lang.org/)
[![Ratatui](https://img.shields.io/badge/ratatui-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)](https://ratatui.rs)
[![Docker](https://img.shields.io/badge/docker-%23000000.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docker.com)
[![CI](https://github.com/opencrabs/opencrabs/actions/workflows/ci.yml/badge.svg)](https://github.com/opencrabs/opencrabs/actions/workflows/ci.yml)
[![GitHub Stars](https://img.shields.io/github/stars/opencrabs/opencrabs?style=social)](https://github.com/opencrabs/opencrabs)

opencrabs/opencrabs β€” Trendshift #3 Repository Of The Day, Rust
opencrabs/opencrabs β€” Trendshift #20 Repository Of The Week, Rust

# πŸ¦€ OpenCrabs

**The autonomous, self-improving AI agent. Single Rust binary. Every channel.**

> Autonomous, self-improving multi-channel AI agent built in Rust.

```
___ ___ _
/ _ \ _ __ ___ _ _ / __|_ _ __ _| |__ ___
| (_) | '_ \/ -_) ' \ | (__| '_/ _` | '_ \(_-<
\___/| .__/\___|_||_| \___|_| \__,_|_.__//__/
|_|

πŸ¦€ The autonomous, self-improving AI agent. Single Rust binary. Every channel.

```

**Author:** [Adolfo Usier](https://github.com/adolfousier)

⭐ Star us on [GitHub](https://github.com/opencrabs/opencrabs) if you like what you see!

**Community funded.** No venture capital, no investors, no company behind it. The people who use OpenCrabs pay for the subscriptions, the inference and the credits that keep it running: see [🫢 Sponsors](#-sponsors).

---

## Table of Contents

- [πŸ“š Documentation](#-documentation)
- [Why OpenCrabs?](#why-opencrabs)
- [πŸ“Š Benchmarks](#-benchmarks)
- [🎬 Onboarding Videos](#-onboarding-videos)
- [🎬 Demo](#-demo)
- [πŸ–₯️ Split Panes](#-split-panes)
- [🎯 Core Features](#-core-features)
- [πŸ”„ Migrating from Other Tools](#-migrating-from-other-tools)
- [🌐 Supported AI Providers](#-supported-ai-providers)
- [πŸ–ΌοΈ Image Generation & Vision](#-image-generation--vision)
- [πŸ“„ Document Generation](#-document-generation)
- [🀝 Agent-to-Agent (A2A) Protocol](#-agent-to-agent-a2a-protocol)
- [πŸš€ Quick Start](#-quick-start)
- [πŸ§™ Onboarding Wizard](#-onboarding-wizard)
- [πŸ”‘ API Keys (keys.toml)](#-api-keys-keystoml)
- [πŸ” Secret Sanitization & Redaction](#-secret-sanitization--redaction)
- [πŸ›‘οΈ Security Controls](#️-security-controls)
- [🏠 Using Local LLMs](#-using-local-llms)
- [πŸ“ Configuration](#-configuration)
- [πŸ› οΈ Configuration (config.toml)](#-configuration-configtoml)
- [🧠 Epistemic Engine](#-epistemic-engine)
- [♻️ Decision Cache (`[decisions]`)](#-decision-cache-decisions)
- [🧾 Audit Recording (`[features]`)](#-audit-recording-features)
- [πŸ›‘οΈ Safety Gates (~/.opencrabs/safety/)](#-safety-gates-opencrabssafety)
- [πŸ“‹ Commands (commands.toml)](#-commands-commandstoml)
- [πŸ”Œ Dynamic Tools (tools.toml)](#-dynamic-tools-toolstoml)
- [πŸ’° Pricing Customization (usage_pricing.toml)](#-pricing-customization-usage_pricingtoml)
- [πŸ”§ Tool System](#-tool-system)
- [⌨️ Keyboard Shortcuts](#-keyboard-shortcuts)
- [πŸ” Debug and Logging](#-debug-and-logging)
- [🧠 Brain System & 3-Tier Memory](#-brain-system--3-tier-memory)
- [🎯 /goal β€” Autonomous Goal Loop](#-goal--autonomous-goal-loop)
- [⏰ Cron Jobs](#-cron-jobs)
- [πŸ—οΈ Architecture](#-architecture)
- [πŸ“ Project Structure](#-project-structure)
- [πŸ› οΈ Development](#-development)
- [πŸ› Platform Notes](#-platform-notes)
- [πŸ”§ Troubleshooting](#-troubleshooting)
- [🧩 Companion Tools](#-companion-tools)
- [⚠️ Disclaimers](#-disclaimers)
- [🀝 Contributing](#-contributing)
- [πŸ“„ License](#-license)
- [🫢 Sponsors](#-sponsors)
- [πŸ™ Acknowledgments](#-acknowledgments)
- [πŸ“ž Support](#-support)
- [✨ Stay Tuned](#-stay-tuned)

---

## πŸ“š Documentation

**Official docs:** [docs.opencrabs.com](https://docs.opencrabs.com) β€” comprehensive guides, architecture deep-dives, and reference material.

The docs and the landing at [opencrabs.com](https://opencrabs.com) are available in six languages: English, PortuguΓͺs (PT-PT), EspaΓ±ol, FranΓ§ais, Русский and Bahasa Indonesia. Pick one from the language switcher in the menu bar; untranslated paragraphs fall back to English rather than going missing.

### Getting Started
- [Installation & Quick Start](https://docs.opencrabs.com/getting-started/installation.html)
- [Onboarding Wizard](src/docs/start/ONBOARDING.md)
- [OpenCrabs Overview](src/docs/start/OPENCRABS.md)

### Reference
- [Architecture](src/docs/reference/ARCHITECTURE_2026_04_14.md)
- [Brain Constitution](src/docs/reference/BRAIN_CONSTITUTION.md)
- [Adding New Providers](src/docs/reference/ADDING_NEW_PROVIDERS.md)
- [Plan JSON Specification](src/docs/reference/plans/plan-json-spec.md)
- [Dynamic Workflows Guide](src/docs/reference/DYNAMIC_WORKFLOWS.md) β€” orchestrate agents with scripts: fan-out, pipelines, structured outputs, checkpointing
- [Decision Cache](src/docs/reference/DECISIONS.md): L1 exact-decision reuse ring, `[decisions]` tiers, shadow/live/off modes, accounting, release-day evaluation (#1648)

### Brain File Templates
- [SOUL.md](src/docs/reference/templates/SOUL.md) β€” personality and voice
- [AGENTS.md](src/docs/reference/templates/AGENTS.md) β€” workspace governance and hard rules
- [TOOLS.md](src/docs/reference/templates/TOOLS.md) β€” tool usage and skills
- [MEMORY.md](src/docs/reference/templates/MEMORY.md) β€” long-term memory
- [CODE.md](src/docs/reference/templates/CODE.md) β€” coding standards
- [SECURITY.md](src/docs/reference/templates/SECURITY.md) β€” security policies the agent follows (not the repo's vulnerability-disclosure policy, which is [/SECURITY.md](SECURITY.md))
- [BOOT.md](src/docs/reference/templates/BOOT.md) β€” startup and service config
- [USER.md](src/docs/reference/templates/USER.md) β€” user profile template
- [HEARTBEAT.md](src/docs/reference/templates/HEARTBEAT.md) β€” periodic checklist, read on demand or by a cron job

### Skills

All of these ship inside the binary, available as `/` in every install, nothing to copy:

- [Security Audit](src/docs/reference/templates/skills/security-audit/SKILL.md)
- [Cost Estimate](src/docs/reference/templates/skills/cost-estimate/SKILL.md)
- [Repo Audit](src/docs/reference/templates/skills/repo-audit/SKILL.md)
- [Multi-Agent](src/docs/reference/templates/skills/multi-agent/SKILL.md)
- [Browser CDP](src/docs/reference/templates/skills/browser-cdp/SKILL.md)
- [A2A Gateway](src/docs/reference/templates/skills/a2a-gateway/SKILL.md)
- [Dynamic Tools](src/docs/reference/templates/skills/dynamic-tools/SKILL.md)

### Opt-in Skills

Not built in, so they do not reach every user. Copy a folder into `~/.opencrabs/skills//` and it shows up as `/` for you only:

- [Motion Graphics](src/docs/reference/templates/optional-skills/motion-graphics/SKILL.md): tutorial videos with Remotion (storyboard, drawn UI, voice-over, subtitles). Install with `cp -r src/docs/reference/templates/optional-skills/motion-graphics ~/.opencrabs/skills/motion-graphics`.

### Cron Templates
- [Cron Jobs Guide](src/docs/reference/templates/cron/README.md)

### Command Templates (opt-in, LLM-flavored)
- [Commands Guide](src/docs/reference/templates/commands/README.md): copy-paste `commands.toml` fragments that call the LLM. The mechanical, zero-cost commands (`/architecture`, `/attach`) are built in and need no install.

---

## Why OpenCrabs?

OpenCrabs runs as a **single binary on your terminal** β€” no server, no gateway, no infrastructure. It makes direct HTTPS calls to LLM providers from your machine. Nothing else leaves your computer.

### OpenCrabs vs Node.js Agent Frameworks

| | **OpenCrabs** (Rust) | **Node.js Frameworks** (e.g. Open Claw) |
|---|---|---|
| **Binary size** | **34–36 MB** single binary, zero dependencies | **1 GB+** `node_modules` with hundreds of transitive packages |
| **Runtime** | None β€” runs natively | Requires Node.js runtime + npm install |
| **Attack surface** | Zero network listeners. Outbound HTTPS only | Server infrastructure: open ports, auth layers, middleware |
| **API key security** | Keys on your machine only. `zeroize` clears them from RAM on drop, `[REDACTED]` in all debug output | Keys in env vars or config. GC doesn't guarantee memory clearing. Heap dumps can leak secrets |
| **Data residency** | 100% local β€” SQLite DB, embeddings, brain files, all in `~/.opencrabs/` | Server-side storage, potential multi-tenant data, network transit |
| **Supply chain** | Single compiled binary. Rust's type system prevents buffer overflows, use-after-free, data races at compile time | npm ecosystem: typosquatting, dependency confusion, prototype pollution |
| **Memory safety** | Compile-time guarantees β€” no GC, no null pointers, no data races | GC-managed, prototype pollution, type coercion bugs |
| **Concurrency** | tokio async + Rust ownership = zero data races guaranteed | Single-threaded event loop, worker threads share memory unsafely |
| **Native TTS/STT** | Built-in local speech-to-text (whisper.cpp) and text-to-speech β€” ~130 MB total stack, fully offline | No native voice. Requires external APIs (Google, AWS, Azure) or heavy Python dependencies (PyTorch, ~5 GB+) |
| **Telemetry** | Zero. No analytics, no tracking, no remote logging | Server infra typically includes monitoring, logging pipelines, APM |

### What stays local (never leaves your machine)

- All chat sessions and messages (SQLite)
- Tool executions (bash, file reads/writes, git)
- Memory and embeddings (local vector search)
- Voice transcription in local STT mode (whisper.cpp, on-device)
- Brain files, config, API keys

### What goes out (only when you use it)

- Your messages to the LLM provider API (Anthropic, OpenAI, GitHub Copilot, etc.)
- Web search queries (optional tool)
- GitHub API via `gh` CLI (optional tool)
- Browser automation (optional, `browser` feature β€” auto-detects Chromium-based browsers via CDP, not Firefox)
- Dynamic tool HTTP requests (only when you define HTTP tools in `tools.toml`)

### πŸ”’ Zero Telemetry β€” Not Even Opt-In

**OpenCrabs does not phone home. Ever.**

No analytics. No tracking. No usage statistics. No remote logging. No crash reports. No "anonymous telemetry." Nothing.

Your data stays on your machine. Your conversations, your tools, your memory, your configuration, your API keys β€” all of it. The only outbound traffic is what you explicitly initiate: LLM API calls, web searches, GitHub commands, browser automation.

Other AI harnesses silently collect usage data, performance metrics, and behavioral analytics. OpenCrabs makes a different bet: **what happens on your machine stays on your machine.**

This isn't a privacy policy checkbox. It's an architectural decision. There is no telemetry code to disable, no opt-out flag to set, no analytics service to block. There's simply nothing to send.

---

## πŸ“Š Benchmarks

Measured on live workspaces, not fixtures. Full reports, including what each one explicitly did **not** measure, live in [`src/eval/results/`](src/eval/results/).

### Memory retrieval ([full report](src/eval/results/memory-retrieval-2026-08-12.md))

Every figure was measured on a live workspace: 9 brain files, 158 daily notes, 93 session documents, 258 MB.

| Metric | Before | After |
|---|---|---|
| Rule-lookup hit rate (`scope="brain"`) | 2/5 | **5/5** |
| Cost of one rule lookup | 33,443 chars (whole `AGENTS.md`) | **~1,000 chars (βˆ’97%)** |
| precision@2, per-turn recall ranking | 0.625 | **0.917** |
| Multilingual precision/recall@2 (6 languages) | not measured | **1.000 / 1.000** |
| Worst index staleness | 15 hours | **current (refresh on write and on search)** |
| Empty placeholder vectors | 495 | **0** |
| Indexed vector rows | 1,944 | **5,744** |

End-to-end check: a fact from **54 days back** (why a monthly invoice cron job had missed its schedule) was recovered in 9 steps, 1m 40s, starting from `memory_search` and reading the primary sources it located. Full chain in the [report](src/eval/results/memory-retrieval-2026-08-12.md).

### Recall quality ([full report](src/eval/results/memory-recall-v1.md))

Fixture-scored (13-section corpus, 12 positive + 12 negative queries), then sanity-checked against a real 156-section memory file and 437 real user messages:

| Metric | Before (hit count β‰₯ 2) | After (normalized BM25 β‰₯ 0.35) |
|---|---|---|
| precision@2 | 0.625 | **0.917** |
| recall@2 | **1.000** | 0.917 |
| False-positive rate | 0.417 | **0.250** |
| Memory injected into messages (real corpus) | 89.5% | **17.4%** |

Recall falling is the point: the old rule answered almost everything, which is exactly why its precision was poor. The six-language eval (en, ru, es, pt, fr, id) scores 1.000 / 1.000 with per-language recall 3/3, and building it caught two real tokenizer bugs: accent-sensitive matching and Cyrillic combining marks.

### Chunked embeddings ([full report](src/eval/results/memory-chunking.md))

25.5% of vector rows were empty placeholders, making those documents invisible to the semantic half of hybrid search, and nothing over the size guard had ever been chunked. Both fixed; ranking is now per chunk instead of per averaged document vector.

### Structural code search ([full report](src/eval/results/code-graph-structural-v1.md))

Measured on this repository (1,902 `.rs` files), ground truth locked by grep before any blind search. The symbol graph (`code-graph`, on by default) adds a structural lane beside FTS5+vector:

| Query class | Before (FTS5+vector) | After (+ symbol graph) |
|---|---|---|
| Callers of a function | text chunks, no caller info | **exact callers, file + line** |
| Callers, generic-heavy path | text chunks | **5/5** callers, file + line ([#1328](https://github.com/opencrabs/opencrabs/pull/1328)) |
| Duplicate implementations | 2 of 3 found | **3 of 3**, exact locations |
| Module structure | file content only | + full function inventory |
| Concept lookup | strong | unchanged β€” text lane untouched |

Graph: 15,769 symbols / 95,601 call edges / 5,649 imports, full-repo index in 12.1 s. Four extractor defects found during and after the run (receiver-qualified callees, enum-variant noise, test-name ranking, nested-call/impl recursion) are fixed β€” regression-tested in the report.

### Latency (criterion, release build)

`cargo bench --bench memory`, in-memory SQLite: FTS search 2.57 ms at 50 docs, vector search 1.02 ms, hybrid RRF fusion 3.49 ms, indexing 214 Β΅s per file. Full tables in the [Development section](#-development).

---

## 🎬 Onboarding Videos

| OS | Quick start | Advanced setup (Telegram, voice, images) |
|----|-------------|------------------------------------------|
| macOS | [Install in under 2 minutes (Homebrew)](https://youtu.be/1_Ta9BFxlSk) | [Advanced setup on macOS](https://youtu.be/Lq891K2GFzU) |
| Ubuntu/Linux | [Install in 3 minutes (one paste)](https://youtu.be/sXfaKfQiAxo) | [Advanced setup on Linux](https://youtu.be/uZLwzerkQt0) |
| Windows | [Install in 2 minutes (PowerShell)](https://youtu.be/kY8SW83obOc) | [Advanced setup on Windows](https://youtu.be/TZn1aYgpRuI) |

All videos: [youtube.com/@OpenCrabs](https://www.youtube.com/@OpenCrabs)

## 🎬 Demo

https://github.com/user-attachments/assets/7f45c5f8-acdf-48d5-b6a4-0e4811a9ee23

## πŸ–₯️ Split Panes

![Split Panes](src/assets/split-panes.png)

---

## 🎯 Core Features

### AI & Providers
| Feature | Description |
|---------|-------------|
| **Multi-Provider** | **OpenCrabs** (built in: no account or key to set up), **Xiaomi MiMo**, Anthropic Claude, OpenAI, GitHub Copilot (uses your Copilot subscription), OpenRouter (400+ models), MiniMax, Google Gemini, z.ai GLM (General API + Coding API), Moonshot Kimi (API plan + Coding plan), Claude CLI, OpenCode CLI, Codex CLI (uses your ChatGPT/Codex subscription), Qwen Native (free OAuth with multi-account rotation), Qwen Code CLI (1k free req/day), and any OpenAI-compatible API (Ollama, LM Studio, LocalAI). Model lists fetched live from provider APIs. New models available instantly. Custom provider dialog: paste-by-default for API keys, Enter-to-load live models, typed-not-in-list models accepted and merged. Each session remembers its provider + model and restores it on switch |
| **Fallback Providers** | Configure a chain of fallback providers β€” if the primary fails, each fallback is tried in sequence automatically. Any configured provider can be a fallback. Config: `[providers.fallback] providers = ["openrouter", "anthropic"]` |
| **Per-Provider Timeouts** | `timeout_secs` caps a non-streaming request (one that buffers a whole body); it never caps a stream. `stream_idle_timeout_secs` is the only stream timer; it caps inter-chunk silence before the stream is treated as dropped and retried, so a long turn that keeps delivering is never cut. Defaults when unset: 3600s for CLI and local providers, 120s for every OpenAI-compatible remote provider (z.ai included), 20s for the native Anthropic and Gemini families |
| **Per-Provider Vision** | Set `vision_model` per provider β€” the LLM calls `analyze_image` as a tool, which uses the vision model on the same provider API to describe images. The chat model stays the same and gets vision capability via tool call. Gemini vision takes priority when configured. Auto-configured for known providers (e.g. MiniMax) on first run |
| **Prompt Caching** | Caches the stable context prefix (system prompt, brain files, earlier turns) on every caching-capable provider β€” Anthropic native (default), OpenAI/OpenRouter (`cache_enabled`), Qwen/Alibaba (zero-config auto), Xiaomi (server-side). Averaging ~87% cache efficiency in real use; watch it live in the Cache Efficiency card of `/usage`. Big reason a larger context window stays affordable |
| **Context Window & Auto-Compaction** | Per-provider `context_window` override (default 200k, works on every provider except the built-in one, whose window is fixed at 200k); transparent auto-compaction at 65% (soft, background) / 90% (hard) of the window gives effectively unlimited session memory with no manual clearing |
| **Real-time Streaming** | Character-by-character response streaming with animated spinner showing model name and live text |
| **Local LLM Support** | Run with LM Studio, Ollama, or any OpenAI-compatible endpoint β€” 100% private, zero-cost |
| **Usage Dashboard** | Per-message token count and cost displayed in header; `/usage` opens an interactive dashboard with daily activity charts, cost breakdowns by project/provider/model/activity, core tool usage stats, a `N% cache` column on the per-model rows showing the share of that model's recorded tokens that came from cache, and period filtering (Today/Week/Month/All-Time). Sessions are auto-categorized on startup (Development, Bug Fixes, Features, Refactoring, Testing, Documentation, CI/Deploy, etc.). Estimated costs for historical sessions shown as `~$X.XX` |
| **Context Awareness** | Live context usage indicator showing actual token counts (e.g. `ctx: 45K/200K (23%)`); auto-compaction at 70% with tool overhead budgeting; accurate tiktoken-based counting calibrated against API actuals |
| **3-Tier Memory** | (1) **Brain MEMORY.md** β€” user-curated durable memory, loaded on demand in the main session (see [Brain Files](#brain-files--one-file-one-job)), (2) **Daily Logs** β€” auto-compaction summaries at `~/.opencrabs/memory/YYYY-MM-DD.md`, (3) **Hybrid Memory Search** β€” FTS5 keyword search + vector embeddings combined via Reciprocal Rank Fusion. Three modes: **Local** (embeddinggemma-300M, 768-dim, no API key, works offline), **API** (any OpenAI-compatible `/v1/embeddings` endpoint: OpenAI, Ollama, Jina, etc.), or **FTS5-only** (no embeddings, VPS-friendly, ~0 RAM overhead). Auto-detects VPS environments and disables local embeddings |
| **Dynamic Brain System** | System brain assembled from workspace MD files (SOUL, USER, AGENTS, TOOLS, MEMORY) β€” all editable live between turns |
| **Multi-Agent Orchestration** | Spawn typed child agents (General, Explore, Plan, Code, Research) for parallel task execution. Five tools: `spawn_agent`, `wait_agent`, `send_input`, `close_agent`, `resume_agent`. Each type gets a role-specific system prompt and filtered tool registry. Configurable subagent provider/model. Children run in isolated sessions with auto-approve β€” no recursive spawning |
| **Recursive Self-Improvement** | ⚠️ Experimental. Automatic feedback ledger tracks every tool execution, user correction, and provider error. Three tools: `feedback_record` (log observations), `feedback_analyze` (query patterns), `self_improve` (autonomously apply brain file changes β€” no human approval). Changes logged to `~/.opencrabs/rsi/improvements.md` with daily archives. Startup digest injects performance summary into system prompt. **Upstream template sync** β€” automatically detects new releases, fetches updated brain file templates from the repo, diffs against local files, and appends only new sections (never overwrites user customizations). Backups created before every merge. Zero tokens spent when version unchanged. Zero setup β€” works out of the box via auto-migration |

### Multimodal Input
| Feature | Description |
|---------|-------------|
| **Image Attachments** | Paste image paths or URLs into the input β€” auto-detected and attached as vision content blocks for multimodal models. Also supports pasting raw image data from the clipboard (copied from a browser, screenshot tool, or any app) β€” on macOS via the clipboard as PNG, on Linux via wl-paste/xclip. The bytes are written to a temp file and routed through the existing image pipeline |
| **Video Attachments** | Send a video on any channel (mp4, m4v, mov, webm, mkv, avi, 3gp, flv) or paste a video path in the TUI β€” the agent calls the `analyze_video` tool, which routes through Google Gemini's multimodal video API (inline ≀18 MB, resumable Files API for larger). Requires `image.vision.enabled = true` with a Gemini API key in `config.toml`. Phase 1 is Gemini-native; a frame-extraction fallback for non-Gemini providers (ffmpeg β†’ analyze_image per frame) is on the roadmap |
| **Universal Paste & Drop** | Paste or drop a file of any type into the input (v0.5.4): one shared attachment router accepts every file type (#1740) and classifies common types (code, docs, archives, data) so each lands in the right pipeline (#1743) |
| **PDF Support** | Attach PDF files by path β€” native Anthropic PDF support; for other providers, text is extracted locally via `pdf-extract`. **Scanned / image-only PDFs** (no embedded text) are rendered to page images so vision models can read them β€” this needs **poppler** (`pdftoppm`) on the system: macOS `brew install poppler`, Debian/Ubuntu `apt install poppler-utils`, Fedora `dnf install poppler-utils`. The one-line installer sets this up automatically; without it, the PDF is still saved and its path handed to the agent (text extraction and the `pdf_to_images` tool can be retried once poppler is present) |
| **Document Parsing** | Built-in `parse_document` tool extracts text from PDF, DOC, DOCX, XLSX, XLSM, XLSB, XLS, ODS, CSV, HTML, TXT, MD, JSON, XML. All native Rust, zero external services: PDF text via `pdf-extract`, legacy Word 97-2003 `.doc` via `rwml`, DOCX/XML via a `quick-xml` streaming walk, spreadsheets (all five Excel/ODS variants) via `calamine`, CSV via `csv`. Scanned/image-only PDFs fall back to page-image rendering for vision models (see **PDF Support** above). Spreadsheet files are parsed into readable table format with sheet headers. Reading legacy binary `.ppt` is out of scope by design |
| **Document Generation** | Built-in `generate_document` tool creates XLSX (live Excel formulas), DOCX, and PDF natively in Rust with zero host dependencies, plus PPTX via python-pptx when present. Full styling per format: brand colors, page headers/footers with logos and page numbers, zebra tables, frozen headers, autofilters, number formats, PowerPoint brand templates. Image blocks embed PNG/JPEG inline with optional captions in PDF and DOCX. Generated files are delivered as downloadable attachments on Telegram/WhatsApp/Discord. See [Document Generation](#-document-generation) |
| **Voice (STT)** | Voice notes transcribed via **Groq Whisper API** (`whisper-large-v3-turbo`), any **OpenAI-compatible STT endpoint** (set `base_url` + `model` under `[providers.stt.openai_compatible]` β€” works with self-hosted Whisper, Deepgram-compatible proxies, etc.), **Voicebox STT** (self-hosted open-source voice stack β€” point the `base_url` of `[providers.stt.voicebox]` at your instance; 2s liveness probe runs before each request so a dead voicebox fails fast), or **Local** whisper.cpp via `whisper-rs` (runs on-device, Tiny 75 MB / Base 142 MB / Small 466 MB / Medium 1.5 GB, zero API cost). All dispatched through a single entry point so every channel gets the same provider priority chain β€” and an optional `[providers.stt].fallback_chain` lets the user codify "if my local voicebox is down, try Groq, then OpenAI" so transient outages auto-route to the next provider with zero user action. Voice notes keep working: transcription runs through whichever STT engine above is configured, and the built-in OpenCrabs provider transcribes out of the box. It answers no `/v1/audio/transcriptions` route, so the audio travels as an `input_audio` part on `/v1/chat/completions` alongside the transcription instruction, and the shipped config sets `stt_model = "qwen3.8-omni-flash"` there. Setting `stt_model` on any other active provider sends transcription through that provider's own endpoint, ahead of `[providers.stt.openai_compatible]`. Choose mode in `/voice`. Included by default |
| **Voice (TTS)** | Agent replies to voice notes with audio via **OpenAI TTS API** (`gpt-4o-mini-tts`), any **OpenAI-compatible TTS endpoint** (set `base_url` + `model` + `voice` under `[providers.tts.openai_compatible]` β€” works with self-hosted Coqui/Bark, ElevenLabs-compatible proxies, etc.), **Voicebox TTS** (async `/generate` β†’ poll `/generate/{id}/status` β†’ fetch audio; set the `base_url` + `profile_id` of `[providers.tts.voicebox]`), or **Local** Piper TTS (runs on-device via Python venv, Ryan / Amy / Lessac / Kristin / Joe / Cori, zero API cost). All outputs normalised to OGG/Opus via `ensure_opus` before delivery β€” consistent format across every channel regardless of backend. `[providers.tts].fallback_chain` provides the same auto-failover behaviour as the STT side. The built-in OpenCrabs provider does not serve synthesis yet: it answers no `/v1/audio/speech` route, so the shipped config leaves `tts_model` unset there. Setting `tts_model` on any active provider that exposes the route sends speech through that provider's own endpoint, ahead of `[providers.tts.openai_compatible]`. A voice note is still transcribed and still answered when no synthesis engine is available: the reply arrives as text. |
| **Attachment Indicator** | Attached images show as `[IMG1:filename.png]` in the input title bar |
| **Image Generation** | Agent generates images with the `generate_image` tool. The default route is Google Gemini (`gemini-3.1-flash-image-preview` "Nano Banana"), enabled via `/image`. The built-in OpenCrabs provider serves it out of the box on fresh installs (`gemini-2.5-flash-image`). Returned as native images/attachments in all channels |

#### Vision setup β€” two paths, pick one

> **Using Xiaomi?** Share images and OpenCrabs automatically routes to MiMo's multimodal model β€” no separate vision key needed.
>
> **Using OpenCrabs?** The built-in provider sets `vision_model = "deepseek-v4.1-flash"` on fresh installs, so attached images work on day 0 with no setup. Upgraded installs keep their existing `config.toml`; add `vision_model = "deepseek-v4.1-flash"` under `[providers.opencrabs]` to turn it on.

**Path A (preferred, simpler).** Set `vision_model = ""` on your active `[providers.]` block in `config.toml`. Works for every built-in and custom provider β€” the agent calls the vision model on the **same provider endpoint** via the `analyze_image` tool, so no second API key is needed. Pick a vision-capable model on that provider (DeepSeek chat models like `deepseek-v4-flash` reject `image_url` content, so point `vision_model` at a vision-capable variant of the same family β€” every provider has at least one).

```toml
[providers.opencode]
enabled = true
vision_model = "mimo-v2-omni" # any vision-capable model on this provider
```

**Path B (fallback).** Enable Gemini globally. Use this only when your active provider has no vision-capable model. Easiest way: run `/image` and the wizard walks you through. Manual setup:

```toml
# config.toml
[image.vision]
enabled = true
model = "gemini-3.1-flash-image-preview"
provider = "openrouter" # Optional: force vision to use a specific provider (bypasses enabled gate)
```

```toml
# keys.toml ← the Gemini key MUST live here, NOT in config.toml
[image]
api_key = "YOUR_GEMINI_KEY"
```

> **Gotcha:** `[image.vision] api_key = "..."` in `config.toml` is silently ignored β€” the field carries `#[serde(skip)]` for security. Use `keys.toml` `[image]` section, or `[providers.image.gemini]` in config.toml + the key in keys.toml.

> **Pinning vision to a provider:** set `[providers.fallback] vision = ["name"]` to try that provider first for `analyze_image` and `analyze_video`, regardless of its `enabled` flag (vision needs only `vision_model` plus a key). Names follow the same rule as every other provider key: the bare section name, so `[providers.custom.myprovider]` is `"myprovider"`. An entry that does not resolve is skipped with a warning and resolution falls through to the normal provider scan. There is no `[image.vision] provider` key; that section configures the Gemini backend only.

> **Pinning generation to a provider:** `[providers.fallback] generation = ["name"]` is the same mechanism for `generate_image`, resolved over each provider's `generation_model` instead of `vision_model`. Order per request: the session's current provider, then this chain, then the global Gemini `[image.generation]` section strictly last β€” and only that Gemini leg is gated by `image.generation.enabled`; a provider route registers the tool even with the flag off. Custom providers need an explicit `base_url` (never guessed); any OpenAI-compatible `/images/generations` endpoint works (OpenRouter, Together, DashScope/Qwen-Image, vLLM, …). Quick setup for the active provider: `/image generation `.

**Diagnostic:** when vision is unavailable for any reason, `is_vision_available` logs the exact cause at INFO level in `~/.opencrabs/logs/opencrabs.YYYY-MM-DD` β€” search for `target=vision`.

#### Context window & auto-compaction (effectively unlimited memory)

OpenCrabs never makes you start a fresh session to "clear context." Instead it auto-compacts: as a session's history approaches the model's context window, it summarizes the older turns in place and keeps going. Two tiers:

- **65% β€” soft trigger:** spawns a background LLM compaction that summarizes history back down to ~65% of the budget. Non-blocking β€” the conversation keeps streaming.
- **90% β€” hard trigger:** synchronous compaction before the next request, so a single turn can never overflow the window.

The triggers are **percentages of the effective window**, so they scale to whatever you set: at the 200k default compaction kicks in around 130k; at a 1M window, around 650k.

It's **transparent** β€” most of the time you won't notice it happen at all. Occasionally you'll catch a brief inline notice while it summarizes (the agent tends to mention it dynamically, in its own voice), then the conversation carries on with the older turns condensed. You never have to start over or manually clear anything.

**The budget defaults to 200,000 tokens** β€” the battle-tested sweet spot: large enough for long sessions, small enough to keep each request fast and cheap. Override it **per provider** in `config.toml`:

```toml
[providers.xiaomi]
context_window = 1000000 # raise the budget; compaction still triggers at 65% / 90% of it

[providers.anthropic]
context_window = 1000000 # native providers too β€” Anthropic, Gemini, and the CLI providers
```

This override works for **every provider** β€” OpenAI-compatible (custom, xiaomi, qwen, openrouter, minimax, …) and the native Anthropic, Gemini, and CLI providers. A provider with no override inherits the 200k default. The one exception is the **built-in OpenCrabs provider**, whose window is fixed at 200k: it is free and serves one model, so `context_window` is ignored there, both under `[providers.opencrabs]` and in any `[providers.custom.*]` section pointed at `inference.opencrabs.com`.

> **Sizing guidance.** 200k is the battle-tested sweet spot for essentially **every cloud model**. Going bigger has two real downsides β€” **cost** and **context loss**:
>
> - **Cost** is the lesser one, and it's softened by caching: OpenCrabs uses prompt caching across every caching-capable provider (currently averaging ~87% efficiency), and a long context is mostly an unchanged prefix served from cache β€” so a bigger window costs far less than the raw token count suggests.
> - **Context loss** is the one to watch: most models degrade as the window fills β€” they lose track of the middle and recall less reliably. Only the latest SOTA models hold large context robustly: closed (Opus 4.7 / 4.8, Fable 5, GPT-5.5, Gemini 3.1, …) or open (Qwen 3.7, Kimi K2.7, MiMo V2.5, GLM 5.2, DeepSeek V4, and the newer releases that keep coming from these and other labs). So raise `context_window` mainly on those frontier models; on anything older or smaller, staying near 200k keeps answers sharper.
>
> **Local models want less:** 128k is a good sweet spot β€” go **lower** if your machine is tight on resources or you start noticing hallucinations/fabrications, and **higher** only if you have more than 32GB of RAM and have tested your model at a larger window. Not sure what fits your setup? Reach out to Adolfo for suggestions/support, or open a [GitHub discussion](https://github.com/opencrabs/opencrabs/discussions).
>
> **Leave auto-compaction on** β€” it's been battle-tested over months and needs no babysitting. Only run a *manual* compaction (the `/compact` command) if you have a specific, strong reason to summarize early; otherwise let it manage itself.

#### Prompt caching (every caching-capable provider)

A long context is mostly a **stable prefix** β€” system prompt, brain files, earlier turns rarely change between requests. OpenCrabs caches that prefix wherever the provider supports it, so you pay full price for it once and a fraction on every reuse. Across real usage it's currently averaging **~87% cache efficiency**, which you can watch live in the **Cache Efficiency** card of `/usage`. This is the main reason a larger `context_window` costs far less than its raw token count suggests.

How it turns on depends on the provider:

- **Anthropic** β€” native prompt caching, on by default: the `cache_control: ephemeral` markers and the caching beta header are added automatically to the system prompt and tools.
- **OpenAI / OpenAI-compatible** β€” OpenAI caches automatically server-side. **OpenRouter caches by default too** β€” OpenCrabs enables it automatically, so there's no flag to discover; set `cache_enabled = false` only if you specifically want to opt out.
- **Qwen / Alibaba** β€” auto-enabled, zero-config (detected by endpoint or a `qwen-` model name; unlocks Alibaba's explicit context cache, ~90% off on hits). See the Qwen note further below.
- **Xiaomi (MiMo)** β€” caches automatically server-side; nothing to configure.

```toml
[providers.openrouter]
enabled = true
# Caching is ON by default β€” uncomment only to opt out:
# cache_enabled = false
```

**About the TTL β€” it's system-controlled.** The prompt-prefix caching that drives that ~87% (Anthropic native, Qwen/Alibaba, Xiaomi) uses a **provider-fixed 5-minute TTL that OpenCrabs does not expose** β€” you can't change it, and you don't need to. It **renews on every cache hit**, so in an active session each message keeps the prefix warm and it survives the *whole* session, however long, as long as you're not idle for more than 5 minutes; only an idle gap longer than that expires it, and the next message simply re-creates it once. The short TTL is the safeguard: a long TTL would keep the cache **alive on the provider's servers during idle time**, which you pay to keep stored β€” exactly how caching bills run away (an idle cache left alive for days can rack up serious charges). Keeping it fixed protects everyone, especially non-technical users, by default.

The one user-settable TTL is `cache_ttl` (default 300s, range 1-86400), which sets OpenRouter's cache TTL via the `X-OpenRouter-Cache-TTL` header. OpenRouter caching is on by default with this conservative 300s; it does **not** touch the prompt-prefix caches above. Leave it at the default unless you specifically understand the cost tradeoff of a longer TTL (a longer one keeps the cache alive longer at standing cost).

#### Changing provider settings: edit the file, or just ask

`vision_model`, `context_window`, provider keys, allowlists β€” any setting β€” can be changed two ways, and **neither needs a restart**:

- **Edit `config.toml` / `keys.toml` directly.** A file watcher hot-reloads on save, so the running TUI *and* the headless daemon both pick the change up on the next message β€” provider swap, tool re-registration, context budget, commands, and skills all update live.
- **Ask OpenCrabs in natural language** β€” e.g. *"set my context window to 1M"*, *"use mimo-v2-omni for vision on xiaomi"*, *"add my OpenRouter key"*. It writes the change through the `config_manager` tool (`write_config`), and if the automatic hot-reload doesn't pick the save up for any reason, it can run `config_manager reload` to force a fresh load from disk on the spot.

### Messaging Integrations
| Feature | Description |
|---------|-------------|
| **Telegram Bot** | Full-featured Telegram bot β€” owner DMs share TUI session, groups get isolated per-group sessions (keyed by chat ID). Photo/voice support (STT transcribes incoming voice notes; TTS replies as OGG/Opus voice notes via `send_voice` when input was audio). Allowed user IDs, allowed chat/group IDs, per-group allow lists (`[channels.telegram.groups.]`), `respond_to` filter (`all`/`dm_only`/`mention`/`auto`, global or per-group). Passive group message capture β€” all messages stored for context even when bot isn't mentioned |
| **Telegram Userbot (experimental)** | Feature-gated, opt-in, receive-only MTProto companion. Experimental: merged from #1209 without a maintainer-side live login yet; expect rough edges. Local QR/code/2FA login; allowlisted text is passively stored under `telegram-userbot` for explicit retrieval through `channel_search`. An operator can also pull a document out on demand with `opencrabs channel userbot-download [--outdir DIR]` (MTProto streams up to 2 GB where the Bot API caps at 20 MB), `--list` shows recent document messages, and the uploader's filename is reduced to its final component so a download cannot land outside `--outdir`. Empty `allowed_chats` is dry mode. It does not invoke the agent or send/edit/react as the user. |
| **WhatsApp** | Pair by scanning a QR code from the TUI: first-run onboarding, or `/channels` then select WhatsApp. The QR is shown in the terminal. You run the bot AS whatever account you scan: your own number (talk via "Message Yourself") or any other number you own, including a WhatsApp Business account, to serve that account's incoming DMs. `response_policy` (`auto`/`owner_only`/`allowlist`/`open`) decides who it answers; the paired account's self-chat and `bot_owner` operator are always allowed. Streaming edits ONE living message in place rather than posting a chunk per step, and a finished multi-step turn is acknowledged with a reaction. Inbound: text, image, video, audio, document, sticker, location, contact card, reactions and poll votes (decrypted and resolved back to the option labels). Media whose CDN URL has expired is recovered through a server re-upload rather than reported as a failed download. Outbound audio goes out as a native voice note (`ptt`) with a recording indicator, pairing directly with built-in `local-tts`. Beyond messaging: block / unblock / list blocked contacts, disappearing messages (per message or channel-wide), pin and unpin chats, forward a message the session has seen, set the profile name and status, post status updates, discover followed newsletters, and create and assign labels. Every action that delivers a message is charged to an outbound send budget. Tool-approval prompts can optionally be sent as interactive buttons (`interactive_buttons`, off by default). Follow-up suggestion sets that exceed the native button cap render as a poll instead, and a vote selects the option (#1616). Per-phone sessions, session persists across restarts |
| **Discord** | Full Discord bot β€” text + image + voice. Owner DMs share TUI session, guild channels get isolated per-channel sessions. Allowed user IDs, allowed channel IDs, `respond_to` filter. Tool calls render as ONE grouped message per turn, collapsed to a summary with an Expand/Collapse button, edited in place as tools run β€” Slack parity. Intermediate narration folds into the same bubble as dim subtext lines (`trace_narration`, default true). Reacting to a bot message becomes an agent turn (approval emoji = keep going with a silent react-back, stop emoji = pause and ask), and the agent reacts back via its `<>` marker. Multiple generated files batch into one gallery-style message. Slash commands are native: `commands.toml` is projected onto Discord's command list per guild with argument hints (#1850, needs the `applications.commands` invite scope). Interactive components: select menus (`discord_send` with `action=select_menu`), modal forms (`action=modal`), component TTL with auto-cleanup, role-based access control, forum thread creation. Live reply tracing with auto-threading (tool turns stream into a linked thread; tables render as native Discord markdown tables) and double-post dedup on tool turns (#1603, #1608). Full proactive control via `discord_send` (17 actions): `send`, `reply`, `react`, `unreact`, `edit`, `delete`, `pin`, `unpin`, `create_thread`, `send_embed`, `get_messages`, `list_channels`, `add_role`, `remove_role`, `kick`, `ban`, `send_file`. Generated images sent as native Discord file attachments |
| **Slack** | Full Slack bot via Socket Mode β€” owner DMs share TUI session, channels get isolated per-channel sessions. Text + image + voice (STT transcribes incoming audio attachments; TTS replies upload an OGG/Opus audio file via Slack's external upload flow β€” renders inline with waveform UI β€” when input was audio and `tts_enabled=true`). Allowed user IDs, allowed channel IDs, `respond_to` filter. Tool calls render as ONE grouped message per turn, collapsed to a summary with an Expand/Collapse button (Block Kit), edited in place as tools run β€” Telegram parity. Reacting to a bot message becomes an agent turn (approval emoji = keep going with a silent react-back, stop emoji = pause and ask), and the agent reacts back via its `<>` marker. All file uploads (generated docs/images, TTS audio) use Slack's supported external upload flow (`files.getUploadURLExternal` + `completeUploadExternal`) with real MIME types. Full proactive control via `slack_send` (17 actions): `send`, `reply`, `react`, `unreact`, `edit`, `delete`, `pin`, `unpin`, `get_messages`, `get_channel`, `list_channels`, `get_user`, `list_members`, `kick_user`, `set_topic`, `send_blocks`, `send_file`. Generated images sent as native Slack file uploads. Bot token + app token from `api.slack.com/apps` (Socket Mode required). **Required Bot Token Scopes:** `chat:write`, `channels:history`, `groups:history`, `im:history`, `mpim:history`, `users:read`, `files:read`, `files:write`, `reactions:write`, `app_mentions:read` |
| **Trello** | Tool-only by default β€” the AI acts on Trello only when explicitly asked via `trello_send`. Opt-in polling via `poll_interval_secs` in config; when enabled, only `@bot_username` mentions from allowed users trigger a response. Full card management via `trello_send` (22 actions): `add_comment`, `create_card`, `move_card`, `find_cards`, `list_boards`, `get_card`, `get_card_comments`, `update_card`, `archive_card`, `add_member_to_card`, `remove_member_from_card`, `add_label_to_card`, `remove_label_from_card`, `add_checklist`, `add_checklist_item`, `complete_checklist_item`, `list_lists`, `get_board_members`, `search`, `get_notifications`, `mark_notifications_read`, `add_attachment`. API Key + Token from `trello.com/power-ups/admin`, board IDs and member-ID allowlist configurable |

#### File & Media Input Support

When users send files, images, or documents across any channel, the agent receives the content automatically β€” no manual forwarding needed. Example: a user uploads a dashboard screenshot to a Trello card with the comment _"I'm seeing this error"_ β€” the agent fetches the attachment, passes it through the vision pipeline, and responds with full context.

| Channel | Images (in) | Text files (in) | Documents (in) | Audio (in) | Audio reply (out) | Image gen (out) |
|---------|-------------|-----------------|----------------|------------|-------------------|-----------------|
| **Telegram** | βœ… vision pipeline | βœ… extracted inline | βœ… / PDF note | βœ… STT | βœ… TTS via `send_voice` (OGG/Opus) | βœ… native photo |
| **WhatsApp** | βœ… vision pipeline | βœ… extracted inline | βœ… / PDF note | βœ… STT | βœ… TTS via upload + `audio_message` (OGG/Opus, `ptt=true`), with a live recording indicator | βœ… native image |
| **Discord** | βœ… vision pipeline | βœ… extracted inline | βœ… / PDF note | βœ… STT | βœ… TTS as `response.ogg` attachment | βœ… file attachment |
| **Slack** | βœ… vision pipeline | βœ… extracted inline | βœ… / PDF note | βœ… STT | βœ… TTS via external upload flow (OGG/Opus, inline waveform) | βœ… file upload |
| **Trello** | βœ… card attachments β†’ vision | βœ… extracted inline | β€” | β€” | β€” | βœ… card attachment + embed |
| **TUI** | βœ… paste path β†’ vision | βœ… paste path β†’ inline | β€” | βœ… STT | β€” (terminal has no native audio) | βœ… `[IMG: name]` display |

Images are passed to the active model's vision pipeline if it supports multimodal input, or routed to the `analyze_image` tool (Google Gemini vision) otherwise. Text files (`.txt`, `.md`, `.json`, `.csv`, source code, etc.) are extracted as UTF-8 and included inline up to 8 000 characters β€” in the TUI simply paste or type the file path.

Videos uploaded on any channel (mp4, m4v, mov, webm, mkv, avi, 3gp, flv) auto-route to `analyze_video` when `image.vision.enabled = true` with a Gemini API key. The TUI also detects pasted video paths and labels them `Video #N` in the attachment indicator. Provider-side limits to keep in mind: Gemini's inline-bytes mode caps at ~20 MB (we use ≀18 MB), and the resumable Files API supports up to 2 GB / ~1 hour videos. Channel-side limits are tighter β€” Telegram's Bot API hard-caps `getFile` downloads at **20 MB** even though chats accept larger uploads, so videos over that size will get a friendly "compress to under 20 MB and resend" reply. Slack file downloads use the bot token (`files:read` scope) and inherit the workspace's per-file upload cap. Frame-extraction fallback for non-Gemini providers is not yet wired β€” without a Gemini key, video uploads return an "unsupported" notice.

#### Telegram rich message formatting

When a Telegram reply carries structured Markdown (tables, headings, lists, `- [ ]` task lists, fenced code, or math), OpenCrabs can render it natively using Telegram's rich messages (Bot API 10.1) β€” real tables, real section headings, real checkboxes β€” instead of plain text or basic HTML.

This is **on by default** via `channels.telegram.rich_messages`. The one caveat: native rich messages are unreadable on **Telegram Web and older clients** β€” those show a "this message is not supported, update Telegram" placeholder, and the rich API has no text fallback. If your audience runs outdated clients, disable it in the onboarding dialog (the "Rich text experience" checkbox) or ask the agent: `/channels telegram richtext off`. With the flag off, the universal HTML rendering is used β€” tables come out as aligned monospace grids, task items as `☐`/`β˜‘`, with proper paragraph spacing, so structured replies look decent on every client.

When enabled, native rich applies to the agent's reply (sent as a fresh rich message so it renders cleanly) and to proactive `telegram_send` messages. Plain-prose replies are left untouched, so incidental characters like a stray `*` or `#` are never reinterpreted. If the rich send fails for any reason, OpenCrabs falls back silently to HTML, so a message is never dropped.

Referenced markdown files (`.md`, `.markdown`) are a special case: they ship as detached documents by default, because some clients (notably Telegram Android) cannot open a markdown document inlined in a rich message. Set `channels.telegram.inline_markdown_documents = true` to inline them into rich intermediates at the reference instead.

**Flow logs** (processing-log messages showing tool calls and intermediate text) also use the rich API when enabled, supporting 32K characters instead of HTML's 4K limit. Long tool chains fit in a single message without splitting. If the rich send fails, flow logs fall back to HTML rendering. The block auto-freezes at 30K characters to stay within limits.

#### Discord tool card

On **Discord**, each turn's tool calls are one message. Collapsed, it shows the summary: the tool count, how many are running or failed, the context budget, tokens per second and the elapsed time. **Expand** opens the card:

- **Five rows per page**, with previous and next buttons. Nothing is dropped: the footer states the page and the call total.
- **Repeats collapse.** Identical consecutive calls show as one row with `Γ—N`, such as `cargo check Γ—3`. Non-adjacent repeats stay separate.
- **A colored bar.** Yellow while a tool runs, red when one failed, green when everything finished clean. The header names the tool that is running.
- **Log** sends the full list, every call uncollapsed, to you as `tool-calls.md`. Only you see it.

Each completion finishes its own call, matched by the provider's call id, so parallel calls of one tool no longer leave a finished row showing a running icon. Cards are stored in SQLite, so after a restart their buttons still work. Telegram keeps its own tool block.

#### /cowork β€” workspace creation (Telegram) and channel opening (Discord)

On **Discord**, `/cowork` (owner only, typed in a channel or thread, or picked from the `/` menu) opens that channel or thread to its members. It writes `[channels.discord.channels.]` with `open = true`, `respond_to = "all"` and the channel's `name`. Members are not registered anywhere: `open` admits them, threads and forum posts included, and DMs and other channels stay locked. Because the owner's `/cowork` and `/respond_to` pass the channel scope, the owner can open or fix a channel from inside it even when it has no entry yet. Run `/cowork` in a server channel; in a DM it refuses.

The Telegram flow below creates a team workspace directly from Telegram. It is **Telegram-only** because it relies on Telegram-specific primitives: group creation via `?startgroup` deep links, invite links, QR codes from `t.me` URLs, and `new_chat_members` service messages for auto-registration. None of these exist in Discord, Slack, or WhatsApp.

**Prerequisite:** Telegram must be configured (bot token set via `/channels telegram` or manual `config.toml` setup).

**Flow:**
1. Owner sends `/cowork` in DM (owner-only command) β†’ bot replies with an **Add to Group** inline button
2. Owner taps it β†’ Telegram's native group picker opens. The deep link requests admin rights inline (`?startgroup=cowork_&admin=invite_users+delete_messages+pin_messages+manage_chat`), so the bot is **added already promoted to admin** β€” no manual promotion step. Always keep the bot as admin: an admin bot reads every message regardless of privacy mode and can create invite links.
3. On joining via cowork, the bot sets that group's `open = true` (persisted) so **every member is allowed** β€” existing and new, no per-user step β€” and posts a short welcome. If it somehow landed without admin, the welcome also nudges the owner to promote it.
4. **Members are auto-registered in an open group:** joining members are added to the group's own allowlist (`[channels.telegram.groups.].allowed_users`) on join, and anyone who was already in the group before the bot can send `/start` to be tracked. This is group-scoped only β€” members can chat in that group (`@mention` the bot) but cannot DM it privately unless also on the global `allowed_users` or `bot_owner`. Already in the group and want to open it without re-adding the bot? Send `/cowork` inside the group (owner-only).
5. **`/start` in a DM never auto-registers** (DMs are invite-only): the bot just returns the sender's Telegram ID so they can share it with the owner to be added (or add it to `config.toml` when self-hosting). `/start` in a non-open group likewise returns the ID and points the user to ask the owner to run `/cowork`. The owner's own `/start` in a group is silent β€” they are already allowed everywhere.

**Cross-channel behavior:** `/cowork` works from any surface. In Telegram DMs, the native flow activates directly. From the TUI, Discord, Slack, or WhatsApp, the agent calls the `cowork_connect` tool which mints a session, registers it with the bot, and returns the `t.me` deep link plus a scannable QR code PNG. The TUI shows the clickable link; channels deliver the QR as a photo.

#### Telegram group security model

When `allowed_users` is configured, the bot enforces a strict allowlist on all incoming messages. The behavior differs between DMs and groups:

**In DMs:**
- Non-allowed users always get a reply: *"You are not authorized. Send /start to get your user ID."* β€” so they know what to do.

**In groups:**
- Non-allowed users get **silently dropped** (no reply, no processing) for normal messages.
- If the user explicitly **@mentions** or **replies to** the bot, they get the "not authorized" reply β€” so they know they need to be added.
- `/start` in an **open** group (`open = true`, see [Per-group access control](#per-group-access-control-per-chat-acl)) registers the sender into that group's allowlist and confirms. In a non-open group it returns the sender's ID and tells them to ask the owner to run `/cowork` or add them β€” it never silently self-adds. The owner's `/start` (which Telegram auto-fires when the bot is added) is silent.

This prevents the bot from spamming "not authorized" in active groups where most members aren't on the allowlist. The bot only engages with non-allowed users when they explicitly reach out.

**Config:**
```toml
[channels.telegram]
allowed_users = ["123456789"] # Only these users can interact
respond_to = "mention" # Bot only responds to @mentions in groups
silence_group_start = true # Silently ignore /start from non-allowed users in groups
```

#### Telegram guest queries

Telegram's Guest Mode (Bot API 10.0) lets someone tag your bot in a chat it is **not** a member of, and bot API method `answerGuestQuery` posts one reply right there. OpenCrabs supports it, off by default:

```toml
[channels.telegram]
guest_queries = true # default false: guest tags are ignored
```

A guest reply answers a stranger in a chat you were never invited to, so the gate is deliberately narrow. The caller must be the `bot_owner`, an admin (`allowed_users`), or listed for **that** chat (`[channels.telegram.groups.].allowed_users`), and only when `guest_queries = true`. A group's `open = true` grants nothing here: it opens that group to the bot, it does not authorize answering guest tags from its members. Refused queries are logged with `guest_query_id`, the caller and the chat; nothing is posted.

What the guest turn sees and sends:

- Only the tagged message. No history from the chat, and nothing from the guest turn enters that group's history either.
- A single reply, posted as soon as the turn starts and then **edited in place** while it runs: the caller watches the same status block a regular chat shows, with the tools the agent is using, the tool count, the context budget and the settled footer. `answerGuestQuery` returns the reply's `inline_message_id` and every later edit goes through `editMessageText` with that id, which is Telegram's own recommendation for this ("reporting its progress back to the chat or group"). Sent with `parse_mode=HTML` so the block keeps its formatting.
- Clamped to Telegram's 4096-character limit, counted the way Telegram counts it (UTF-16 units, so an emoji is 2). An overlong answer is cut and marked, and if the block would leave too little room for a real answer the answer ships alone.
- Secrets are scrubbed, always: a guest reply goes to a chat you do not control, so it never takes the DM branch that shows credentials.
- Channel directives are stripped, so a local file path can never leak into a stranger's chat.
- No typing indicator is possible (the bot is not a member of the chat, so there is nothing to type in), and no per-tool messages: the API gives one reply per summon.
- Tools that need approval are denied: approvals are buttons in a chat, and the caller is not the machine's owner.
- The tool surface is **scoped**, not the owner's. A guest turn may only reach `web_search`, `exa_search`, `brave_search`, `read_file` and `tool_search`; anything else is refused before it runs, and `tool_search` will not advertise it. `read_file` stays inside the session working directory, so an absolute path elsewhere on disk, or a symlink pointing out of it, is refused. The allowlist is default-deny on purpose: a tool added in a later release stays out of a stranger's reach until it is listed.

Guest queries land on the raw `getUpdates` layer (`allowed_updates` includes `guest_message`), the same path `message_reaction` used before teloxide grew a typed variant for it. The pinned `teloxide-core` has no guest variant, so a guest update is never dispatched as a normal message.

#### Bot owner and owner-only commands

Every channel has a `bot_owner` field (`[channels.telegram]`, `[channels.discord]`, `[channels.slack]`, `[channels.whatsapp]`, `[channels.trello]`). It names the user ID(s) (phone for WhatsApp) treated as the bot owner. On first-run setup the owner is seeded automatically from the first entry in your allow list (`allowed_users`, or `allowed_phones` for WhatsApp), and existing configs are migrated on load. Set `bot_owner` explicitly to pin the owner instead of relying on list order.

The owner gets access that other allowlisted users do not. All channel commands except `/new` are owner-only: `/compact`, `/clear`, `/doctor`, `/evolve`, `/help`, `/models`, `/rtk`, `/sessions`, `/stop`, `/usage`, `/profiles`, `/goal`, `/mission-control`, `/rename`, `/cd`, `/respond_to`, `/redact`, `/restart`, `/exit`, `/architecture`, `/attach`, `/audit`. `/new` stays open for session recovery (bugged/hallucinated sessions). Non-owners who try get a short "owner only" notice.

**Deny-by-default access model (all channels):** if neither `allowed_users` (nor `allowed_phones`/`allowed_roles`) nor `bot_owner` is configured, the bot refuses all interactions β€” unconfigured installs are locked down by default on Telegram, Discord, Slack, and WhatsApp alike. Set at least one to unlock access. This prevents open-mode footguns on fresh deployments.

```toml
[channels.telegram]
allowed_users = ["123456789"] # who may interact
# bot_owner = ["123456789"] # owner for owner-only commands (auto-seeded from allowed_users[0])
```

#### Flood governor (`[channels.telegram.rate_limiter]`)

Telegram rate-limits per peer, and a forum supergroup is **one peer no matter how
many topics it has**. Under load a busy turn can outrun that on its own, so the
governor paces outbound calls proactively instead of only reacting to 429s.

Enforcement engages **only for forum peers** (a chat seen carrying a topic id).
DMs are never paced, so leaving this alone is the right default; the knobs exist
for deployments whose traffic differs from the one the defaults were sized on.

```toml
[channels.telegram.rate_limiter]
enabled = true # master switch; forum peers only either way

typing_min_interval_secs = 3 # sendChatAction: min gap between actions
typing_burst = 8 # ...and how many may bunch up
typing_max_hold_secs = 30 # longest a typing indicator is held

edits_per_minute = 30 # classic editMessageText budget
edit_burst = 10

rich_per_minute = 30 # sendRichMessage budget (separate bucket)
rich_burst = 10

send_min_interval_millis = 1000 # new messages: min gap
sends_ceiling_per_minute = 18
sends_burst = 5

summary_log_secs = 300 # how often the admitted/dropped summary is logged
```

When a bucket empties, what happens depends on what is queued. **Chrome** (the
clock, brain previews, status churn) is dropped and counted: it self-heals on the
next full-state render, so nothing is lost. A **final** message is never dropped;
it queues latest-wins and lands when the bucket refills. Every value is read live
on each gate evaluation, so edits take effect without a restart.

The defaults follow Telegram's documented bot regime per peer: roughly 20 typing
actions per 5s and 40 per 30s, edits observed safe at 30/min, sends kept under
~20/min.

#### Per-group access control (per-chat ACL)

Telegram groups can have their own member list, so a user can be allowed in **one group** without gaining DM access:

- `allowed_users` (channel level) β€” **admins**: may DM the bot and act in any chat.
- `bot_owner` β€” the **owner**: always allowed everywhere.
- `[channels.telegram.groups.].allowed_users` β€” allowed in **that group only**. These users are refused in DMs unless they are also an admin or the owner, which closes the "DM the bot privately to escape group oversight" bypass.
- `[channels.telegram.groups.].open` β€” **per-group blanket allow** (default `false`). When `true`, *any* member of that group passes the group ACL without being individually listed, and joining members / members who `/start` are auto-registered into `allowed_users` so there's a visible roster. DMs and every other group stay locked. This is the ONLY switch that relaxes group access β€” it is **per-group, never global**, and defaults off so the bot is secure by default. There is no global `open`.
- `[channels.telegram.groups.].name` β€” the group's title, **recorded automatically** so config is readable rather than a wall of chat ids. Written on the group's next message and refreshed when the group is renamed; only for groups that already have a section, so the bot never adds one for a room you didn't configure. Purely a label: the ACL keys off the chat id and never reads it. Safe to edit or delete by hand (it comes back on the next message).

DMs are gated to admins + owner. If neither `allowed_users` nor `bot_owner` is set, the bot refuses all interactions (deny-by-default). Set at least one to unlock access. Each group can also override `respond_to` just for itself.

```toml
[channels.telegram]
allowed_users = ["111"] # admins: DM + any chat
respond_to = "mention" # global default

[channels.telegram.groups.-1001234567890]
name = "Release Crew" # recorded from Telegram; a label, never part of the ACL
allowed_users = ["222", "333"] # allowed in this group only, never via DM
respond_to = "all" # per-group override of the global respond_to
open = true # any member of THIS group is allowed (blanket, per-group)
```

`respond_to` accepts `all`, `mention`, `dm_only`, or `auto` (reply to all while there is at most one active sender, then switch to mention-only once a second unique sender appears).

**Discord channels work the same way.** Each channel (or forum/thread parent) can have its own entry:

```toml
[channels.discord.channels.1473207147025137778]
name = "general" # display only
respond_to = "all" # this channel's mode; unset inherits the global respond_to
open = true # any member of THIS channel is allowed, threads and forum posts included
```

A channel with its own entry passes the channel scope without an `allowed_channels` entry. A thread or forum post takes any setting it does not set from its parent channel, and its own setting wins over the parent's. `open` never admits anyone while the bot has no `allowed_users`, `allowed_roles` or `bot_owner`, and DMs and other channels stay locked. `auto` on Discord currently behaves as `mention`. The owner can run `/respond_to` inside a channel or thread to set that entry; the command reports the mode that applies there.

**`/cowork` opens a group.** Running `/cowork` (owner-only) is the explicit, owner-initiated action that sets that group's `open = true` (persisted, until you change it): either by adding the bot to a group via the cowork deep link, or by sending `/cowork` inside a group the bot is already in. Once open, every member (existing and new) is allowed and tracked in the group's `allowed_users` β€” no per-user `/start` needed β€” while DM access stays closed. Auto-registration only happens in open groups; a group you never `/cowork` (or set `open = true` on) stays secure by default and admits no one automatically.

#### Voice and file pickup in groups

In mention-only groups (`respond_to = "mention"`), users can now share files and voice messages even when the bot isn't directly tagged in the same message. Here's how it works:

1. **Fire-and-forget file capture** β€” The bot downloads ALL incoming voice, video, document, and audio files from group messages to `~/.opencrabs/tmp/`, regardless of whether the bot was mentioned. This happens silently in the background.
2. **Tag-then-ask** β€” A user sends a voice message, then tags the bot in a follow-up message (e.g. `@bot what did I just say?`). The bot scans the tmp directory for recent voice files from that chat (5-minute window), transcribes the most recent one, and prepends the transcript to the user's message.

This solves the core UX problem in mention-only groups: previously, tagging the bot in the *same* message as a voice note didn't work because Telegram sends voice and text as separate messages.

**Supported file types:** `.ogg` (voice notes), `.mp4` (video notes), documents, and audio files.

### Terminal UI
| Feature | Description |
|---------|-------------|
| **Cursor Navigation** | Full cursor movement: Left/Right arrows, Ctrl+Left/Right word jump, Home/End, Delete, Backspace at position |
| **Input History** | Persistent command history (`~/.opencrabs/history.txt`), loaded on startup, capped at 500 entries |
| **Inline Tool Approval** | Claude Code-style `❯ Yes / Always / No` selector with arrow key navigation |
| **Inline Plan Approval** | Interactive plan review selector (Approve / Reject / Request Changes / View Plan) |
| **Session Management** | Create, rename, delete sessions with persistent SQLite storage; each session remembers its provider + model β€” switching sessions auto-restores the provider (no manual `/models` needed); token counts and context % per session. New sessions auto-generate a meaningful title from the first user message (no more "New Chat") |
| **Direct Model Switch** | `/models ` switches the current session instantly β€” no picker β€” on the TUI and every channel. You can also name it the way you say it: `/models xiaomi mimo v2.5 pro` resolves the same as `/models xiaomi/mimo-v2.5-pro`, with spacing, hyphens, dots and case interchangeable. Matching is programmatic against the provider's own catalogue, so it costs no model call and never invents a model that provider doesn't serve; an ambiguous reference is refused with the candidates listed rather than guessed. Add `all` (`/models minimax/MiniMax-M3 all`) to apply to every non-archived session (Telegram also offers an inline "Apply to all sessions" button). `opencrabs session set-model` does the same from the terminal, and `[providers.] force_default = true` pushes the section's default pair to all sessions on config reload |
| **Theme Switching** | `/theme` opens an interactive picker β€” arrow through the roster with **live preview** (the whole UI recolors under the cursor), Enter applies + persists, Esc reverts. 8 hand-built presets (`crab-dark` default, dracula, alucard, monokai, solarized-light, solarized-dark, catppuccin-mocha, catppuccin-latte) plus a 31-theme curated pack converted from the opencode and alacritty-theme catalogs (tokyonight + storm, nord, gruvbox + light, kanagawa, everforest, rosepine + dawn, catppuccin-macchiato/frappe, github, material, palenight, zenburn, aura, ayu, and more β€” `/theme list` shows the full roster), plus your own presets: drop a TOML in `~/.opencrabs/themes/*.toml` and it hot-loads into the picker, and `cargo run --example theme_pack_gen -- ` (the generator that built the curated pack) converts any alacritty-theme TOML or opencode theme JSON into a ready-to-drop file with a provenance header, including `#RGB`/`#RRGGBBAA` colors, defs refs and dark/light variants; fetch loop and curation notes live in `src/tui/theme_catalog/pack/README.md`. `/theme list`, `/theme set `, `/theme reset` keep the text surface; the active theme persists via the `[tui.theme]` config key and survives restarts. Every widget resolves its colours through the theme engine, with no raw ANSI remaining (a suite-level guard fails on any new raw site), and themes can declare an optional canvas background (#1634). Non-truecolor terminals automatically get an ANSI-mapped fallback tier instead of broken colors |
| **Split Panes** | Horizontal (`\|` in sessions) and vertical (`_` in sessions) pane splitting β€” tmux-style. Each pane runs its own session with independent provider, model, and context. Run 10 sessions side by side, all processing in parallel. `Tab` to cycle focus, `Ctrl+X` to close pane |
| **Parallel Sessions** | Multiple sessions can have in-flight requests to different providers simultaneously. Send a message in one session, switch to another, send another β€” both process in parallel. Background sessions auto-approve tool calls; you'll see results when you switch back |
| **Scroll While Streaming** | Scroll up during streaming without being yanked back to bottom; auto-scroll re-enables when you scroll back down or send a message |
| **Path Normalization** | Home paths (`/home/user/...`) automatically collapsed to `~` in system prompt, tool call display, and brain files β€” keeps context lean and readable |
| **Recent File Memory** | Agent remembers recently accessed file paths across sessions β€” no need to re-specify paths you were just working on |
| **Compaction Summary** | Auto-compaction shows the full summary in chat as a system message β€” see exactly what the agent remembered |
| **Syntax Highlighting** | 100+ languages with line numbers via syntect |
| **Markdown Rendering** | Rich text formatting with code blocks, headings, lists, and inline styles |
| **Tool Context Persistence** | Tool call groups saved to DB and reconstructed on session reload β€” no vanishing tool history |
| **Expand / Collapse Blocks** | Click a tool-call group or a **Thinking** block, or press `Ctrl+O`, to expand it. Reasoning cycles through three states so a long thought never floods the view: **collapsed β†’ capped** (first ~10 lines + a "… N more (click / ctrl+o for full)" hint) **β†’ full β†’ collapsed**. Tool-call groups toggle expand/collapse (on Discord, the card pages through its rows; see *Discord tool card*). A plain **click** toggles the block under the cursor; a **click-and-drag** selects text to copy (even over a collapsible block) instead of expanding it |
| **Multi-line Input** | Alt+Enter / Shift+Enter for newlines; Enter to send |
| **Abort Processing** | EscapeΓ—2 within 3 seconds to cancel any in-progress request |
| **Clipboard Image Paste** | Copy an image from a browser, screenshot tool, or any app and paste it directly into the input. Raw image bytes are read from the OS clipboard (macOS: osascript, Linux: wl-paste/xclip), written to a temp file, and attached through the existing image pipeline. No need to save to disk first. `Ctrl+V` in the chat input reads the clipboard directly and works in every terminal, including a screenshot copied to the clipboard (image data, no text). `Cmd+V` works where the terminal pastes or passes the key through; iTerm2 and Terminal.app swallow `Cmd+V` for an image-only clipboard, so use `Ctrl+V` there. While the chat input is focused on macOS, an unpasted screenshot in the clipboard also shows a vanishing `Image in clipboard - Ctrl+V to paste` hint inside the input area, like the other notices, until you paste or the clipboard changes |
| **File Drag & Drop** | Drag a file onto the TUI and the terminal inserts its path; OpenCrabs unescapes it and takes it from there. Images attach as vision content, text files (`.txt`, `.md`, `.json`, source code) are read from disk and inlined into the message, and PDFs surface a hint pointing the agent at `pdf_to_images` + `analyze_image`. Over SSH the dropped path names a file on the wrong machine; see [Dropping files into a TUI running on a VPS](#dropping-files-into-a-tui-running-on-a-vps) |
| **Bang Operator (`!cmd`)** | Run any shell command directly from the input β€” no LLM round-trip. Output is shown as a system message in the working directory context. Full-screen editors (`!vi`, `!vim`, `!nano`, `!emacs`) are the exception on Unix terminals: the TUI hands the real terminal to the editor, so you edit in place and return to the chat when it exits (a mid-edit Ctrl+Z ends the editor instead of hanging the TUI); on Windows, editors are pipe-captured like any other command |
| **Auto-Update** | Checks GitHub for new releases on startup and once every 24h in the background. When a new version is found it silently installs and hot-restarts. Disable via `[agent] auto_update = false` in `config.toml` to be prompted instead |

### Agent Capabilities
| Feature | Description |
|---------|-------------|
| **Full Terminal Access** | 30+ built-in tools (file I/O, glob, grep, web search, code execution, image gen/analysis, memory search, cron jobs) plus **any CLI tool on your system** via `bash` β€” GitHub CLI, Docker, SSH, Python, Node, ffmpeg, curl, and everything else just work |
| **RTK Token Savings** | Automatic bash output optimization via [RTK](https://github.com/rtk-ai/rtk) integration β€” enabled by default, zero config. Prepends `rtk` to supported commands (git, cargo, npm, pnpm, yarn, docker, kubectl, grep, find, ls, tree, curl, and 100+ more) to filter noise from command output. Reduces token usage on bash commands by 60-90% without losing critical information. Check savings with `/rtk` command. RTK binary bundled with prebuilt OpenCrabs releases and installed by `/evolve` on update; if it is ever missing, OpenCrabs auto-downloads the right binary for your platform on first use |
| **Per-Session Isolation** | Each session is an independent agent with its own provider, model, context, and tool state. Sessions can run tasks in parallel against different providers β€” ask Claude a question in one session while Kimi works on code in another |
| **Self-Healing** | Detects and recovers from phantom tool calls, gaslighting preambles, text repetition loops, XML tool call failures, and provider errors. Short-circuits repeated failing bash commands and rejects interactive commands that would hang. Near-match loop detection catches tool loops that differ only by a counter or whitespace across every tool except read_file, and reworded announcement loops both mid-turn and across turns, that exact-match guards miss (#957, #961). Fact detectors now cover **mixed iterations** as well: a fabricated claim riding in the same response as a real tool call is corrected in-turn instead of delivered, and a rustc diagnostic code asserted in prose is checked against the tool output that would have to vouch for it (#1693). Marker-less bare-participle announcements β€” "Reading them, with mtimes so I know each postdates the tree" β€” fire in English, Portuguese and Spanish (#1694). Automatic context compaction at 65% (soft) and 90% (hard). Sticky fallback promotion when primary recovers |
| **Self-Sustaining** | Agent can modify its own source, build, test, and hot-restart via Unix `exec()` |
| **Self-Improving** | Learns from experience β€” saves reusable workflows as custom commands, writes lessons learned to memory, updates its own brain files. All local, no data leaves your machine |
| **Autonomous /goal** | Set a goal with `/goal ` and the agent loops autonomously: executing, self-evaluating with an LLM judge, and continuing with a correction prompt until the goal is satisfied or the turn budget runs out. Supports `/goal pause`, `/goal resume`, `/goal status`, and `/goal clear` |
| **Dynamic Tools** | Define custom tools at runtime via `~/.opencrabs/tools.toml` β€” the agent can call them autonomously like built-in tools. HTTP and shell executors, template parameters (`{{param}}`), enable/disable without restart. The `tool_manage` meta-tool lets the agent create, remove, and reload tools on the fly |
| **Skills (cross-harness)** | Multi-stage workflow templates in the de-facto `SKILL.md` format used by Claude Code, Anthropic managed agents, and OpenClaw. Drop a `SKILL.md` under `~/.opencrabs/skills//` and it auto-registers as `/` β€” no `commands.toml` entry needed. Works in the TUI **and** every connected channel (Telegram, Discord, Slack, WhatsApp). Built-ins ship with the binary (always version-matched); user skills override by file presence. Two built-ins out of the box: `/security-audit` (language-agnostic CVE & static-analysis audit, scores 0-100) and `/cost-estimate` (codebase valuation with AI-assisted ROI). Same `SKILL.md` is portable across harnesses |
| **Mission Control** | Full-screen `/mission-control` dialog showing every actionable artifact in one place: pending RSI proposals (inbox cards), recent RSI activity (improvements log feed), the schedule queue (cron jobs + paused/active state), and a live **Analytics** panel (brain file sizes, tool usage with proportional bars, failure rates, RSI applied by dimension, phantom-detection and resolution rates, per-model reliability, stream-recovery counts) with **D / W / M / All** window tabs so a fixed 30-day view cannot hide a tool that has already recovered. Apply or reject inbox proposals inline with `a` / `r` β€” same machinery as the agent's `rsi_proposals` tool, byte-identical install. Tab between panels, j/k to navigate, Enter for the detail popup, Esc to close. Cron paused jobs flag in orange, active in teal β€” at-a-glance state |
| **Skills picker** | Full-screen `/skills` dialog with a live filter input β€” start typing to narrow the list (case-insensitive on name + description), Tab / Shift-Tab cycle the filtered cards (wraps at the edges), Enter runs the selected skill (sends its body as a prompt to the agent), Esc closes. Built-in skills badge orange; user-installed skills badge teal. When the filter narrows to a single match, Enter just fires it β€” fastest path to launch a skill |
| **Browser Automation** | Native browser control via CDP (Chrome DevTools Protocol). Auto-detects your default Chromium-based browser (Chrome, Brave, Edge, Arc, Vivaldi, Opera, Chromium) and uses its profile β€” your logins, cookies, and extensions carry over. 9 browser tools: navigate, click, type, screenshot, eval JS, extract content, wait for elements, find/inventory elements, batched multi-action. Headed or headless mode with display auto-detection. **Shadow DOM aware:** CSS/text/aria search, the interactive inventory, and click/type/act/wait/screenshot all resolve inside open shadow roots, and closed roots still resolve over CDP. **Note:** Firefox is not supported (no CDP) β€” if Firefox is your default, OpenCrabs falls back to the first available Chromium browser. Feature-gated under `browser` (included by default) |
| **ACP Server Mode** | Agent Client Protocol server over stdio JSON-RPC (#1540): editors and agent harnesses like Zed and [MonoCode](https://github.com/hardbeat920/monocode) drive OpenCrabs as their coding agent. `opencrabs acp` serves the session over stdio; prompts, tool calls and streaming updates ride the ACP session protocol. Sessions are first-class: the context meter is restored on load and rides usage updates, `session/load` replays the transcript and restores the per-session model, `set_model` persists across processes, native `session/set_mode` applies the approval policy server-side, `session/compact` pushes, and `session/new` offers a live model catalog |
| **Natural Language Commands** | Tell OpenCrabs to create slash commands β€” it writes them to `commands.toml` autonomously via the `config_manager` tool |
| **Mechanical Commands (#933)** | `/architecture [path]` (directory tree, depth-capped, secrets/vendor dirs excluded), `/attach ` (docs-only file attach: `.md` or `docs/` files, hidden paths and secret files refused in compiled code), `/audit [N]` (audit trail: ACTION rows always, READ + OUTCOME when `[features] audit_recording = true`). All three run in the binary with zero API cost; the LLM-flavored `/architecture-explain` lives as an opt-in template in `src/docs/reference/templates/commands/` |
| **Live Settings** | Agent can read/write `config.toml` at runtime; Settings TUI screen (press `S`) shows current config; approval policy persists across restarts. Default: auto-approve (use `/approve` to change) |
| **Web Search** | DuckDuckGo (built-in, no key needed) + EXA AI (neural, free via MCP) by default; Brave Search optional (key in `keys.toml`) |
| **Debug Logging** | `--debug` flag or `debug_logs = true` in config enables file logging; config toggle hot-reloads live without restart; `DEBUG_LOGS_LOCATION` env var for custom log directory |
| **Agent-to-Agent (A2A)** | HTTP gateway implementing A2A Protocol RC v1.0 β€” peer-to-peer agent communication via JSON-RPC 2.0. Supports `message/send`, `message/stream` (SSE), `tasks/get`, `tasks/cancel`. Built-in `a2a_send` tool lets the agent proactively call remote A2A agents. Optional Bearer token auth. Includes multi-agent debate (Bee Colony) with confidence-weighted consensus. Task persistence across restarts |
| **Profiles** | Run multiple isolated instances from the same installation. Each profile gets its own config, keys, memory, sessions, and database. Create with `opencrabs profile create `, switch with `-p `. Migrate config between profiles with `profile migrate`. Export/import for sharing. Token-lock isolation prevents two profiles from using the same bot credential |

### CLI
| Command | Description |
|---------|-------------|
| `opencrabs` | Launch interactive TUI (default) |
| `opencrabs chat` | Launch TUI with optional `--session ` to resume, `--onboard` to force wizard |
| `opencrabs run ` | Execute a single prompt non-interactively. Already unattended under the default `approval_policy`; `--auto-approve` / `--yolo` only when the policy is `ask`. `--quiet` suppresses UI chrome for machine-pure stdout. `--format text\|json\|markdown` |
| `opencrabs agent` | Interactive CLI agent β€” multi-turn conversation in your terminal, no TUI. `-m ` for single-message mode |
| `opencrabs status` | System overview: version, provider, channels, database, brain, cron, dynamic tools |
| `opencrabs doctor` | Full diagnostics: config, provider connectivity, database, brain, channels, CLI tools in PATH |
| `opencrabs init` | Initialize configuration (`--force` to overwrite) |
| `opencrabs config` | Show current configuration (`--show-secrets` to reveal keys) |
| `opencrabs onboard` | Run the onboarding setup wizard |
| `opencrabs channel list` | List all configured channels with enabled/disabled status |
| `opencrabs channel doctor` | Run health checks on all enabled channels |
| `opencrabs memory list` | List brain files and memory entries |
| `opencrabs memory get ` | Show contents of a specific memory or brain file |
| `opencrabs memory stats` | Memory statistics: file count, total size, entry count |
| `opencrabs memory prune` | Dry-run preview of cold beliefs (key, confidence, hits, age) and cold MEMORY.md sections (heading, bytes) beyond the age threshold; `--apply` archives the sections first (beliefs ride along) then sweeps remaining cold beliefs, with receipts in `memory/archive/` + backups. `--max-age-days ` overrides the threshold (default: 90) |
| `opencrabs session list` | List all sessions with provider, model, token count (`--all` includes archived) |
| `opencrabs session get ` | Show session details and recent messages |
| `opencrabs session set-model [target]` | Non-interactive model switch: target by id prefix, `--name ""`, or `--all` (non-archived sessions). The pair splits on the first slash, so `openrouter/tencent/hy3:free` works. A running instance applies it on each session's next message |
| `opencrabs session notify --text "..."` | Send a notification into a session from outside it β€” the subcommand meant for scripts and tooling. `--title` adds a header, `--sender` sets the label the recipient sees (default "CLI tooling"), `--interrupt` delivers even while that session is mid-turn, `--format json` returns a machine-readable delivery verdict |
| `opencrabs db init` | Initialize database |
| `opencrabs db stats` | Show database statistics |
| `opencrabs db clear` | Clear all sessions and messages (`--force` to skip confirmation) |
| `opencrabs cron add\|list\|remove\|enable\|disable\|test` | Manage scheduled cron jobs |
| `opencrabs logs status\|view\|clean\|open` | Log management |
| `opencrabs service install\|start\|stop\|restart\|status\|uninstall` | OS service management (launchd on macOS, systemd on Linux) |
| `opencrabs daemon` | Run in headless daemon mode β€” channels only, no TUI |
| `opencrabs acp` | Serve the Agent Client Protocol (ACP) over stdio JSON-RPC: editors and GUI harnesses like Zed and [MonoCode](https://github.com/hardbeat920/monocode) drive OpenCrabs as their coding agent. See [MonoCode GUI](#monocode-gui) |
| `opencrabs evolve` | Update to the latest release binary and hot-restart, the same path as the `/evolve` command and the automatic 24h check. `--check-only` reports whether an update exists without installing it |
| `opencrabs completions ` | Generate shell completions (bash, zsh, fish, powershell) |
| `opencrabs migrate ` | Migrate from OpenClaw or Hermes. Scans the system, shows interactive picker, spawns agent to handle migration. `--dry-run` to preview |
| `opencrabs version` | Print version and exit |

Global flags: `--debug` (enable file logging), `--config ` (custom config file), `--profile ` / `-p ` (run as a named profile).

### MonoCode GUI

Prefer windows and buttons over the TUI? [MonoCode](https://github.com/hardbeat920/monocode) (MIT, Tauri desktop app for macOS/Linux/Windows) runs OpenCrabs as a native coding-agent provider over ACP. No proxy, no token reselling: MonoCode spawns your local `opencrabs` binary and drives it over stdio JSON-RPC.

1. **Install OpenCrabs v0.5.2 or newer**: `opencrabs acp` ships the ACP server (#1540)
2. **Get a MonoCode build carrying the OpenCrabs provider**: the provider adapter is upstream PR [hardbeat920/monocode#343](https://github.com/hardbeat920/monocode/pull/343) (draft, pending merge); builds with it expose OpenCrabs directly in the provider picker
3. **Pick OpenCrabs and go**: sessions, resume, slash commands, approval gates, plan mode, compaction and model switching all speak native ACP. Sessions born in MonoCode appear in `opencrabs session list` and resume in the TUI as first-class sessions

### Debug Logging

OpenCrabs writes structured debug logs to files when debug logging is active. Two ways to turn it on:

| Method | How | Can be turned off? |
|--------|-----|--------------------|
| CLI flag | Launch with `--debug` | No, stays on for the process lifetime |
| Config toggle | Set `debug_logs = true` under `[agent]` in `config.toml` | Yes, flip back to `false` and it hot-reloads live |

**Precedence:** the two are ORed. If `--debug` is set, debug logging stays on regardless of the config value. A config edit setting `debug_logs = false` cannot silence an operator who launched with the flag. If only the config toggle is set, flipping it back to `false` turns logging off immediately, no restart needed.

**Where logs land:** `~/.opencrabs/logs/` by default. Override the directory with the `DEBUG_LOGS_LOCATION` env var.

**Panic records:** the TUI installs a panic hook that appends every panic it sees to `panic.log`, in that same directory (`DEBUG_LOGS_LOCATION` moves it too), with the source location, the first `opencrabs::` backtrace frame and the captured stack, and mirrors a one-line `PANIC [frame] :: ` record into the daily log. That file is written directly rather than through the debug gate, so it exists even with `debug_logs = false`, and the age-based cleanup leaves it alone because `cleanup_old_logs` only prunes names matching `opencrabs.YYYY-MM-DD`. A TUI that died with nothing in the daily log has its record here.

**Hot-reload:** edit `debug_logs` in `config.toml` (or ask the agent to flip it via `config_manager`) and the change takes effect on the next event. No restart required.

### Brain Files β€” One File, One Job

OpenCrabs's behavior lives in plain-markdown **brain files** in `~/.opencrabs/`. Each file owns exactly **one kind of content**, so a rule lives in one place and never drifts out of sync. The agent β€” and its self-improvement engine β€” route every learning to the file that owns it.

| File | Owns | Scope | In context |
|------|------|-------|-----------|
| **SOUL.md** | Who you are β€” **personality / voice** (how the agent *sounds*) | Generic | Always |
| **USER.md** | Facts about your human β€” identity, role, preferences | Personal | Always |
| **AGENTS.md** | Workspace process **+ the enforced hard rules** (safety/permission gates: never delete/push/email without approval) | Generic | **Always** |
| **MEMORY.md** | What the agent has *learned* β€” facts, corrections, lessons | Personal | On demand Β· main session only |
| **CODE.md** | How code is written β€” standards, testing, your language/framework preference | Generic | On demand |
| **TOOLS.md** | Tools β€” access, skills, commands | Generic | On demand |
| **SECURITY.md** | Security policy β€” code review, network, data, credentials | Generic | On demand |
| **BOOT.md** | Startup + runtime β€” boot steps, memory-save triggers, upgrade/evolve, running as a service | Generic | On demand |

**Loading is lazy, but the gates are always on.** Three files are injected on every turn β€” and survive new sessions and context compaction: `SOUL.md` (personality), `USER.md` (who you're helping), and `AGENTS.md` (the enforced hard rules). Everything else is listed in an "Available Context Files" index and pulled with the `load_brain_file` tool only when a task needs it β€” saving 10–20k tokens per turn. The hard rules live in always-loaded AGENTS (not in on-demand files) precisely so they can never be silently dropped; `MEMORY.md` is personal and loads only in your **main session**, never in shared/group chats.

**Generic vs. personal.** The generic files (SOUL/AGENTS/CODE/TOOLS/SECURITY/BOOT) ship as the same templates for everyone (you then customize them); `USER.md` and `MEMORY.md` accumulate per user and stay private. Each file declares its scope in a `> **Owns:**` header at the top, and the discipline is simple: **one kind of content per file, never duplicated across files** β€” copies drift and go stale. (`HEARTBEAT.md` is a small periodic-task checklist, empty by default.)

> **Deep dive:** for the full directive lifecycle (how directives flow from human/RSI through storage to system prompt), see the [Brain Constitution](src/docs/reference/BRAIN_CONSTITUTION.md).

### Project Directive Files β€” Auto-Discovery

Brain files are OpenCrabs's own. **Project directive files** are the rule files that *other* AI coding tools drop in a repo, and OpenCrabs discovers them automatically. Point the agent at any repository (via `/cd`, a channel workspace, or launching inside one) and it scans that directory for the conventions the repo already ships for Claude Code, Cursor, Windsurf, Cline, Gemini, GitHub Copilot, OpenCode, and the cross-tool `AGENTS.md` standard. No config, no import step: if the files are there, the agent knows.

What it looks for:

| Source | Files |
|--------|-------|
| Cross-tool standard | `AGENTS.md` |
| Claude Code | `CLAUDE.md`, `CLAUDE.local.md`, `.claude/CLAUDE.md`, `.claude/rules/**/*.md` |
| Cursor | `.cursorrules`, `.cursor/rules/**/*.mdc` |
| Windsurf | `.windsurfrules` |
| Cline | `.clinerules` (file or `.clinerules/**/*.{md,txt}`) |
| Gemini CLI | `GEMINI.md` |
| GitHub Copilot | `.github/copilot-instructions.md` |
| OpenCode | `.opencode/AGENTS.md` |

For directory-based rule systems (Cursor `.mdc`, Claude/Cline rules), the frontmatter is parsed and each rule is sorted into one of three tiers, so the agent knows *when* each is relevant:

| Tier | Trigger | How it is surfaced |
|------|---------|--------------------|
| **Always apply** | plain root files; Cursor `alwaysApply: true`; Claude/Cline rules with no `paths` | listed as always-relevant for this project |
| **Conditional** | Cursor `globs`; Claude/Cline `paths` | listed with the glob/path patterns so the agent reads them when touching matching files |
| **On-demand** | Cursor `.mdc` with only a `description` | listed with the description text so the agent judges relevance from the task |

The scan produces a compact **filenames-only index** in the system prompt (not the full file contents), so it costs almost nothing and the agent pulls a file with `read_file` when it actually needs it. The index rebuilds when you `/cd` to another repo and when a directive file is added or edited, and it survives context compaction because it is part of the always-rebuilt preamble. Nothing is added when a project has no directive files.

### Profiles β€” Multi-Instance Crab Agents

Run multiple isolated OpenCrabs instances from the same installation. Each profile gets its own config, brain files, memory, sessions, database, and gateway service.

| Command | Description |
|---------|-------------|
| `opencrabs profile create ` | Create a new profile with fresh config and brain files |
| `opencrabs profile list` | List all profiles with last-used timestamps |
| `opencrabs profile delete ` | Delete a profile and all its data |
| `opencrabs profile export -o profile.tar.gz` | Export a profile as a portable archive |
| `opencrabs profile import profile.tar.gz` | Import a profile from an archive |
| `opencrabs profile migrate --from --to ` | Copy config and brain files between profiles (no DB or sessions) |
| `opencrabs -p ` | Launch OpenCrabs as the specified profile |

**Default profile:** `~/.opencrabs/` β€” works exactly as before. No migration needed. Users who never touch profiles see zero difference.

**TUI footer:** when you launch with `-p ` (or `OPENCRABS_PROFILE` set), the bottom status bar adds a `profile: ` chip so multi-profile users can tell at a glance which instance a given pane is bound to. Without `-p`, no chip is shown β€” the agent is using the base `~/.opencrabs/` directory and there is no real profile by that name to label.

**Named profiles** live at `~/.opencrabs/profiles//` with full isolation:
```
~/.opencrabs/
β”œβ”€β”€ config.toml # default profile
β”œβ”€β”€ opencrabs.db
β”œβ”€β”€ profiles.toml # profile registry
β”œβ”€β”€ locks/ # token-lock files
└── profiles/
β”œβ”€β”€ hermes/ # named profile
β”‚ β”œβ”€β”€ config.toml
β”‚ β”œβ”€β”€ keys.toml
β”‚ β”œβ”€β”€ opencrabs.db
β”‚ β”œβ”€β”€ SOUL.md
β”‚ └── memory/
└── scout/
└── ...
```

**Token-lock isolation:** Two profiles cannot use the same bot credential (Telegram token, Discord token, etc.). On startup, each profile acquires a lock on its channel tokens. If another profile already holds the lock, the channel refuses to start β€” preventing two instances from fighting over the same bot.

**Profile migration:** Use `opencrabs profile migrate --from default --to hermes` to copy all `.md` brain files, `.toml` config files, and `memory/` entries to a new profile. Sessions and database are not copied β€” the new profile starts clean. Add `--force` to overwrite existing files in the target profile. After migrating, customize the new profile's `SOUL.md`, `USER.md`, and `config.toml` to give it a different personality and provider setup.

### Running OpenCrabs β€” TUI vs Daemon

OpenCrabs runs in one of two modes. Pick the one that fits the machine, and **for any one profile run only one at a time**.

| Mode | How you start it | What it is | Use it on |
|------|------------------|------------|-----------|
| **TUI (interactive)** | Just run the binary: `opencrabs` | The full terminal UI β€” chat panes, sessions, settings. Your channels run too and share your session. | A machine you sit at (laptop / desktop) |
| **Daemon (headless)** | `opencrabs daemon`, or install it as a service: `opencrabs service install && opencrabs service start` | No UI. Channels only (Telegram / Discord / Slack / WhatsApp) + cron. Survives reboots, SSH disconnects, and crashes. | An always-on box / VPS |

**Why not both at once?** A bot credential (e.g. a Telegram token) can only hold one live `getUpdates` poll. If a daemon and a TUI both own the same profile's token they fight (HTTP 409) and the channel drops.

**The TUI always wins.** When you open the TUI while a daemon for the *same profile* is running, the TUI shuts that daemon down first, takes over the chan