diff --git a/README.md b/README.md
index 140846ac..2d785f1d 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,21 @@
+
+
-๐ **nanobot** is an **ultra-lightweight** personal AI agent inspired by [OpenClaw](https://github.com/openclaw/openclaw).
-
-โก๏ธ Delivers core agent functionality with **99% fewer lines of code**.
-
-๐ Real-time line count: run `bash core_agent_lines.sh` to verify anytime.
+๐ **nanobot** is an open-source and ultra-lightweight personal AI agent in the spirit of [OpenClaw](https://github.com/openclaw/openclaw), [Claude Code](https://www.anthropic.com/claude-code), and [Codex](https://www.openai.com/codex/). It keeps the core agent loop small and readable while still supporting chat channels, memory, MCP, an OpenAI-compatible API, and practical deployment paths, so you can go from local setup to a long-running personal agent with minimal overhead.
## ๐ข News
@@ -100,46 +98,94 @@
-> ๐ nanobot is for educational, research, and technical exchange purposes only. It is unrelated to crypto and does not involve any official token or coin.
-## Key Features of nanobot:
+## ๐ก Key Features of nanobot
-๐ชถ **Ultra-Lightweight**: A lightweight implementation built for stable, long-running AI agents.
+- **Ultra-lightweight**: stable long-running agent behavior with a small, readable core.
+- **Research-ready**: the codebase is intentionally simple enough to study, modify, and extend.
+- **Practical**: chat channels, API, memory, MCP, and deployment paths are already built in.
+- **Hackable**: you can start fast, then go deeper through repo docs instead of a monolithic landing page.
-๐ฌ **Research-Ready**: Clean, readable code that's easy to understand, modify, and extend for research.
+## ๐ฆ Install
-โก๏ธ **Lightning Fast**: Minimal footprint means faster startup, lower resource usage, and quicker iterations.
+> [!IMPORTANT]
+> If you want the newest features and experiments, install from source. If you want the most stable day-to-day experience, install from PyPI or with `uv`.
-๐ **Easy-to-Use**: One-click to deploy and you're ready to go.
+**Install from source**
+
+```bash
+git clone https://github.com/HKUDS/nanobot.git
+cd nanobot
+pip install -e .
+```
+
+**Install with `uv`**
+
+```bash
+uv tool install nanobot-ai
+```
+
+**Install from PyPI**
+
+```bash
+pip install nanobot-ai
+```
+
+## ๐ Quick Start
+
+**1. Initialize**
+
+```bash
+nanobot onboard
+```
+
+**2. Configure** (`~/.nanobot/config.json`)
+
+Configure these **two parts** in your config (other options have defaults). Add or merge the following blocks into your existing config instead of replacing the whole file.
+
+*Set your API key* (e.g. [OpenRouter](https://openrouter.ai/keys), recommended for global users):
+
+```json
+{
+ "providers": {
+ "openrouter": {
+ "apiKey": "sk-or-v1-xxx"
+ }
+ }
+}
+```
+
+*Set your model* (optionally pin a provider โ defaults to auto-detection):
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "provider": "openrouter",
+ "model": "anthropic/claude-opus-4-6"
+ }
+ }
+}
+```
+
+**3. Chat**
+
+```bash
+nanobot agent
+```
+
+
+- Want different LLM providers, web search, MCP, security settings, or more config options? See [`Configuration`](./docs/configuration.md)
+- Want to run nanobot in chat apps like Telegram, Discord, WeChat or Feishu? See [`Chat Apps`](./docs/chat-apps.md)
+- Want Docker or Linux service deployment? See [`Deployment`](./docs/deployment.md)
## ๐๏ธ Architecture
-
+
-## Table of Contents
-
-- [News](#-news)
-- [Key Features](#key-features-of-nanobot)
-- [Architecture](#๏ธ-architecture)
-- [Features](#-features)
-- [Install](#-install)
-- [Quick Start](#-quick-start)
-- [Chat Apps](#-chat-apps)
-- [Agent Social Network](#-agent-social-network)
-- [Configuration](#๏ธ-configuration)
-- [Multiple Instances](#-multiple-instances)
-- [Memory](#-memory)
-- [CLI Reference](#-cli-reference)
-- [In-Chat Commands](#-in-chat-commands)
-- [Python SDK](#-python-sdk)
-- [OpenAI-Compatible API](#-openai-compatible-api)
-- [Docker](#-docker)
-- [Linux Service](#-linux-service)
-- [Project Structure](#-project-structure)
-- [Contribute & Roadmap](#-contribute--roadmap)
-- [Star History](#-star-history)
+๐ nanobot stays lightweight by centering everything around a small agent loop: messages come in from chat apps, the LLM decides when tools are needed, and memory or skills are pulled in only as context instead of becoming a heavy orchestration layer. That keeps the core path readable and easy to extend, while still letting you add channels, tools, memory, and deployment options without turning the system into a monolith.
## โจ Features
@@ -164,2061 +210,14 @@
-## ๐ฆ Install
+## ๐ Docs
-> [!IMPORTANT]
-> This README may describe features that are available first in the latest source code.
-> If you want the newest features and experiments, install from source.
-> If you want the most stable day-to-day experience, install from PyPI or with `uv`.
+Browse the [repo docs](./docs/README.md) for the latest features and GitHub development version, or visit [nanobot.wiki](https://nanobot.wiki/docs/latest/getting-started/nanobot-overview) for the stable release documentation.
-**Install from source** (latest features, experimental changes may land here first; recommended for development)
-
-```bash
-git clone https://github.com/HKUDS/nanobot.git
-cd nanobot
-pip install -e .
-```
-
-**Install with [uv](https://github.com/astral-sh/uv)** (stable release, fast)
-
-```bash
-uv tool install nanobot-ai
-```
-
-**Install from PyPI** (stable release)
-
-```bash
-pip install nanobot-ai
-```
-
-### Update to latest version
-
-**PyPI / pip**
-
-```bash
-pip install -U nanobot-ai
-nanobot --version
-```
-
-**uv**
-
-```bash
-uv tool upgrade nanobot-ai
-nanobot --version
-```
-
-**Using WhatsApp?** Rebuild the local bridge after upgrading:
-
-```bash
-rm -rf ~/.nanobot/bridge
-nanobot channels login whatsapp
-```
-
-## ๐ Quick Start
-
-> [!TIP]
-> Set your API key in `~/.nanobot/config.json`.
-> Get API keys: [OpenRouter](https://openrouter.ai/keys) (Global)
->
-> For other LLM providers, please see the [Providers](#providers) section.
->
-> For web search capability setup, please see [Web Search](#web-search).
-
-**1. Initialize**
-
-```bash
-nanobot onboard
-```
-
-Use `nanobot onboard --wizard` if you want the interactive setup wizard.
-
-**2. Configure** (`~/.nanobot/config.json`)
-
-Configure these **two parts** in your config (other options have defaults).
-
-*Set your API key* (e.g. OpenRouter, recommended for global users):
-```json
-{
- "providers": {
- "openrouter": {
- "apiKey": "sk-or-v1-xxx"
- }
- }
-}
-```
-
-*Set your model* (optionally pin a provider โ defaults to auto-detection):
-```json
-{
- "agents": {
- "defaults": {
- "model": "anthropic/claude-opus-4-5",
- "provider": "openrouter"
- }
- }
-}
-```
-
-**3. Chat**
-
-```bash
-nanobot agent
-```
-
-That's it! You have a working AI agent in 2 minutes.
-
-## ๐ฌ Chat Apps
-
-Connect nanobot to your favorite chat platform. Want to build your own? See the [Channel Plugin Guide](./docs/CHANNEL_PLUGIN_GUIDE.md).
-
-| Channel | What you need |
-|---------|---------------|
-| **Telegram** | Bot token from @BotFather |
-| **Discord** | Bot token + Message Content intent |
-| **WhatsApp** | QR code scan (`nanobot channels login whatsapp`) |
-| **WeChat (Weixin)** | QR code scan (`nanobot channels login weixin`) |
-| **Feishu** | App ID + App Secret |
-| **DingTalk** | App Key + App Secret |
-| **Slack** | Bot token + App-Level token |
-| **Matrix** | Homeserver URL + Access token |
-| **Email** | IMAP/SMTP credentials |
-| **QQ** | App ID + App Secret |
-| **Wecom** | Bot ID + Bot Secret |
-| **Microsoft Teams** | App ID + App Password + public HTTPS endpoint |
-| **Mochat** | Claw token (auto-setup available) |
-
-
-Telegram (Recommended)
-
-**1. Create a bot**
-- Open Telegram, search `@BotFather`
-- Send `/newbot`, follow prompts
-- Copy the token
-
-**2. Configure**
-
-```json
-{
- "channels": {
- "telegram": {
- "enabled": true,
- "token": "YOUR_BOT_TOKEN",
- "allowFrom": ["YOUR_USER_ID"]
- }
- }
-}
-```
-
-> You can find your **User ID** in Telegram settings. It is shown as `@yourUserId`.
-> Copy this value **without the `@` symbol** and paste it into the config file.
-
-
-**3. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-
-Mochat (Claw IM)
-
-Uses **Socket.IO WebSocket** by default, with HTTP polling fallback.
-
-**1. Ask nanobot to set up Mochat for you**
-
-Simply send this message to nanobot (replace `xxx@xxx` with your real email):
-
-```
-Read https://raw.githubusercontent.com/HKUDS/MoChat/refs/heads/main/skills/nanobot/skill.md and register on MoChat. My Email account is xxx@xxx Bind me as your owner and DM me on MoChat.
-```
-
-nanobot will automatically register, configure `~/.nanobot/config.json`, and connect to Mochat.
-
-**2. Restart gateway**
-
-```bash
-nanobot gateway
-```
-
-That's it โ nanobot handles the rest!
-
-
-
-
-Manual configuration (advanced)
-
-If you prefer to configure manually, add the following to `~/.nanobot/config.json`:
-
-> Keep `claw_token` private. It should only be sent in `X-Claw-Token` header to your Mochat API endpoint.
-
-```json
-{
- "channels": {
- "mochat": {
- "enabled": true,
- "base_url": "https://mochat.io",
- "socket_url": "https://mochat.io",
- "socket_path": "/socket.io",
- "claw_token": "claw_xxx",
- "agent_user_id": "6982abcdef",
- "sessions": ["*"],
- "panels": ["*"],
- "reply_delay_mode": "non-mention",
- "reply_delay_ms": 120000
- }
- }
-}
-```
-
-
-
-
-
-
-
-
-Discord
-
-**1. Create a bot**
-- Go to https://discord.com/developers/applications
-- Create an application โ Bot โ Add Bot
-- Copy the bot token
-
-**2. Enable intents**
-- In the Bot settings, enable **MESSAGE CONTENT INTENT**
-- (Optional) Enable **SERVER MEMBERS INTENT** if you plan to use allow lists based on member data
-
-**3. Get your User ID**
-- Discord Settings โ Advanced โ enable **Developer Mode**
-- Right-click your avatar โ **Copy User ID**
-
-**4. Configure**
-
-```json
-{
- "channels": {
- "discord": {
- "enabled": true,
- "token": "YOUR_BOT_TOKEN",
- "allowFrom": ["YOUR_USER_ID"],
- "allowChannels": [],
- "groupPolicy": "mention",
- "streaming": true
- }
- }
-}
-```
-
-> `groupPolicy` controls how the bot responds in group channels:
-> - `"mention"` (default) โ Only respond when @mentioned
-> - `"open"` โ Respond to all messages
-> DMs always respond when the sender is in `allowFrom`.
-> - If you set group policy to open create new threads as private threads and then @ the bot into it. Otherwise the thread itself and the channel in which you spawned it will spawn a bot session.
-> `allowChannels` restricts the bot to specific Discord channel IDs. Empty (default) means respond in every channel the bot can see. Example: `["1234567890", "0987654321"]`. The filter applies after `allowFrom`, so both must pass.
-> `streaming` defaults to `true`. Disable it only if you explicitly want non-streaming replies.
-
-**5. Invite the bot**
-- OAuth2 โ URL Generator
-- Scopes: `bot`
-- Bot Permissions: `Send Messages`, `Read Message History`
-- Open the generated invite URL and add the bot to your server
-
-**6. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-
-Matrix (Element)
-
-Install Matrix dependencies first:
-
-```bash
-pip install nanobot-ai[matrix]
-```
-
-> [!NOTE]
-> Matrix is not supported on Windows. `matrix-nio[e2e]` depends on
-> `python-olm`, which has no pre-built Windows wheel and is skipped by the
-> `matrix` extra on `sys_platform == 'win32'`. The command above will still
-> succeed on Windows but without `matrix-nio` installed, so enabling the
-> Matrix channel will fail at startup. Use macOS, Linux, or WSL2.
-
-**1. Create/choose a Matrix account**
-
-- Create or reuse a Matrix account on your homeserver (for example `matrix.org`).
-- Confirm you can log in with Element.
-
-**2. Get credentials**
-
-- You need:
- - `userId` (example: `@nanobot:matrix.org`)
- - `password`
-
-(Note: `accessToken` and `deviceId` are still supported for legacy reasons, but
-for reliable encryption, password login is recommended instead. If the
-`password` is provided, `accessToken` and `deviceId` will be ignored.)
-
-**3. Configure**
-
-```json
-{
- "channels": {
- "matrix": {
- "enabled": true,
- "homeserver": "https://matrix.org",
- "userId": "@nanobot:matrix.org",
- "password": "mypasswordhere",
- "e2eeEnabled": true,
- "allowFrom": ["@your_user:matrix.org"],
- "groupPolicy": "open",
- "groupAllowFrom": [],
- "allowRoomMentions": false,
- "maxMediaBytes": 20971520
- }
- }
-}
-```
-
-> Keep a persistent `matrix-store` โ encrypted session state is lost if these change across restarts.
-
-| Option | Description |
-|--------|-------------|
-| `allowFrom` | User IDs allowed to interact. Empty denies all; use `["*"]` to allow everyone. |
-| `groupPolicy` | `open` (default), `mention`, or `allowlist`. |
-| `groupAllowFrom` | Room allowlist (used when policy is `allowlist`). |
-| `allowRoomMentions` | Accept `@room` mentions in mention mode. |
-| `e2eeEnabled` | E2EE support (default `true`). Set `false` for plaintext-only. |
-| `maxMediaBytes` | Max attachment size (default `20MB`). Set `0` to block all media. |
-
-
-
-
-**4. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-
-WhatsApp
-
-Requires **Node.js โฅ18**.
-
-**1. Link device**
-
-```bash
-nanobot channels login whatsapp
-# Scan QR with WhatsApp โ Settings โ Linked Devices
-```
-
-**2. Configure**
-
-```json
-{
- "channels": {
- "whatsapp": {
- "enabled": true,
- "allowFrom": ["+1234567890"]
- }
- }
-}
-```
-
-**3. Run** (two terminals)
-
-```bash
-# Terminal 1
-nanobot channels login whatsapp
-
-# Terminal 2
-nanobot gateway
-```
-
-> WhatsApp bridge updates are not applied automatically for existing installations.
-> After upgrading nanobot, rebuild the local bridge with:
-> `rm -rf ~/.nanobot/bridge && nanobot channels login whatsapp`
-
-
-
-
-Feishu
-
-Uses **WebSocket** long connection โ no public IP required.
-
-**1. Create a Feishu bot**
-- Visit [Feishu Open Platform](https://open.feishu.cn/app)
-- Create a new app โ Enable **Bot** capability
-- **Permissions**:
- - `im:message` (send messages) and `im:message.p2p_msg:readonly` (receive messages)
- - **Streaming replies** (default in nanobot): add **`cardkit:card:write`** (often labeled **Create and update cards** in the Feishu developer console). Required for CardKit entities and streamed assistant text. Older apps may not have it yet โ open **Permission management**, enable the scope, then **publish** a new app version if the console requires it.
- - If you **cannot** add `cardkit:card:write`, set `"streaming": false` under `channels.feishu` (see below). The bot still works; replies use normal interactive cards without token-by-token streaming.
-- **Events**: Add `im.message.receive_v1` (receive messages)
- - Select **Long Connection** mode (requires running nanobot first to establish connection)
-- Get **App ID** and **App Secret** from "Credentials & Basic Info"
-- Publish the app
-
-**2. Configure**
-
-```json
-{
- "channels": {
- "feishu": {
- "enabled": true,
- "appId": "cli_xxx",
- "appSecret": "xxx",
- "encryptKey": "",
- "verificationToken": "",
- "allowFrom": ["ou_YOUR_OPEN_ID"],
- "groupPolicy": "mention",
- "reactEmoji": "OnIt",
- "doneEmoji": "DONE",
- "toolHintPrefix": "๐ง",
- "streaming": true,
- "domain": "feishu"
- }
- }
-}
-```
-
-> `streaming` defaults to `true`. Use `false` if your app does not have **`cardkit:card:write`** (see permissions above).
-> `encryptKey` and `verificationToken` are optional for Long Connection mode.
-> `allowFrom`: Add your open_id (find it in nanobot logs when you message the bot). Use `["*"]` to allow all users.
-> `groupPolicy`: `"mention"` (default โ respond only when @mentioned), `"open"` (respond to all group messages). Private chats always respond.
-> `reactEmoji`: Emoji for "processing" status (default: `OnIt`). See [available emojis](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce).
-> `doneEmoji`: Optional emoji for "completed" status (e.g., `DONE`, `OK`, `HEART`). When set, bot adds this reaction after removing `reactEmoji`.
-> `toolHintPrefix`: Prefix for inline tool hints in streaming cards (default: `๐ง`).
-> `domain`: `"feishu"` (default) for China (open.feishu.cn), `"lark"` for international Lark (open.larksuite.com).
-
-**3. Run**
-
-```bash
-nanobot gateway
-```
-
-> [!TIP]
-> Feishu uses WebSocket to receive messages โ no webhook or public IP needed!
-
-
-
-
-QQ (QQๅ่)
-
-Uses **botpy SDK** with WebSocket โ no public IP required. Currently supports **private messages only**.
-
-**1. Register & create bot**
-- Visit [QQ Open Platform](https://q.qq.com) โ Register as a developer (personal or enterprise)
-- Create a new bot application
-- Go to **ๅผๅ่ฎพ็ฝฎ (Developer Settings)** โ copy **AppID** and **AppSecret**
-
-**2. Set up sandbox for testing**
-- In the bot management console, find **ๆฒ็ฎฑ้
็ฝฎ (Sandbox Config)**
-- Under **ๅจๆถๆฏๅ่กจ้
็ฝฎ**, click **ๆทปๅ ๆๅ** and add your own QQ number
-- Once added, scan the bot's QR code with mobile QQ โ open the bot profile โ tap "ๅๆถๆฏ" to start chatting
-
-**3. Configure**
-
-> - `allowFrom`: Add your openid (find it in nanobot logs when you message the bot). Use `["*"]` for public access.
-> - `msgFormat`: Optional. Use `"plain"` (default) for maximum compatibility with legacy QQ clients, or `"markdown"` for richer formatting on newer clients.
-> - For production: submit a review in the bot console and publish. See [QQ Bot Docs](https://bot.q.qq.com/wiki/) for the full publishing flow.
-
-```json
-{
- "channels": {
- "qq": {
- "enabled": true,
- "appId": "YOUR_APP_ID",
- "secret": "YOUR_APP_SECRET",
- "allowFrom": ["YOUR_OPENID"],
- "msgFormat": "plain"
- }
- }
-}
-```
-
-**4. Run**
-
-```bash
-nanobot gateway
-```
-
-Now send a message to the bot from QQ โ it should respond!
-
-
-
-
-DingTalk (้้)
-
-Uses **Stream Mode** โ no public IP required.
-
-**1. Create a DingTalk bot**
-- Visit [DingTalk Open Platform](https://open-dev.dingtalk.com/)
-- Create a new app -> Add **Robot** capability
-- **Configuration**:
- - Toggle **Stream Mode** ON
-- **Permissions**: Add necessary permissions for sending messages
-- Get **AppKey** (Client ID) and **AppSecret** (Client Secret) from "Credentials"
-- Publish the app
-
-**2. Configure**
-
-```json
-{
- "channels": {
- "dingtalk": {
- "enabled": true,
- "clientId": "YOUR_APP_KEY",
- "clientSecret": "YOUR_APP_SECRET",
- "allowFrom": ["YOUR_STAFF_ID"]
- }
- }
-}
-```
-
-> `allowFrom`: Add your staff ID. Use `["*"]` to allow all users.
-
-**3. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-
-Slack
-
-Uses **Socket Mode** โ no public URL required.
-
-**1. Create a Slack app**
-- Go to [Slack API](https://api.slack.com/apps) โ **Create New App** โ "From scratch"
-- Pick a name and select your workspace
-
-**2. Configure the app**
-- **Socket Mode**: Toggle ON โ Generate an **App-Level Token** with `connections:write` scope โ copy it (`xapp-...`)
-- **OAuth & Permissions**: Add bot scopes: `chat:write`, `reactions:write`, `app_mentions:read`
-- **Event Subscriptions**: Toggle ON โ Subscribe to bot events: `message.im`, `message.channels`, `app_mention` โ Save Changes
-- **App Home**: Scroll to **Show Tabs** โ Enable **Messages Tab** โ Check **"Allow users to send Slash commands and messages from the messages tab"**
-- **Install App**: Click **Install to Workspace** โ Authorize โ copy the **Bot Token** (`xoxb-...`)
-
-**3. Configure nanobot**
-
-```json
-{
- "channels": {
- "slack": {
- "enabled": true,
- "botToken": "xoxb-...",
- "appToken": "xapp-...",
- "allowFrom": ["YOUR_SLACK_USER_ID"],
- "groupPolicy": "mention"
- }
- }
-}
-```
-
-**4. Run**
-
-```bash
-nanobot gateway
-```
-
-DM the bot directly or @mention it in a channel โ it should respond!
-
-> [!TIP]
-> - `groupPolicy`: `"mention"` (default โ respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels).
-> - DM policy defaults to open. Set `"dm": {"enabled": false}` to disable DMs.
-
-
-
-
-Email
-
-Give nanobot its own email account. It polls **IMAP** for incoming mail and replies via **SMTP** โ like a personal email assistant.
-
-**1. Get credentials (Gmail example)**
-- Create a dedicated Gmail account for your bot (e.g. `my-nanobot@gmail.com`)
-- Enable 2-Step Verification โ Create an [App Password](https://myaccount.google.com/apppasswords)
-- Use this app password for both IMAP and SMTP
-
-**2. Configure**
-
-> - `consentGranted` must be `true` to allow mailbox access. This is a safety gate โ set `false` to fully disable.
-> - `allowFrom`: Add your email address. Use `["*"]` to accept emails from anyone.
-> - `smtpUseTls` and `smtpUseSsl` default to `true` / `false` respectively, which is correct for Gmail (port 587 + STARTTLS). No need to set them explicitly.
-> - Set `"autoReplyEnabled": false` if you only want to read/analyze emails without sending automatic replies.
-> - `allowedAttachmentTypes`: Save inbound attachments matching these MIME types โ `["*"]` for all, e.g. `["application/pdf", "image/*"]` (default `[]` = disabled).
-> - `maxAttachmentSize`: Max size per attachment in bytes (default `2000000` / 2MB).
-> - `maxAttachmentsPerEmail`: Max attachments to save per email (default `5`).
-
-```json
-{
- "channels": {
- "email": {
- "enabled": true,
- "consentGranted": true,
- "imapHost": "imap.gmail.com",
- "imapPort": 993,
- "imapUsername": "my-nanobot@gmail.com",
- "imapPassword": "your-app-password",
- "smtpHost": "smtp.gmail.com",
- "smtpPort": 587,
- "smtpUsername": "my-nanobot@gmail.com",
- "smtpPassword": "your-app-password",
- "fromAddress": "my-nanobot@gmail.com",
- "allowFrom": ["your-real-email@gmail.com"],
- "allowedAttachmentTypes": ["application/pdf", "image/*"]
- }
- }
-}
-```
-
-
-**3. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-
-WeChat (ๅพฎไฟก / Weixin)
-
-Uses **HTTP long-poll** with QR-code login via the ilinkai personal WeChat API. No local WeChat desktop client is required.
-
-**1. Install with WeChat support**
-
-```bash
-pip install "nanobot-ai[weixin]"
-```
-
-**2. Configure**
-
-```json
-{
- "channels": {
- "weixin": {
- "enabled": true,
- "allowFrom": ["YOUR_WECHAT_USER_ID"]
- }
- }
-}
-```
-
-> - `allowFrom`: Add the sender ID you see in nanobot logs for your WeChat account. Use `["*"]` to allow all users.
-> - `token`: Optional. If omitted, log in interactively and nanobot will save the token for you.
-> - `routeTag`: Optional. When your upstream Weixin deployment requires request routing, nanobot will send it as the `SKRouteTag` header.
-> - `stateDir`: Optional. Defaults to nanobot's runtime directory for Weixin state.
-> - `pollTimeout`: Optional long-poll timeout in seconds.
-
-**3. Login**
-
-```bash
-nanobot channels login weixin
-```
-
-Use `--force` to re-authenticate and ignore any saved token:
-
-```bash
-nanobot channels login weixin --force
-```
-
-**4. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-
-Wecom (ไผไธๅพฎไฟก)
-
-> Here we use [wecom-aibot-sdk-python](https://github.com/chengyongru/wecom_aibot_sdk) (community Python version of the official [@wecom/aibot-node-sdk](https://www.npmjs.com/package/@wecom/aibot-node-sdk)).
->
-> Uses **WebSocket** long connection โ no public IP required.
-
-**1. Install the optional dependency**
-
-```bash
-pip install nanobot-ai[wecom]
-```
-
-**2. Create a WeCom AI Bot**
-
-Go to the WeCom admin console โ Intelligent Robot โ Create Robot โ select **API mode** with **long connection**. Copy the Bot ID and Secret.
-
-**3. Configure**
-
-```json
-{
- "channels": {
- "wecom": {
- "enabled": true,
- "botId": "your_bot_id",
- "secret": "your_bot_secret",
- "allowFrom": ["your_id"]
- }
- }
-}
-```
-
-**4. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-
-Microsoft Teams (MVP โ DM only)
-
-> Direct-message text in/out, tenant-aware OAuth, conversation reference persistence.
-> Uses a public HTTPS webhook โ no WebSocket; you need a tunnel or reverse proxy.
-
-**1. Install the optional dependency**
-
-```bash
-pip install nanobot-ai[msteams]
-```
-
-**2. Create a Teams / Azure bot app registration**
-
-Create or reuse a Microsoft Teams / Azure bot app registration. Set the bot messaging endpoint to a public HTTPS URL ending in `/api/messages`.
-
-**3. Configure**
-
-```json
-{
- "channels": {
- "msteams": {
- "enabled": true,
- "appId": "YOUR_APP_ID",
- "appPassword": "YOUR_APP_SECRET",
- "tenantId": "YOUR_TENANT_ID",
- "host": "0.0.0.0",
- "port": 3978,
- "path": "/api/messages",
- "allowFrom": ["*"],
- "replyInThread": true,
- "mentionOnlyResponse": "Hi โ what can I help with?",
- "validateInboundAuth": true
- }
- }
-}
-```
-
-> - `replyInThread: true` replies to the triggering Teams activity when a stored `activity_id` is available.
-> - `mentionOnlyResponse` controls what Nanobot receives when a user sends only a bot mention (`Nanobot`). Set to `""` to ignore mention-only messages.
-> - `validateInboundAuth: true` enables inbound Bot Framework bearer-token validation (signature, issuer, audience, lifetime, `serviceUrl`). This is the safe default for public deployments. Only set it to `false` for local development or tightly controlled testing.
-
-**4. Run**
-
-```bash
-nanobot gateway
-```
-
-
-
-## ๐ Agent Social Network
-
-๐ nanobot is capable of linking to the agent social network (agent community). **Just send one message and your nanobot joins automatically!**
-
-| Platform | How to Join (send this message to your bot) |
-|----------|-------------|
-| [**Moltbook**](https://www.moltbook.com/) | `Read https://moltbook.com/skill.md and follow the instructions to join Moltbook` |
-| [**ClawdChat**](https://clawdchat.ai/) | `Read https://clawdchat.ai/skill.md and follow the instructions to join ClawdChat` |
-
-Simply send the command above to your nanobot (via CLI or any chat channel), and it will handle the rest.
-
-## โ๏ธ Configuration
-
-Config file: `~/.nanobot/config.json`
-
-> [!NOTE]
-> If your config file is older than the current schema, you can refresh it without overwriting your existing values:
-> run `nanobot onboard`, then answer `N` when asked whether to overwrite the config.
-> nanobot will merge in missing default fields and keep your current settings.
-
-### Environment Variables for Secrets
-
-Instead of storing secrets directly in `config.json`, you can use `${VAR_NAME}` references that are resolved from environment variables at startup:
-
-```json
-{
- "channels": {
- "telegram": { "token": "${TELEGRAM_TOKEN}" },
- "email": {
- "imapPassword": "${IMAP_PASSWORD}",
- "smtpPassword": "${SMTP_PASSWORD}"
- }
- },
- "providers": {
- "groq": { "apiKey": "${GROQ_API_KEY}" }
- }
-}
-```
-
-For **systemd** deployments, use `EnvironmentFile=` in the service unit to load variables from a file that only the deploying user can read:
-
-```ini
-# /etc/systemd/system/nanobot.service (excerpt)
-[Service]
-EnvironmentFile=/home/youruser/nanobot_secrets.env
-User=nanobot
-ExecStart=...
-```
-
-```bash
-# /home/youruser/nanobot_secrets.env (mode 600, owned by youruser)
-TELEGRAM_TOKEN=your-token-here
-IMAP_PASSWORD=your-password-here
-```
-
-### Providers
-
-> [!TIP]
-> - **Voice transcription**: Voice messages (Telegram, WhatsApp) are automatically transcribed using Whisper. By default Groq is used (free tier). Set `"transcriptionProvider": "openai"` under `channels` to use OpenAI Whisper instead โ the API key is picked from the matching provider config.
-> - **MiniMax Coding Plan**: Exclusive discount links for the nanobot community: [Overseas](https://platform.minimax.io/subscribe/coding-plan?code=9txpdXw04g&source=link) ยท [Mainland China](https://platform.minimaxi.com/subscribe/token-plan?code=GILTJpMTqZ&source=link)
-> - **MiniMax (Mainland China)**: If your API key is from MiniMax's mainland China platform (minimaxi.com), set `"apiBase": "https://api.minimaxi.com/v1"` in your minimax provider config.
-> - **MiniMax thinking mode**: Use `providers.minimaxAnthropic` when you want `reasoningEffort` / thinking mode. MiniMax exposes that capability through its Anthropic-compatible endpoint, so nanobot keeps it as a separate provider instead of guessing MiniMax-specific thinking parameters on the generic OpenAI-compatible `minimax` endpoint. It uses the same `MINIMAX_API_KEY`. Default Anthropic-compatible base URL: `https://api.minimax.io/anthropic`; for mainland China use `https://api.minimaxi.com/anthropic`.
-> - **VolcEngine / BytePlus Coding Plan**: Use dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan` instead of the pay-per-use `volcengine` / `byteplus` providers.
-> - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config.
-> - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config.
-> - **Step Fun (Mainland China)**: If your API key is from Step Fun's mainland China platform (stepfun.com), set `"apiBase": "https://api.stepfun.com/v1"` in your stepfun provider config.
-
-| Provider | Purpose | Get API Key |
-|----------|---------|-------------|
-| `custom` | Any OpenAI-compatible endpoint | โ |
-| `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
-| `volcengine` | LLM (VolcEngine, pay-per-use) | [Coding Plan](https://www.volcengine.com/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) ยท [volcengine.com](https://www.volcengine.com) |
-| `byteplus` | LLM (VolcEngine international, pay-per-use) | [Coding Plan](https://www.byteplus.com/en/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) ยท [byteplus.com](https://www.byteplus.com) |
-| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
-| `azure_openai` | LLM (Azure OpenAI) | [portal.azure.com](https://portal.azure.com) |
-| `openai` | LLM + Voice transcription (Whisper) | [platform.openai.com](https://platform.openai.com) |
-| `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
-| `groq` | LLM + Voice transcription (Whisper, default) | [console.groq.com](https://console.groq.com) |
-| `minimax` | LLM (MiniMax direct) | [platform.minimaxi.com](https://platform.minimaxi.com) |
-| `minimax_anthropic` | LLM (MiniMax Anthropic-compatible endpoint, thinking mode) | [platform.minimaxi.com](https://platform.minimaxi.com) |
-| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
-| `aihubmix` | LLM (API gateway, access to all models) | [aihubmix.com](https://aihubmix.com) |
-| `siliconflow` | LLM (SiliconFlow/็ก
ๅบๆตๅจ) | [siliconflow.cn](https://siliconflow.cn) |
-| `dashscope` | LLM (Qwen) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
-| `moonshot` | LLM (Moonshot/Kimi) | [platform.moonshot.cn](https://platform.moonshot.cn) |
-| `zhipu` | LLM (Zhipu GLM) | [open.bigmodel.cn](https://open.bigmodel.cn) |
-| `mimo` | LLM (MiMo) | [platform.xiaomimimo.com](https://platform.xiaomimimo.com) |
-| `ollama` | LLM (local, Ollama) | โ |
-| `lm_studio` | LLM (local, LM Studio) | โ |
-| `mistral` | LLM | [docs.mistral.ai](https://docs.mistral.ai/) |
-| `stepfun` | LLM (Step Fun/้ถ่ทๆ่พฐ) | [platform.stepfun.com](https://platform.stepfun.com) |
-| `ovms` | LLM (local, OpenVINO Model Server) | [docs.openvino.ai](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html) |
-| `vllm` | LLM (local, any OpenAI-compatible server) | โ |
-| `openai_codex` | LLM (Codex, OAuth) | `nanobot provider login openai-codex` |
-| `github_copilot` | LLM (GitHub Copilot, OAuth) | `nanobot provider login github-copilot` |
-| `qianfan` | LLM (Baidu Qianfan) | [cloud.baidu.com](https://cloud.baidu.com/doc/qianfan/s/Hmh4suq26) |
-
-
-
-OpenAI Codex (OAuth)
-
-Codex uses OAuth instead of API keys. Requires a ChatGPT Plus or Pro account.
-No `providers.openaiCodex` block is needed in `config.json`; `nanobot provider login` stores the OAuth session outside config.
-
-**1. Login:**
-```bash
-nanobot provider login openai-codex
-```
-
-**2. Set model** (merge into `~/.nanobot/config.json`):
-```json
-{
- "agents": {
- "defaults": {
- "model": "openai-codex/gpt-5.1-codex"
- }
- }
-}
-```
-
-**3. Chat:**
-```bash
-nanobot agent -m "Hello!"
-
-# Target a specific workspace/config locally
-nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello!"
-
-# One-off workspace override on top of that config
-nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test -m "Hello!"
-```
-
-> Docker users: use `docker run -it` for interactive OAuth login.
-
-
-
-
-
-GitHub Copilot (OAuth)
-
-GitHub Copilot uses OAuth instead of API keys. Requires a [GitHub account with a plan](https://github.com/features/copilot/plans) configured.
-No `providers.githubCopilot` block is needed in `config.json`; `nanobot provider login` stores the OAuth session outside config.
-
-**1. Login:**
-```bash
-nanobot provider login github-copilot
-```
-
-**2. Set model** (merge into `~/.nanobot/config.json`):
-```json
-{
- "agents": {
- "defaults": {
- "model": "github-copilot/gpt-4.1"
- }
- }
-}
-```
-
-**3. Chat:**
-```bash
-nanobot agent -m "Hello!"
-
-# Target a specific workspace/config locally
-nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello!"
-
-# One-off workspace override on top of that config
-nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test -m "Hello!"
-```
-
-> Docker users: use `docker run -it` for interactive OAuth login.
-
-
-
-
-Custom Provider (Any OpenAI-compatible API)
-
-Connects directly to any OpenAI-compatible endpoint โ llama.cpp, Together AI, Fireworks, Azure OpenAI, or any self-hosted server. Model name is passed as-is.
-
-```json
-{
- "providers": {
- "custom": {
- "apiKey": "your-api-key",
- "apiBase": "https://api.your-provider.com/v1"
- }
- },
- "agents": {
- "defaults": {
- "model": "your-model-name"
- }
- }
-}
-```
-
-> For local servers that don't require authentication, set `apiKey` to `null`.
->
-> `custom` is the right choice for providers that expose an OpenAI-compatible **chat completions** API. It does **not** force third-party endpoints onto the OpenAI/Azure **Responses API**.
->
-> If your proxy or gateway is specifically Responses-API-compatible, use the `azure_openai` provider shape instead and point `apiBase` at that endpoint:
->
-> ```json
-> {
-> "providers": {
-> "azure_openai": {
-> "apiKey": "your-api-key",
-> "apiBase": "https://api.your-provider.com",
-> "defaultModel": "your-model-name"
-> }
-> },
-> "agents": {
-> "defaults": {
-> "provider": "azure_openai",
-> "model": "your-model-name"
-> }
-> }
-> }
-> ```
->
-> In short: **chat-completions-compatible endpoint โ `custom`**; **Responses-compatible endpoint โ `azure_openai`**.
-
-
-
-
-Ollama (local)
-
-Run a local model with Ollama, then add to config:
-
-**1. Start Ollama** (example):
-```bash
-ollama run llama3.2
-```
-
-**2. Add to config** (partial โ merge into `~/.nanobot/config.json`):
-```json
-{
- "providers": {
- "ollama": {
- "apiBase": "http://localhost:11434"
- }
- },
- "agents": {
- "defaults": {
- "provider": "ollama",
- "model": "llama3.2"
- }
- }
-}
-```
-
-> `provider: "auto"` also works when `providers.ollama.apiBase` is configured, but setting `"provider": "ollama"` is the clearest option.
-
-
-
-
-LM Studio (local)
-
-[LM Studio](https://lmstudio.ai/) provides a local OpenAI-compatible server for running LLMs. Download models through the LM Studio UI, then start the local server.
-
-**1. Start LM Studio server:**
-- Launch LM Studio
-- Go to the "Local Server" tab
-- Load a model (e.g., Llama, Mistral, Qwen)
-- Click "Start Server" (default port: 1234)
-
-**2. Add to config** (partial โ merge into `~/.nanobot/config.json`):
-```json
-{
- "providers": {
- "lm_studio": {
- "apiKey": null,
- "apiBase": "http://localhost:1234/v1"
- }
- },
- "agents": {
- "defaults": {
- "provider": "lm_studio",
- "model": "local-model"
- }
- }
-}
-```
-
-> **Note:** Set `apiKey` to `null` for LM Studio since it runs locally and doesn't require authentication. The model name should match what's shown in the LM Studio UI.
-> `provider: "auto"` also works when `providers.lm_studio.apiBase` is configured, but setting `"provider": "lm_studio"` is the clearest option.
-
-
-
-
-OpenVINO Model Server (local / OpenAI-compatible)
-
-Run LLMs locally on Intel GPUs using [OpenVINO Model Server](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html). OVMS exposes an OpenAI-compatible API at `/v3`.
-
-> Requires Docker and an Intel GPU with driver access (`/dev/dri`).
-
-**1. Pull the model** (example):
-
-```bash
-mkdir -p ov/models && cd ov
-
-docker run -d \
- --rm \
- --user $(id -u):$(id -g) \
- -v $(pwd)/models:/models \
- openvino/model_server:latest-gpu \
- --pull \
- --model_name openai/gpt-oss-20b \
- --model_repository_path /models \
- --source_model OpenVINO/gpt-oss-20b-int4-ov \
- --task text_generation \
- --tool_parser gptoss \
- --reasoning_parser gptoss \
- --enable_prefix_caching true \
- --target_device GPU
-```
-
-> This downloads the model weights. Wait for the container to finish before proceeding.
-
-**2. Start the server** (example):
-
-```bash
-docker run -d \
- --rm \
- --name ovms \
- --user $(id -u):$(id -g) \
- -p 8000:8000 \
- -v $(pwd)/models:/models \
- --device /dev/dri \
- --group-add=$(stat -c "%g" /dev/dri/render* | head -n 1) \
- openvino/model_server:latest-gpu \
- --rest_port 8000 \
- --model_name openai/gpt-oss-20b \
- --model_repository_path /models \
- --source_model OpenVINO/gpt-oss-20b-int4-ov \
- --task text_generation \
- --tool_parser gptoss \
- --reasoning_parser gptoss \
- --enable_prefix_caching true \
- --target_device GPU
-```
-
-**3. Add to config** (partial โ merge into `~/.nanobot/config.json`):
-
-```json
-{
- "providers": {
- "ovms": {
- "apiBase": "http://localhost:8000/v3"
- }
- },
- "agents": {
- "defaults": {
- "provider": "ovms",
- "model": "openai/gpt-oss-20b"
- }
- }
-}
-```
-
-> OVMS is a local server โ no API key required. Supports tool calling (`--tool_parser gptoss`), reasoning (`--reasoning_parser gptoss`), and streaming.
-> See the [official OVMS docs](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html) for more details.
-
-
-
-vLLM (local / OpenAI-compatible)
-
-Run your own model with vLLM or any OpenAI-compatible server, then add to config:
-
-**1. Start the server** (example):
-```bash
-vllm serve meta-llama/Llama-3.1-8B-Instruct --port 8000
-```
-
-**2. Add to config** (partial โ merge into `~/.nanobot/config.json`):
-
-*Provider (set API key to null for local servers):*
-```json
-{
- "providers": {
- "vllm": {
- "apiKey": null,
- "apiBase": "http://localhost:8000/v1"
- }
- }
-}
-```
-
-*Model:*
-```json
-{
- "agents": {
- "defaults": {
- "model": "meta-llama/Llama-3.1-8B-Instruct"
- }
- }
-}
-```
-
-
-
-
-Adding a New Provider (Developer Guide)
-
-nanobot uses a **Provider Registry** (`nanobot/providers/registry.py`) as the single source of truth.
-Adding a new provider only takes **2 steps** โ no if-elif chains to touch.
-
-**Step 1.** Add a `ProviderSpec` entry to `PROVIDERS` in `nanobot/providers/registry.py`:
-
-```python
-ProviderSpec(
- name="myprovider", # config field name
- keywords=("myprovider", "mymodel"), # model-name keywords for auto-matching
- env_key="MYPROVIDER_API_KEY", # env var name
- display_name="My Provider", # shown in `nanobot status`
- default_api_base="https://api.myprovider.com/v1", # OpenAI-compatible endpoint
-)
-```
-
-**Step 2.** Add a field to `ProvidersConfig` in `nanobot/config/schema.py`:
-
-```python
-class ProvidersConfig(BaseModel):
- ...
- myprovider: ProviderConfig = ProviderConfig()
-```
-
-That's it! Environment variables, model routing, config matching, and `nanobot status` display will all work automatically.
-
-**Common `ProviderSpec` options:**
-
-| Field | Description | Example |
-|-------|-------------|---------|
-| `default_api_base` | OpenAI-compatible base URL | `"https://api.deepseek.com"` |
-| `env_extras` | Additional env vars to set | `(("ZHIPUAI_API_KEY", "{api_key}"),)` |
-| `model_overrides` | Per-model parameter overrides | `(("kimi-k2.5", {"temperature": 1.0}),)` |
-| `is_gateway` | Can route any model (like OpenRouter) | `True` |
-| `detect_by_key_prefix` | Detect gateway by API key prefix | `"sk-or-"` |
-| `detect_by_base_keyword` | Detect gateway by API base URL | `"openrouter"` |
-| `strip_model_prefix` | Strip provider prefix before sending to gateway | `True` (for AiHubMix) |
-| `supports_max_completion_tokens` | Use `max_completion_tokens` instead of `max_tokens`; required for providers that reject both being set simultaneously (e.g. VolcEngine) | `True` |
-
-
-
-### Channel Settings
-
-Global settings that apply to all channels. Configure under the `channels` section in `~/.nanobot/config.json`:
-
-```json
-{
- "channels": {
- "sendProgress": true,
- "sendToolHints": false,
- "sendMaxRetries": 3,
- "transcriptionProvider": "groq",
- "telegram": { ... }
- }
-}
-```
-
-| Setting | Default | Description |
-|---------|---------|-------------|
-| `sendProgress` | `true` | Stream agent's text progress to the channel |
-| `sendToolHints` | `false` | Stream tool-call hints (e.g. `read_file("โฆ")`) |
-| `sendMaxRetries` | `3` | Max delivery attempts per outbound message, including the initial send (0-10 configured, minimum 1 actual attempt) |
-| `transcriptionProvider` | `"groq"` | Voice transcription backend: `"groq"` (free tier, default) or `"openai"`. API key is auto-resolved from the matching provider config. |
-
-#### Retry Behavior
-
-Retry is intentionally simple.
-
-When a channel `send()` raises, nanobot retries at the channel-manager layer. By default, `channels.sendMaxRetries` is `3`, and that count includes the initial send.
-
-- **Attempt 1**: Send immediately
-- **Attempt 2**: Retry after `1s`
-- **Attempt 3**: Retry after `2s`
-- **Higher retry budgets**: Backoff continues as `1s`, `2s`, `4s`, then stays capped at `4s`
-- **Transient failures**: Network hiccups and temporary API limits often recover on the next attempt
-- **Permanent failures**: Invalid tokens, revoked access, or banned channels will exhaust the retry budget and fail cleanly
-
-> [!NOTE]
-> This design is deliberate: channel implementations should raise on delivery failure, and the channel manager owns the shared retry policy.
->
-> Some channels may still apply small API-specific retries internally. For example, Telegram separately retries timeout and flood-control errors before surfacing a final failure to the manager.
->
-> If a channel is completely unreachable, nanobot cannot notify the user through that same channel. Watch logs for `Failed to send to {channel} after N attempts` to spot persistent delivery failures.
-
-### Web Search
-
-> [!TIP]
-> Use `proxy` in `tools.web` to route all web requests (search + fetch) through a proxy:
-> ```json
-> { "tools": { "web": { "proxy": "http://127.0.0.1:7890" } } }
-> ```
-
-nanobot supports multiple web search providers. Configure in `~/.nanobot/config.json` under `tools.web.search`.
-
-By default, web tools are enabled and web search uses `duckduckgo`, so search works out of the box without an API key.
-
-If you want to disable all built-in web tools entirely, set `tools.web.enable` to `false`. This removes both `web_search` and `web_fetch` from the tool list sent to the LLM.
-
-If you need to allow trusted private ranges such as Tailscale / CGNAT addresses, you can explicitly exempt them from SSRF blocking with `tools.ssrfWhitelist`:
-
-```json
-{
- "tools": {
- "ssrfWhitelist": ["100.64.0.0/10"]
- }
-}
-```
-
-| Provider | Config fields | Env var fallback | Free |
-|----------|--------------|------------------|------|
-| `brave` | `apiKey` | `BRAVE_API_KEY` | No |
-| `tavily` | `apiKey` | `TAVILY_API_KEY` | No |
-| `jina` | `apiKey` | `JINA_API_KEY` | Free tier (10M tokens) |
-| `kagi` | `apiKey` | `KAGI_API_KEY` | No |
-| `searxng` | `baseUrl` | `SEARXNG_BASE_URL` | Yes (self-hosted) |
-| `duckduckgo` (default) | โ | โ | Yes |
-
-**Disable all built-in web tools:**
-```json
-{
- "tools": {
- "web": {
- "enable": false
- }
- }
-}
-```
-
-**Brave:**
-```json
-{
- "tools": {
- "web": {
- "search": {
- "provider": "brave",
- "apiKey": "BSA..."
- }
- }
- }
-}
-```
-
-**Tavily:**
-```json
-{
- "tools": {
- "web": {
- "search": {
- "provider": "tavily",
- "apiKey": "tvly-..."
- }
- }
- }
-}
-```
-
-**Jina** (free tier with 10M tokens):
-```json
-{
- "tools": {
- "web": {
- "search": {
- "provider": "jina",
- "apiKey": "jina_..."
- }
- }
- }
-}
-```
-
-**Kagi:**
-```json
-{
- "tools": {
- "web": {
- "search": {
- "provider": "kagi",
- "apiKey": "your-kagi-api-key"
- }
- }
- }
-}
-```
-
-**SearXNG** (self-hosted, no API key needed):
-```json
-{
- "tools": {
- "web": {
- "search": {
- "provider": "searxng",
- "baseUrl": "https://searx.example"
- }
- }
- }
-}
-```
-
-**DuckDuckGo** (zero config):
-```json
-{
- "tools": {
- "web": {
- "search": {
- "provider": "duckduckgo"
- }
- }
- }
-}
-```
-
-| Option | Type | Default | Description |
-|--------|------|---------|-------------|
-| `enable` | boolean | `true` | Enable or disable all built-in web tools (`web_search` + `web_fetch`) |
-| `proxy` | string or null | `null` | Proxy for all web requests, for example `http://127.0.0.1:7890` |
-
-#### `tools.web.search`
-
-| Option | Type | Default | Description |
-|--------|------|---------|-------------|
-| `provider` | string | `"duckduckgo"` | Search backend: `brave`, `tavily`, `jina`, `searxng`, `duckduckgo` |
-| `apiKey` | string | `""` | API key for Brave or Tavily |
-| `baseUrl` | string | `""` | Base URL for SearXNG |
-| `maxResults` | integer | `5` | Results per search (1โ10) |
-
-### MCP (Model Context Protocol)
-
-> [!TIP]
-> The config format is compatible with Claude Desktop / Cursor. You can copy MCP server configs directly from any MCP server's README.
-
-nanobot supports [MCP](https://modelcontextprotocol.io/) โ connect external tool servers and use them as native agent tools.
-
-Add MCP servers to your `config.json`:
-
-```json
-{
- "tools": {
- "mcpServers": {
- "filesystem": {
- "command": "npx",
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
- },
- "my-remote-mcp": {
- "url": "https://example.com/mcp/",
- "headers": {
- "Authorization": "Bearer xxxxx"
- }
- }
- }
- }
-}
-```
-
-Two transport modes are supported:
-
-| Mode | Config | Example |
-|------|--------|---------|
-| **Stdio** | `command` + `args` | Local process via `npx` / `uvx` |
-| **HTTP** | `url` + `headers` (optional) | Remote endpoint (`https://mcp.example.com/sse`) |
-
-Use `toolTimeout` to override the default 30s per-call timeout for slow servers:
-
-```json
-{
- "tools": {
- "mcpServers": {
- "my-slow-server": {
- "url": "https://example.com/mcp/",
- "toolTimeout": 120
- }
- }
- }
-}
-```
-
-Use `enabledTools` to register only a subset of tools from an MCP server:
-
-```json
-{
- "tools": {
- "mcpServers": {
- "filesystem": {
- "command": "npx",
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
- "enabledTools": ["read_file", "mcp_filesystem_write_file"]
- }
- }
- }
-}
-```
-
-`enabledTools` accepts either the raw MCP tool name (for example `read_file`) or the wrapped nanobot tool name (for example `mcp_filesystem_write_file`).
-
-- Omit `enabledTools`, or set it to `["*"]`, to register all tools.
-- Set `enabledTools` to `[]` to register no tools from that server.
-- Set `enabledTools` to a non-empty list of names to register only that subset.
-
-MCP tools are automatically discovered and registered on startup. The LLM can use them alongside built-in tools โ no extra configuration needed.
-
-
-
-
-### Security
-
-> [!TIP]
-> For production deployments, set `"restrictToWorkspace": true` and `"tools.exec.sandbox": "bwrap"` in your config to sandbox the agent.
-> In `v0.1.4.post3` and earlier, an empty `allowFrom` allowed all senders. Since `v0.1.4.post4`, empty `allowFrom` denies all access by default. To allow all senders, set `"allowFrom": ["*"]`.
-
-| Option | Default | Description |
-|--------|---------|-------------|
-| `tools.restrictToWorkspace` | `false` | When `true`, restricts **all** agent tools (shell, file read/write/edit, list) to the workspace directory. Prevents path traversal and out-of-scope access. |
-| `tools.exec.sandbox` | `""` | Sandbox backend for shell commands. Set to `"bwrap"` to wrap exec calls in a [bubblewrap](https://github.com/containers/bubblewrap) sandbox โ the process can only see the workspace (read-write) and media directory (read-only); config files and API keys are hidden. Automatically enables `restrictToWorkspace` for file tools. **Linux only** โ requires `bwrap` installed (`apt install bubblewrap`; pre-installed in the Docker image). Not available on macOS or Windows (bwrap depends on Linux kernel namespaces). |
-| `tools.exec.enable` | `true` | When `false`, the shell `exec` tool is not registered at all. Use this to completely disable shell command execution. |
-| `tools.exec.pathAppend` | `""` | Extra directories to append to `PATH` when running shell commands (e.g. `/usr/sbin` for `ufw`). |
-| `channels.*.allowFrom` | `[]` (deny all) | Whitelist of user IDs. Empty denies all; use `["*"]` to allow everyone. |
-
-**Docker security**: The official Docker image runs as a non-root user (`nanobot`, UID 1000) with bubblewrap pre-installed. When using `docker-compose.yml`, the container drops all Linux capabilities except `SYS_ADMIN` (required for bwrap's namespace isolation).
-
-
-### Auto Compact
-
-When a user is idle for longer than a configured threshold, nanobot **proactively** compresses the older part of the session context into a summary while keeping a recent legal suffix of live messages. This reduces token cost and first-token latency when the user returns โ instead of re-processing a long stale context with an expired KV cache, the model receives a compact summary, the most recent live context, and fresh input.
-
-```json
-{
- "agents": {
- "defaults": {
- "idleCompactAfterMinutes": 15
- }
- }
-}
-```
-
-| Option | Default | Description |
-|--------|---------|-------------|
-| `agents.defaults.idleCompactAfterMinutes` | `0` (disabled) | Minutes of idle time before auto-compaction starts. Set to `0` to disable. Recommended: `15` โ close to a typical LLM KV cache expiry window, so stale sessions get compacted before the user returns. |
-
-`sessionTtlMinutes` remains accepted as a legacy alias for backward compatibility, but `idleCompactAfterMinutes` is the preferred config key going forward.
-
-How it works:
-1. **Idle detection**: On each idle tick (~1 s), checks all sessions for expiration.
-2. **Background compaction**: Idle sessions summarize the older live prefix via LLM and keep the most recent legal suffix (currently 8 messages).
-3. **Summary injection**: When the user returns, the summary is injected as runtime context (one-shot, not persisted) alongside the retained recent suffix.
-4. **Restart-safe resume**: The summary is also mirrored into session metadata so it can still be recovered after a process restart.
-
-> [!NOTE]
-> Mental model: "summarize older context, keep the freshest live turns, **and overwrite the session file with the compact form.**" It is not a full `session.clear()`, but it is a write โ not a soft cursor move.
->
-> Concretely, auto compact rewrites `sessions/.jsonl` in place: older messages (including their structured `tool_calls` / `tool_call_id` / `reasoning_content`) are replaced by just the retained recent suffix (currently 8 messages), while the archived prefix is preserved only as a plain-text summary appended to `memory/history.jsonl` (or a `[RAW] ...` flattened dump if LLM summarization fails). The original structured JSON of those turns is no longer recoverable from the session file.
->
-> This differs from the **token-driven soft consolidation** that fires when a prompt exceeds the context budget: that path only advances an internal `last_consolidated` cursor and leaves the session file untouched, so the raw tool-call trail stays on disk and can still be replayed or audited. If you rely on that trail for debugging or auditing, leave `idleCompactAfterMinutes` at the default `0` and let only the token-driven path run.
-
-### Timezone
-
-Time is context. Context should be precise.
-
-By default, nanobot uses `UTC` for runtime time context. If you want the agent to think in your local time, set `agents.defaults.timezone` to a valid [IANA timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones):
-
-```json
-{
- "agents": {
- "defaults": {
- "timezone": "Asia/Shanghai"
- }
- }
-}
-```
-
-This affects runtime time strings shown to the model, such as runtime context and heartbeat prompts. It also becomes the default timezone for cron schedules when a cron expression omits `tz`, and for one-shot `at` times when the ISO datetime has no explicit offset.
-
-Common examples: `UTC`, `America/New_York`, `America/Los_Angeles`, `Europe/London`, `Europe/Berlin`, `Asia/Tokyo`, `Asia/Shanghai`, `Asia/Singapore`, `Australia/Sydney`.
-
-> Need another timezone? Browse the full [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
-
-### Unified Session
-
-By default, each channel ร chat ID combination gets its own session. If you use nanobot across multiple channels (e.g. Telegram + Discord + CLI) and want them to share the same conversation, enable `unifiedSession`:
-
-```json
-{
- "agents": {
- "defaults": {
- "unifiedSession": true
- }
- }
-}
-```
-
-When enabled, all incoming messages โ regardless of which channel they arrive on โ are routed into a single shared session. Switching from Telegram to Discord (or any other channel) continues the same conversation seamlessly.
-
-| Behavior | `false` (default) | `true` |
-|----------|-------------------|--------|
-| Session key | `channel:chat_id` | `unified:default` |
-| Cross-channel continuity | No | Yes |
-| `/new` clears | Current channel session | Shared session |
-| `/stop` finds tasks | By channel session | By shared session |
-| Existing `session_key_override` (e.g. Telegram thread) | Respected | Still respected โ not overwritten |
-
-> This is designed for single-user, multi-device setups. It is **off by default** โ existing users see zero behavior change.
-
-### Disabled Skills
-
-nanobot ships with built-in skills, and your workspace can also define custom skills under `skills/`. If you want to hide specific skills from the agent, set `agents.defaults.disabledSkills` to a list of skill directory names:
-
-```json
-{
- "agents": {
- "defaults": {
- "disabledSkills": ["github", "weather"]
- }
- }
-}
-```
-
-Disabled skills are excluded from the main agent's skill summary, from always-on skill injection, and from subagent skill summaries. This is useful when some bundled skills are unnecessary for your deployment or should not be exposed to end users.
-
-| Option | Default | Description |
-|--------|---------|-------------|
-| `agents.defaults.disabledSkills` | `[]` | List of skill directory names to exclude from loading. Applies to both built-in skills and workspace skills. |
-
-## ๐งฉ Multiple Instances
-
-Run multiple nanobot instances simultaneously with separate configs and runtime data. Use `--config` as the main entrypoint. Optionally pass `--workspace` during `onboard` when you want to initialize or update the saved workspace for a specific instance.
-
-### Quick Start
-
-If you want each instance to have its own dedicated workspace from the start, pass both `--config` and `--workspace` during onboarding.
-
-**Initialize instances:**
-
-```bash
-# Create separate instance configs and workspaces
-nanobot onboard --config ~/.nanobot-telegram/config.json --workspace ~/.nanobot-telegram/workspace
-nanobot onboard --config ~/.nanobot-discord/config.json --workspace ~/.nanobot-discord/workspace
-nanobot onboard --config ~/.nanobot-feishu/config.json --workspace ~/.nanobot-feishu/workspace
-```
-
-**Configure each instance:**
-
-Edit `~/.nanobot-telegram/config.json`, `~/.nanobot-discord/config.json`, etc. with different channel settings. The workspace you passed during `onboard` is saved into each config as that instance's default workspace.
-
-**Run instances:**
-
-```bash
-# Instance A - Telegram bot
-nanobot gateway --config ~/.nanobot-telegram/config.json
-
-# Instance B - Discord bot
-nanobot gateway --config ~/.nanobot-discord/config.json
-
-# Instance C - Feishu bot with custom port
-nanobot gateway --config ~/.nanobot-feishu/config.json --port 18792
-```
-
-### Path Resolution
-
-When using `--config`, nanobot derives its runtime data directory from the config file location. The workspace still comes from `agents.defaults.workspace` unless you override it with `--workspace`.
-
-To open a CLI session against one of these instances locally:
-
-```bash
-nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello from Telegram instance"
-nanobot agent -c ~/.nanobot-discord/config.json -m "Hello from Discord instance"
-
-# Optional one-off workspace override
-nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test
-```
-
-> `nanobot agent` starts a local CLI agent using the selected workspace/config. It does not attach to or proxy through an already running `nanobot gateway` process.
-
-| Component | Resolved From | Example |
-|-----------|---------------|---------|
-| **Config** | `--config` path | `~/.nanobot-A/config.json` |
-| **Workspace** | `--workspace` or config | `~/.nanobot-A/workspace/` |
-| **Cron Jobs** | config directory | `~/.nanobot-A/cron/` |
-| **Media / runtime state** | config directory | `~/.nanobot-A/media/` |
-
-### How It Works
-
-- `--config` selects which config file to load
-- By default, the workspace comes from `agents.defaults.workspace` in that config
-- If you pass `--workspace`, it overrides the workspace from the config file
-
-### Minimal Setup
-
-1. Copy your base config into a new instance directory.
-2. Set a different `agents.defaults.workspace` for that instance.
-3. Start the instance with `--config`.
-
-Example config:
-
-```json
-{
- "agents": {
- "defaults": {
- "workspace": "~/.nanobot-telegram/workspace",
- "model": "anthropic/claude-sonnet-4-6"
- }
- },
- "channels": {
- "telegram": {
- "enabled": true,
- "token": "YOUR_TELEGRAM_BOT_TOKEN"
- }
- },
- "gateway": {
- "host": "127.0.0.1",
- "port": 18790
- }
-}
-```
-
-Start separate instances:
-
-```bash
-nanobot gateway --config ~/.nanobot-telegram/config.json
-nanobot gateway --config ~/.nanobot-discord/config.json
-```
-
-Each gateway instance also exposes a lightweight HTTP health endpoint on
-`gateway.host:gateway.port`. By default, the gateway binds to `127.0.0.1`,
-so the endpoint stays local unless you explicitly set `gateway.host` to a
-public or LAN-facing address.
-
-- `GET /health` returns `{"status":"ok"}`
-- Other paths return `404`
-
-Override workspace for one-off runs when needed:
-
-```bash
-nanobot gateway --config ~/.nanobot-telegram/config.json --workspace /tmp/nanobot-telegram-test
-```
-
-### Common Use Cases
-
-- Run separate bots for Telegram, Discord, Feishu, and other platforms
-- Keep testing and production instances isolated
-- Use different models or providers for different teams
-- Serve multiple tenants with separate configs and runtime data
-
-### Notes
-
-- Each instance must use a different port if they run at the same time
-- Use a different workspace per instance if you want isolated memory, sessions, and skills
-- `--workspace` overrides the workspace defined in the config file
-- Cron jobs and runtime media/state are derived from the config directory
-
-## ๐ง Memory
-
-nanobot uses a layered memory system designed to stay light in the moment and durable over
-time.
-
-- `memory/history.jsonl` stores append-only summarized history
-- `SOUL.md`, `USER.md`, and `memory/MEMORY.md` store long-term knowledge managed by Dream
-- `Dream` can also promote repeated workflows into reusable workspace skills under `skills/`
-- `Dream` runs on a schedule and can also be triggered manually
-- memory changes can be inspected and restored with built-in commands
-
-If you want the full design, see [docs/MEMORY.md](docs/MEMORY.md).
-
-## ๐ป CLI Reference
-
-| Command | Description |
-|---------|-------------|
-| `nanobot onboard` | Initialize config & workspace at `~/.nanobot/` |
-| `nanobot onboard --wizard` | Launch the interactive onboarding wizard |
-| `nanobot onboard -c -w ` | Initialize or refresh a specific instance config and workspace |
-| `nanobot agent -m "..."` | Chat with the agent |
-| `nanobot agent -w ` | Chat against a specific workspace |
-| `nanobot agent -w -c ` | Chat against a specific workspace/config |
-| `nanobot agent` | Interactive chat mode |
-| `nanobot agent --no-markdown` | Show plain-text replies |
-| `nanobot agent --logs` | Show runtime logs during chat |
-| `nanobot serve` | Start the OpenAI-compatible API |
-| `nanobot gateway` | Start the gateway |
-| `nanobot status` | Show status |
-| `nanobot provider login openai-codex` | OAuth login for providers |
-| `nanobot channels login ` | Authenticate a channel interactively |
-| `nanobot channels status` | Show channel status |
-
-Interactive mode exits: `exit`, `quit`, `/exit`, `/quit`, `:q`, or `Ctrl+D`.
-
-## ๐ฌ In-Chat Commands
-
-These commands work inside chat channels and interactive agent sessions:
-
-| Command | Description |
-|---------|-------------|
-| `/new` | Start a new conversation |
-| `/stop` | Stop the current task |
-| `/restart` | Restart the bot |
-| `/status` | Show bot status |
-| `/dream` | Run Dream memory consolidation now |
-| `/dream-log` | Show the latest Dream memory change |
-| `/dream-log ` | Show a specific Dream memory change |
-| `/dream-restore` | List recent Dream memory versions |
-| `/dream-restore ` | Restore memory to the state before a specific change |
-| `/help` | Show available in-chat commands |
-
-
-Heartbeat (Periodic Tasks)
-
-The gateway wakes up every 30 minutes and checks `HEARTBEAT.md` in your workspace (`~/.nanobot/workspace/HEARTBEAT.md`). If the file has tasks, the agent executes them and delivers results to your most recently active chat channel.
-
-**Setup:** edit `~/.nanobot/workspace/HEARTBEAT.md` (created automatically by `nanobot onboard`):
-
-```markdown
-## Periodic Tasks
-
-- [ ] Check weather forecast and send a summary
-- [ ] Scan inbox for urgent emails
-```
-
-The agent can also manage this file itself โ ask it to "add a periodic task" and it will update `HEARTBEAT.md` for you.
-
-> **Note:** The gateway must be running (`nanobot gateway`) and you must have chatted with the bot at least once so it knows which channel to deliver to.
-
-
-
-## ๐ Python SDK
-
-Use nanobot as a library โ no CLI, no gateway, just Python:
-
-```python
-from nanobot import Nanobot
-
-bot = Nanobot.from_config()
-result = await bot.run("Summarize the README")
-print(result.content)
-```
-
-Each call carries a `session_key` for conversation isolation โ different keys get independent history:
-
-```python
-await bot.run("hi", session_key="user-alice")
-await bot.run("hi", session_key="task-42")
-```
-
-Add lifecycle hooks to observe or customize the agent:
-
-```python
-from nanobot.agent import AgentHook, AgentHookContext
-
-class AuditHook(AgentHook):
- async def before_execute_tools(self, ctx: AgentHookContext) -> None:
- for tc in ctx.tool_calls:
- print(f"[tool] {tc.name}")
-
-result = await bot.run("Hello", hooks=[AuditHook()])
-```
-
-See [docs/PYTHON_SDK.md](docs/PYTHON_SDK.md) for the full SDK reference.
-
-## ๐ OpenAI-Compatible API
-
-nanobot can expose a minimal OpenAI-compatible endpoint for local integrations:
-
-```bash
-pip install "nanobot-ai[api]"
-nanobot serve
-```
-
-By default, the API binds to `127.0.0.1:8900`. You can change this in `config.json`.
-
-### Behavior
-
-- Session isolation: pass `"session_id"` in the request body to isolate conversations; omit for a shared default session (`api:default`)
-- Single-message input: each request must contain exactly one `user` message
-- Fixed model: omit `model`, or pass the same model shown by `/v1/models`
-- Streaming: set `stream=true` to receive Server-Sent Events (`text/event-stream`) with OpenAI-compatible delta chunks, terminated by `data: [DONE]`; omit or set `stream=false` for a single JSON response
-- **File uploads**: supports images, PDF, Word (.docx), Excel (.xlsx), PowerPoint (.pptx) via JSON base64 or `multipart/form-data` (max 10MB per file)
-- API requests run in the synthetic `api` channel, so the `message` tool does **not** automatically deliver to Telegram/Discord/etc. To proactively send to another chat, call `message` with an explicit `channel` and `chat_id` for an enabled channel.
-
-Example tool call for cross-channel delivery from an API session:
-
-```json
-{
- "content": "Build finished successfully.",
- "channel": "telegram",
- "chat_id": "123456789"
-}
-```
-
-If `channel` points to a channel that is not enabled in your config, nanobot will queue the outbound event but no platform delivery will occur.
-
-### Endpoints
-
-- `GET /health`
-- `GET /v1/models`
-- `POST /v1/chat/completions`
-
-### curl
-
-```bash
-curl http://127.0.0.1:8900/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "messages": [{"role": "user", "content": "hi"}],
- "session_id": "my-session"
- }'
-```
-
-### File Upload (JSON base64)
-
-Send images inline using the OpenAI multimodal content format:
-
-```bash
-curl http://127.0.0.1:8900/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "messages": [{"role": "user", "content": [
- {"type": "text", "text": "Describe this image"},
- {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR..."}}
- ]}]
- }'
-```
-
-### File Upload (multipart/form-data)
-
-Upload any supported file type (images, PDF, Word, Excel, PPT) via multipart:
-
-```bash
-# Single file
-curl http://127.0.0.1:8900/v1/chat/completions \
- -F "message=Summarize this report" \
- -F "files=@report.docx"
-
-# Multiple files with session isolation
-curl http://127.0.0.1:8900/v1/chat/completions \
- -F "message=Compare these files" \
- -F "files=@chart.png" \
- -F "files=@data.xlsx" \
- -F "session_id=my-session"
-```
-
-Supported file types:
-- **Images**: PNG, JPEG, GIF, WebP (sent to AI as base64 for vision analysis)
-- **Documents**: PDF, Word (.docx), Excel (.xlsx), PowerPoint (.pptx) (text extracted and sent to AI)
-- **Text**: TXT, Markdown, CSV, JSON, etc. (read directly)
-
-### Python (`requests`)
-
-```python
-import requests
-
-resp = requests.post(
- "http://127.0.0.1:8900/v1/chat/completions",
- json={
- "messages": [{"role": "user", "content": "hi"}],
- "session_id": "my-session", # optional: isolate conversation
- },
- timeout=120,
-)
-resp.raise_for_status()
-print(resp.json()["choices"][0]["message"]["content"])
-```
-
-### Python (`openai`)
-
-```python
-from openai import OpenAI
-
-client = OpenAI(
- base_url="http://127.0.0.1:8900/v1",
- api_key="dummy",
-)
-
-resp = client.chat.completions.create(
- model="MiniMax-M2.7",
- messages=[{"role": "user", "content": "hi"}],
- extra_body={"session_id": "my-session"}, # optional: isolate conversation
-)
-print(resp.choices[0].message.content)
-```
-
-## ๐ณ Docker
-
-> [!TIP]
-> The `-v ~/.nanobot:/home/nanobot/.nanobot` flag mounts your local config directory into the container, so your config and workspace persist across container restarts.
-> The container runs as user `nanobot` (UID 1000). If you get **Permission denied**, fix ownership on the host first: `sudo chown -R 1000:1000 ~/.nanobot`, or pass `--user $(id -u):$(id -g)` to match your host UID. Podman users can use `--userns=keep-id` instead.
-
-### Docker Compose
-
-```bash
-docker compose run --rm nanobot-cli onboard # first-time setup
-vim ~/.nanobot/config.json # add API keys
-docker compose up -d nanobot-gateway # start gateway
-```
-
-```bash
-docker compose run --rm nanobot-cli agent -m "Hello!" # run CLI
-docker compose logs -f nanobot-gateway # view logs
-docker compose down # stop
-```
-
-### Docker
-
-```bash
-# Build the image
-docker build -t nanobot .
-
-# Initialize config (first time only)
-docker run -v ~/.nanobot:/home/nanobot/.nanobot --rm nanobot onboard
-
-# Edit config on host to add API keys
-vim ~/.nanobot/config.json
-
-# Run gateway (connects to enabled channels, e.g. Telegram/Discord/Mochat)
-docker run -v ~/.nanobot:/home/nanobot/.nanobot -p 18790:18790 nanobot gateway
-
-# Or run a single command
-docker run -v ~/.nanobot:/home/nanobot/.nanobot --rm nanobot agent -m "Hello!"
-docker run -v ~/.nanobot:/home/nanobot/.nanobot --rm nanobot status
-```
-
-## ๐ง Linux Service
-
-Run the gateway as a systemd user service so it starts automatically and restarts on failure.
-
-**1. Find the nanobot binary path:**
-
-```bash
-which nanobot # e.g. /home/user/.local/bin/nanobot
-```
-
-**2. Create the service file** at `~/.config/systemd/user/nanobot-gateway.service` (replace `ExecStart` path if needed):
-
-```ini
-[Unit]
-Description=Nanobot Gateway
-After=network.target
-
-[Service]
-Type=simple
-ExecStart=%h/.local/bin/nanobot gateway
-Restart=always
-RestartSec=10
-NoNewPrivileges=yes
-ProtectSystem=strict
-ReadWritePaths=%h
-
-[Install]
-WantedBy=default.target
-```
-
-**3. Enable and start:**
-
-```bash
-systemctl --user daemon-reload
-systemctl --user enable --now nanobot-gateway
-```
-
-**Common operations:**
-
-```bash
-systemctl --user status nanobot-gateway # check status
-systemctl --user restart nanobot-gateway # restart after config changes
-journalctl --user -u nanobot-gateway -f # follow logs
-```
-
-If you edit the `.service` file itself, run `systemctl --user daemon-reload` before restarting.
-
-> **Note:** User services only run while you are logged in. To keep the gateway running after logout, enable lingering:
->
-> ```bash
-> loginctl enable-linger $USER
-> ```
-
-## ๐ Project Structure
-
-```
-nanobot/
-โโโ agent/ # ๐ง Core agent logic
-โ โโโ loop.py # Agent loop (LLM โ tool execution)
-โ โโโ context.py # Prompt builder
-โ โโโ memory.py # Persistent memory
-โ โโโ skills.py # Skills loader
-โ โโโ subagent.py # Background task execution
-โ โโโ tools/ # Built-in tools (incl. spawn)
-โโโ skills/ # ๐ฏ Bundled skills (github, weather, tmux...)
-โโโ channels/ # ๐ฑ Chat channel integrations (supports plugins)
-โโโ bus/ # ๐ Message routing
-โโโ cron/ # โฐ Scheduled tasks
-โโโ heartbeat/ # ๐ Proactive wake-up
-โโโ providers/ # ๐ค LLM providers (OpenRouter, etc.)
-โโโ session/ # ๐ฌ Conversation sessions
-โโโ config/ # โ๏ธ Configuration
-โโโ cli/ # ๐ฅ๏ธ Commands
-```
+- Talk to your nanobot with familiar chat apps: [`Chat Apps`](./docs/chat-apps.md)
+- Configure providers, web search, MCP, and runtime behavior: [`Configuration`](./docs/configuration.md)
+- Integrate nanobot with local tools and automations: [`OpenAI-Compatible API`](./docs/openai-api.md) ยท [`Python SDK`](./docs/python-sdk.md)
+- Run nanobot with Docker or as a Linux service: [`Deployment`](./docs/deployment.md)
## ๐ค Contribute & Roadmap
@@ -2235,11 +234,11 @@ PRs welcome! The codebase is intentionally small and readable. ๐ค
**Roadmap** โ Pick an item and [open a PR](https://github.com/HKUDS/nanobot/pulls)!
-- [ ] **Multi-modal** โ See and hear (images, voice, video)
-- [ ] **Long-term memory** โ Never forget important context
-- [ ] **Better reasoning** โ Multi-step planning and reflection
-- [ ] **More integrations** โ Calendar and more
-- [ ] **Self-improvement** โ Learn from feedback and mistakes
+- **Multi-modal** โ See and hear (images, voice, video)
+- **Long-term memory** โ Never forget important context
+- **Better reasoning** โ Multi-step planning and reflection
+- **More integrations** โ Calendar and more
+- **Self-improvement** โ Learn from feedback and mistakes
### Contributors
@@ -2263,9 +262,4 @@ PRs welcome! The codebase is intentionally small and readable. ๐ค
Thanks for visiting โจ nanobot!
-
-
-
-
- nanobot is for educational, research, and technical exchange purposes only
-
+
\ No newline at end of file
diff --git a/docs/PYTHON_SDK.md b/docs/PYTHON_SDK.md
deleted file mode 100644
index 2b51055a..00000000
--- a/docs/PYTHON_SDK.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Python SDK
-
-> **Note:** This interface is currently an experiment in the latest source code version and is planned to officially ship in `v0.1.5`.
-
-Use nanobot programmatically โ load config, run the agent, get results.
-
-## Quick Start
-
-```python
-import asyncio
-from nanobot import Nanobot
-
-async def main():
- bot = Nanobot.from_config()
- result = await bot.run("What time is it in Tokyo?")
- print(result.content)
-
-asyncio.run(main())
-```
-
-## API
-
-### `Nanobot.from_config(config_path?, *, workspace?)`
-
-Create a `Nanobot` from a config file.
-
-| Param | Type | Default | Description |
-|-------|------|---------|-------------|
-| `config_path` | `str \| Path \| None` | `None` | Path to `config.json`. Defaults to `~/.nanobot/config.json`. |
-| `workspace` | `str \| Path \| None` | `None` | Override workspace directory from config. |
-
-Raises `FileNotFoundError` if an explicit path doesn't exist.
-
-### `await bot.run(message, *, session_key?, hooks?)`
-
-Run the agent once. Returns a `RunResult`.
-
-| Param | Type | Default | Description |
-|-------|------|---------|-------------|
-| `message` | `str` | *(required)* | The user message to process. |
-| `session_key` | `str` | `"sdk:default"` | Session identifier for conversation isolation. Different keys get independent history. |
-| `hooks` | `list[AgentHook] \| None` | `None` | Lifecycle hooks for this run only. |
-
-```python
-# Isolated sessions โ each user gets independent conversation history
-await bot.run("hi", session_key="user-alice")
-await bot.run("hi", session_key="user-bob")
-```
-
-### `RunResult`
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `content` | `str` | The agent's final text response. |
-| `tools_used` | `list[str]` | Tool names invoked during the run. |
-| `messages` | `list[dict]` | Raw message history (for debugging). |
-
-## Hooks
-
-Hooks let you observe or modify the agent loop without touching internals.
-
-Subclass `AgentHook` and override any method:
-
-| Method | When |
-|--------|------|
-| `before_iteration(ctx)` | Before each LLM call |
-| `on_stream(ctx, delta)` | On each streamed token |
-| `on_stream_end(ctx)` | When streaming finishes |
-| `before_execute_tools(ctx)` | Before tool execution (inspect `ctx.tool_calls`) |
-| `after_iteration(ctx, response)` | After each LLM response |
-| `finalize_content(ctx, content)` | Transform final output text |
-
-### Example: Audit Hook
-
-```python
-from nanobot.agent import AgentHook, AgentHookContext
-
-class AuditHook(AgentHook):
- def __init__(self):
- self.calls = []
-
- async def before_execute_tools(self, ctx: AgentHookContext) -> None:
- for tc in ctx.tool_calls:
- self.calls.append(tc.name)
- print(f"[audit] {tc.name}({tc.arguments})")
-
-hook = AuditHook()
-result = await bot.run("List files in /tmp", hooks=[hook])
-print(f"Tools used: {hook.calls}")
-```
-
-### Composing Hooks
-
-Pass multiple hooks โ they run in order, errors in one don't block others:
-
-```python
-result = await bot.run("hi", hooks=[AuditHook(), MetricsHook()])
-```
-
-Under the hood this uses `CompositeHook` for fan-out with error isolation.
-
-### `finalize_content` Pipeline
-
-Unlike the async methods (fan-out), `finalize_content` is a pipeline โ each hook's output feeds the next:
-
-```python
-class Censor(AgentHook):
- def finalize_content(self, ctx, content):
- return content.replace("secret", "***") if content else content
-```
-
-## Full Example
-
-```python
-import asyncio
-from nanobot import Nanobot
-from nanobot.agent import AgentHook, AgentHookContext
-
-class TimingHook(AgentHook):
- async def before_iteration(self, ctx: AgentHookContext) -> None:
- import time
- ctx.metadata["_t0"] = time.time()
-
- async def after_iteration(self, ctx, response) -> None:
- import time
- elapsed = time.time() - ctx.metadata.get("_t0", 0)
- print(f"[timing] iteration took {elapsed:.2f}s")
-
-async def main():
- bot = Nanobot.from_config(workspace="/my/project")
- result = await bot.run(
- "Explain the main function",
- hooks=[TimingHook()],
- )
- print(result.content)
-
-asyncio.run(main())
-```
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..5158f802
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,30 @@
+# nanobot Docs
+
+For the latest documentation, visit [nanobot.wiki](https://nanobot.wiki/docs/0.1.5/getting-started/nanobot-overview).
+
+The pages in this directory track the current repository and may move faster than the published website.
+
+## Core Docs
+
+| Topic | Repo docs |
+|---|---|
+| Install and quick start | [`quick-start.md`](./quick-start.md) |
+| Chat apps | [`chat-apps.md`](./chat-apps.md) |
+| Agent social network | [`agent-social-network.md`](./agent-social-network.md) |
+| Configuration | [`configuration.md`](./configuration.md) |
+| Multiple instances | [`multiple-instances.md`](./multiple-instances.md) |
+| CLI reference | [`cli-reference.md`](./cli-reference.md) |
+| In-chat commands | [`chat-commands.md`](./chat-commands.md) |
+| OpenAI-compatible API | [`openai-api.md`](./openai-api.md) |
+| Deployment | [`deployment.md`](./deployment.md) |
+
+## Advanced Docs
+
+| Topic | Repo docs |
+|---|---|
+| Memory | [`memory.md`](./memory.md) |
+| Python SDK | [`python-sdk.md`](./python-sdk.md) |
+| Channel plugin guide | [`channel-plugin-guide.md`](./channel-plugin-guide.md) |
+| WebSocket channel | [`websocket.md`](./websocket.md) |
+| Custom tools | [`my-tool.md`](./my-tool.md) |
+
diff --git a/docs/agent-social-network.md b/docs/agent-social-network.md
new file mode 100644
index 00000000..74579b8b
--- /dev/null
+++ b/docs/agent-social-network.md
@@ -0,0 +1,10 @@
+# Agent Social Network
+
+๐ nanobot is capable of linking to the agent social network (agent community). **Just send one message and your nanobot joins automatically!**
+
+| Platform | How to Join (send this message to your bot) |
+|----------|-------------|
+| [**Moltbook**](https://www.moltbook.com/) | `Read https://moltbook.com/skill.md and follow the instructions to join Moltbook` |
+| [**ClawdChat**](https://clawdchat.ai/) | `Read https://clawdchat.ai/skill.md and follow the instructions to join ClawdChat` |
+
+Simply send the command above to your nanobot (via CLI or any chat channel), and it will handle the rest.
diff --git a/docs/CHANNEL_PLUGIN_GUIDE.md b/docs/channel-plugin-guide.md
similarity index 99%
rename from docs/CHANNEL_PLUGIN_GUIDE.md
rename to docs/channel-plugin-guide.md
index 12a41685..d37a9288 100644
--- a/docs/CHANNEL_PLUGIN_GUIDE.md
+++ b/docs/channel-plugin-guide.md
@@ -19,7 +19,7 @@ We'll build a minimal webhook channel that receives messages via HTTP POST and s
### Project Structure
-```
+```text
nanobot-channel-webhook/
โโโ nanobot_channel_webhook/
โ โโโ __init__.py # re-export WebhookChannel
diff --git a/docs/chat-apps.md b/docs/chat-apps.md
new file mode 100644
index 00000000..9332bdc0
--- /dev/null
+++ b/docs/chat-apps.md
@@ -0,0 +1,661 @@
+# Chat Apps
+
+Connect nanobot to your favorite chat platform. Want to build your own? See the [Channel Plugin Guide](./channel-plugin-guide.md).
+
+| Channel | What you need |
+|---------|---------------|
+| **Telegram** | Bot token from @BotFather |
+| **Discord** | Bot token + Message Content intent |
+| **WhatsApp** | QR code scan (`nanobot channels login whatsapp`) |
+| **WeChat (Weixin)** | QR code scan (`nanobot channels login weixin`) |
+| **Feishu** | App ID + App Secret |
+| **DingTalk** | App Key + App Secret |
+| **Slack** | Bot token + App-Level token |
+| **Matrix** | Homeserver URL + Access token |
+| **Email** | IMAP/SMTP credentials |
+| **QQ** | App ID + App Secret |
+| **Wecom** | Bot ID + Bot Secret |
+| **Microsoft Teams** | App ID + App Password + public HTTPS endpoint |
+| **Mochat** | Claw token (auto-setup available) |
+
+
+Telegram (Recommended)
+
+**1. Create a bot**
+- Open Telegram, search `@BotFather`
+- Send `/newbot`, follow prompts
+- Copy the token
+
+**2. Configure**
+
+```json
+{
+ "channels": {
+ "telegram": {
+ "enabled": true,
+ "token": "YOUR_BOT_TOKEN",
+ "allowFrom": ["YOUR_USER_ID"]
+ }
+ }
+}
+```
+
+> You can find your **User ID** in Telegram settings. It is shown as `@yourUserId`.
+> Copy this value **without the `@` symbol** and paste it into the config file.
+
+
+**3. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
+
+Mochat (Claw IM)
+
+Uses **Socket.IO WebSocket** by default, with HTTP polling fallback.
+
+**1. Ask nanobot to set up Mochat for you**
+
+Simply send this message to nanobot (replace `xxx@xxx` with your real email):
+
+```
+Read https://raw.githubusercontent.com/HKUDS/MoChat/refs/heads/main/skills/nanobot/skill.md and register on MoChat. My Email account is xxx@xxx Bind me as your owner and DM me on MoChat.
+```
+
+nanobot will automatically register, configure `~/.nanobot/config.json`, and connect to Mochat.
+
+**2. Restart gateway**
+
+```bash
+nanobot gateway
+```
+
+That's it โ nanobot handles the rest!
+
+
+
+
+Manual configuration (advanced)
+
+If you prefer to configure manually, add the following to `~/.nanobot/config.json`:
+
+> Keep `claw_token` private. It should only be sent in `X-Claw-Token` header to your Mochat API endpoint.
+
+```json
+{
+ "channels": {
+ "mochat": {
+ "enabled": true,
+ "base_url": "https://mochat.io",
+ "socket_url": "https://mochat.io",
+ "socket_path": "/socket.io",
+ "claw_token": "claw_xxx",
+ "agent_user_id": "6982abcdef",
+ "sessions": ["*"],
+ "panels": ["*"],
+ "reply_delay_mode": "non-mention",
+ "reply_delay_ms": 120000
+ }
+ }
+}
+```
+
+
+
+
+
+
+
+
+Discord
+
+**1. Create a bot**
+- Go to https://discord.com/developers/applications
+- Create an application โ Bot โ Add Bot
+- Copy the bot token
+
+**2. Enable intents**
+- In the Bot settings, enable **MESSAGE CONTENT INTENT**
+- (Optional) Enable **SERVER MEMBERS INTENT** if you plan to use allow lists based on member data
+
+**3. Get your User ID**
+- Discord Settings โ Advanced โ enable **Developer Mode**
+- Right-click your avatar โ **Copy User ID**
+
+**4. Configure**
+
+```json
+{
+ "channels": {
+ "discord": {
+ "enabled": true,
+ "token": "YOUR_BOT_TOKEN",
+ "allowFrom": ["YOUR_USER_ID"],
+ "allowChannels": [],
+ "groupPolicy": "mention",
+ "streaming": true
+ }
+ }
+}
+```
+
+> `groupPolicy` controls how the bot responds in group channels:
+> - `"mention"` (default) โ Only respond when @mentioned
+> - `"open"` โ Respond to all messages
+> DMs always respond when the sender is in `allowFrom`.
+> - If you set group policy to open create new threads as private threads and then @ the bot into it. Otherwise the thread itself and the channel in which you spawned it will spawn a bot session.
+> `allowChannels` restricts the bot to specific Discord channel IDs. Empty (default) means respond in every channel the bot can see. Example: `["1234567890", "0987654321"]`. The filter applies after `allowFrom`, so both must pass.
+> `streaming` defaults to `true`. Disable it only if you explicitly want non-streaming replies.
+
+**5. Invite the bot**
+- OAuth2 โ URL Generator
+- Scopes: `bot`
+- Bot Permissions: `Send Messages`, `Read Message History`
+- Open the generated invite URL and add the bot to your server
+
+**6. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
+
+Matrix (Element)
+
+Install Matrix dependencies first:
+
+```bash
+pip install nanobot-ai[matrix]
+```
+
+> [!NOTE]
+> Matrix is not supported on Windows. `matrix-nio[e2e]` depends on
+> `python-olm`, which has no pre-built Windows wheel and is skipped by the
+> `matrix` extra on `sys_platform == 'win32'`. The command above will still
+> succeed on Windows but without `matrix-nio` installed, so enabling the
+> Matrix channel will fail at startup. Use macOS, Linux, or WSL2.
+
+**1. Create/choose a Matrix account**
+
+- Create or reuse a Matrix account on your homeserver (for example `matrix.org`).
+- Confirm you can log in with Element.
+
+**2. Get credentials**
+
+- You need:
+ - `userId` (example: `@nanobot:matrix.org`)
+ - `password`
+
+(Note: `accessToken` and `deviceId` are still supported for legacy reasons, but
+for reliable encryption, password login is recommended instead. If the
+`password` is provided, `accessToken` and `deviceId` will be ignored.)
+
+**3. Configure**
+
+```json
+{
+ "channels": {
+ "matrix": {
+ "enabled": true,
+ "homeserver": "https://matrix.org",
+ "userId": "@nanobot:matrix.org",
+ "password": "mypasswordhere",
+ "e2eeEnabled": true,
+ "allowFrom": ["@your_user:matrix.org"],
+ "groupPolicy": "open",
+ "groupAllowFrom": [],
+ "allowRoomMentions": false,
+ "maxMediaBytes": 20971520
+ }
+ }
+}
+```
+
+> Keep a persistent `matrix-store` โ encrypted session state is lost if these change across restarts.
+
+| Option | Description |
+|--------|-------------|
+| `allowFrom` | User IDs allowed to interact. Empty denies all; use `["*"]` to allow everyone. |
+| `groupPolicy` | `open` (default), `mention`, or `allowlist`. |
+| `groupAllowFrom` | Room allowlist (used when policy is `allowlist`). |
+| `allowRoomMentions` | Accept `@room` mentions in mention mode. |
+| `e2eeEnabled` | E2EE support (default `true`). Set `false` for plaintext-only. |
+| `maxMediaBytes` | Max attachment size (default `20MB`). Set `0` to block all media. |
+
+
+
+
+**4. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
+
+WhatsApp
+
+Requires **Node.js โฅ18**.
+
+**1. Link device**
+
+```bash
+nanobot channels login whatsapp
+# Scan QR with WhatsApp โ Settings โ Linked Devices
+```
+
+**2. Configure**
+
+```json
+{
+ "channels": {
+ "whatsapp": {
+ "enabled": true,
+ "allowFrom": ["+1234567890"]
+ }
+ }
+}
+```
+
+**3. Run** (two terminals)
+
+```bash
+# Terminal 1
+nanobot channels login whatsapp
+
+# Terminal 2
+nanobot gateway
+```
+
+> WhatsApp bridge updates are not applied automatically for existing installations.
+> After upgrading nanobot, rebuild the local bridge with:
+> `rm -rf ~/.nanobot/bridge && nanobot channels login whatsapp`
+
+
+
+
+Feishu
+
+Uses **WebSocket** long connection โ no public IP required.
+
+**1. Create a Feishu bot**
+- Visit [Feishu Open Platform](https://open.feishu.cn/app)
+- Create a new app โ Enable **Bot** capability
+- **Permissions**:
+ - `im:message` (send messages) and `im:message.p2p_msg:readonly` (receive messages)
+ - **Streaming replies** (default in nanobot): add **`cardkit:card:write`** (often labeled **Create and update cards** in the Feishu developer console). Required for CardKit entities and streamed assistant text. Older apps may not have it yet โ open **Permission management**, enable the scope, then **publish** a new app version if the console requires it.
+ - If you **cannot** add `cardkit:card:write`, set `"streaming": false` under `channels.feishu` (see below). The bot still works; replies use normal interactive cards without token-by-token streaming.
+- **Events**: Add `im.message.receive_v1` (receive messages)
+ - Select **Long Connection** mode (requires running nanobot first to establish connection)
+- Get **App ID** and **App Secret** from "Credentials & Basic Info"
+- Publish the app
+
+**2. Configure**
+
+```json
+{
+ "channels": {
+ "feishu": {
+ "enabled": true,
+ "appId": "cli_xxx",
+ "appSecret": "xxx",
+ "encryptKey": "",
+ "verificationToken": "",
+ "allowFrom": ["ou_YOUR_OPEN_ID"],
+ "groupPolicy": "mention",
+ "reactEmoji": "OnIt",
+ "doneEmoji": "DONE",
+ "toolHintPrefix": "๐ง",
+ "streaming": true,
+ "domain": "feishu"
+ }
+ }
+}
+```
+
+> `streaming` defaults to `true`. Use `false` if your app does not have **`cardkit:card:write`** (see permissions above).
+> `encryptKey` and `verificationToken` are optional for Long Connection mode.
+> `allowFrom`: Add your open_id (find it in nanobot logs when you message the bot). Use `["*"]` to allow all users.
+> `groupPolicy`: `"mention"` (default โ respond only when @mentioned), `"open"` (respond to all group messages). Private chats always respond.
+> `reactEmoji`: Emoji for "processing" status (default: `OnIt`). See [available emojis](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce).
+> `doneEmoji`: Optional emoji for "completed" status (e.g., `DONE`, `OK`, `HEART`). When set, bot adds this reaction after removing `reactEmoji`.
+> `toolHintPrefix`: Prefix for inline tool hints in streaming cards (default: `๐ง`).
+> `domain`: `"feishu"` (default) for China (open.feishu.cn), `"lark"` for international Lark (open.larksuite.com).
+
+**3. Run**
+
+```bash
+nanobot gateway
+```
+
+> [!TIP]
+> Feishu uses WebSocket to receive messages โ no webhook or public IP needed!
+
+
+
+
+QQ (QQๅ่)
+
+Uses **botpy SDK** with WebSocket โ no public IP required. Currently supports **private messages only**.
+
+**1. Register & create bot**
+- Visit [QQ Open Platform](https://q.qq.com) โ Register as a developer (personal or enterprise)
+- Create a new bot application
+- Go to **ๅผๅ่ฎพ็ฝฎ (Developer Settings)** โ copy **AppID** and **AppSecret**
+
+**2. Set up sandbox for testing**
+- In the bot management console, find **ๆฒ็ฎฑ้
็ฝฎ (Sandbox Config)**
+- Under **ๅจๆถๆฏๅ่กจ้
็ฝฎ**, click **ๆทปๅ ๆๅ** and add your own QQ number
+- Once added, scan the bot's QR code with mobile QQ โ open the bot profile โ tap "ๅๆถๆฏ" to start chatting
+
+**3. Configure**
+
+> - `allowFrom`: Add your openid (find it in nanobot logs when you message the bot). Use `["*"]` for public access.
+> - `msgFormat`: Optional. Use `"plain"` (default) for maximum compatibility with legacy QQ clients, or `"markdown"` for richer formatting on newer clients.
+> - For production: submit a review in the bot console and publish. See [QQ Bot Docs](https://bot.q.qq.com/wiki/) for the full publishing flow.
+
+```json
+{
+ "channels": {
+ "qq": {
+ "enabled": true,
+ "appId": "YOUR_APP_ID",
+ "secret": "YOUR_APP_SECRET",
+ "allowFrom": ["YOUR_OPENID"],
+ "msgFormat": "plain"
+ }
+ }
+}
+```
+
+**4. Run**
+
+```bash
+nanobot gateway
+```
+
+Now send a message to the bot from QQ โ it should respond!
+
+
+
+
+DingTalk (้้)
+
+Uses **Stream Mode** โ no public IP required.
+
+**1. Create a DingTalk bot**
+- Visit [DingTalk Open Platform](https://open-dev.dingtalk.com/)
+- Create a new app -> Add **Robot** capability
+- **Configuration**:
+ - Toggle **Stream Mode** ON
+- **Permissions**: Add necessary permissions for sending messages
+- Get **AppKey** (Client ID) and **AppSecret** (Client Secret) from "Credentials"
+- Publish the app
+
+**2. Configure**
+
+```json
+{
+ "channels": {
+ "dingtalk": {
+ "enabled": true,
+ "clientId": "YOUR_APP_KEY",
+ "clientSecret": "YOUR_APP_SECRET",
+ "allowFrom": ["YOUR_STAFF_ID"]
+ }
+ }
+}
+```
+
+> `allowFrom`: Add your staff ID. Use `["*"]` to allow all users.
+
+**3. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
+
+Slack
+
+Uses **Socket Mode** โ no public URL required.
+
+**1. Create a Slack app**
+- Go to [Slack API](https://api.slack.com/apps) โ **Create New App** โ "From scratch"
+- Pick a name and select your workspace
+
+**2. Configure the app**
+- **Socket Mode**: Toggle ON โ Generate an **App-Level Token** with `connections:write` scope โ copy it (`xapp-...`)
+- **OAuth & Permissions**: Add bot scopes: `chat:write`, `reactions:write`, `app_mentions:read`
+- **Event Subscriptions**: Toggle ON โ Subscribe to bot events: `message.im`, `message.channels`, `app_mention` โ Save Changes
+- **App Home**: Scroll to **Show Tabs** โ Enable **Messages Tab** โ Check **"Allow users to send Slash commands and messages from the messages tab"**
+- **Install App**: Click **Install to Workspace** โ Authorize โ copy the **Bot Token** (`xoxb-...`)
+
+**3. Configure nanobot**
+
+```json
+{
+ "channels": {
+ "slack": {
+ "enabled": true,
+ "botToken": "xoxb-...",
+ "appToken": "xapp-...",
+ "allowFrom": ["YOUR_SLACK_USER_ID"],
+ "groupPolicy": "mention"
+ }
+ }
+}
+```
+
+**4. Run**
+
+```bash
+nanobot gateway
+```
+
+DM the bot directly or @mention it in a channel โ it should respond!
+
+> [!TIP]
+> - `groupPolicy`: `"mention"` (default โ respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels).
+> - DM policy defaults to open. Set `"dm": {"enabled": false}` to disable DMs.
+
+
+
+
+Email
+
+Give nanobot its own email account. It polls **IMAP** for incoming mail and replies via **SMTP** โ like a personal email assistant.
+
+**1. Get credentials (Gmail example)**
+- Create a dedicated Gmail account for your bot (e.g. `my-nanobot@gmail.com`)
+- Enable 2-Step Verification โ Create an [App Password](https://myaccount.google.com/apppasswords)
+- Use this app password for both IMAP and SMTP
+
+**2. Configure**
+
+> - `consentGranted` must be `true` to allow mailbox access. This is a safety gate โ set `false` to fully disable.
+> - `allowFrom`: Add your email address. Use `["*"]` to accept emails from anyone.
+> - `smtpUseTls` and `smtpUseSsl` default to `true` / `false` respectively, which is correct for Gmail (port 587 + STARTTLS). No need to set them explicitly.
+> - Set `"autoReplyEnabled": false` if you only want to read/analyze emails without sending automatic replies.
+> - `allowedAttachmentTypes`: Save inbound attachments matching these MIME types โ `["*"]` for all, e.g. `["application/pdf", "image/*"]` (default `[]` = disabled).
+> - `maxAttachmentSize`: Max size per attachment in bytes (default `2000000` / 2MB).
+> - `maxAttachmentsPerEmail`: Max attachments to save per email (default `5`).
+
+```json
+{
+ "channels": {
+ "email": {
+ "enabled": true,
+ "consentGranted": true,
+ "imapHost": "imap.gmail.com",
+ "imapPort": 993,
+ "imapUsername": "my-nanobot@gmail.com",
+ "imapPassword": "your-app-password",
+ "smtpHost": "smtp.gmail.com",
+ "smtpPort": 587,
+ "smtpUsername": "my-nanobot@gmail.com",
+ "smtpPassword": "your-app-password",
+ "fromAddress": "my-nanobot@gmail.com",
+ "allowFrom": ["your-real-email@gmail.com"],
+ "allowedAttachmentTypes": ["application/pdf", "image/*"]
+ }
+ }
+}
+```
+
+
+**3. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
+
+WeChat (ๅพฎไฟก / Weixin)
+
+Uses **HTTP long-poll** with QR-code login via the ilinkai personal WeChat API. No local WeChat desktop client is required.
+
+**1. Install with WeChat support**
+
+```bash
+pip install "nanobot-ai[weixin]"
+```
+
+**2. Configure**
+
+```json
+{
+ "channels": {
+ "weixin": {
+ "enabled": true,
+ "allowFrom": ["YOUR_WECHAT_USER_ID"]
+ }
+ }
+}
+```
+
+> - `allowFrom`: Add the sender ID you see in nanobot logs for your WeChat account. Use `["*"]` to allow all users.
+> - `token`: Optional. If omitted, log in interactively and nanobot will save the token for you.
+> - `routeTag`: Optional. When your upstream Weixin deployment requires request routing, nanobot will send it as the `SKRouteTag` header.
+> - `stateDir`: Optional. Defaults to nanobot's runtime directory for Weixin state.
+> - `pollTimeout`: Optional long-poll timeout in seconds.
+
+**3. Login**
+
+```bash
+nanobot channels login weixin
+```
+
+Use `--force` to re-authenticate and ignore any saved token:
+
+```bash
+nanobot channels login weixin --force
+```
+
+**4. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
+
+Wecom (ไผไธๅพฎไฟก)
+
+> Here we use [wecom-aibot-sdk-python](https://github.com/chengyongru/wecom_aibot_sdk) (community Python version of the official [@wecom/aibot-node-sdk](https://www.npmjs.com/package/@wecom/aibot-node-sdk)).
+>
+> Uses **WebSocket** long connection โ no public IP required.
+
+**1. Install the optional dependency**
+
+```bash
+pip install nanobot-ai[wecom]
+```
+
+**2. Create a WeCom AI Bot**
+
+Go to the WeCom admin console โ Intelligent Robot โ Create Robot โ select **API mode** with **long connection**. Copy the Bot ID and Secret.
+
+**3. Configure**
+
+```json
+{
+ "channels": {
+ "wecom": {
+ "enabled": true,
+ "botId": "your_bot_id",
+ "secret": "your_bot_secret",
+ "allowFrom": ["your_id"]
+ }
+ }
+}
+```
+
+**4. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
+
+Microsoft Teams (MVP โ DM only)
+
+> Direct-message text in/out, tenant-aware OAuth, conversation reference persistence.
+> Uses a public HTTPS webhook โ no WebSocket; you need a tunnel or reverse proxy.
+
+**1. Install the optional dependency**
+
+```bash
+pip install nanobot-ai[msteams]
+```
+
+**2. Create a Teams / Azure bot app registration**
+
+Create or reuse a Microsoft Teams / Azure bot app registration. Set the bot messaging endpoint to a public HTTPS URL ending in `/api/messages`.
+
+**3. Configure**
+
+```json
+{
+ "channels": {
+ "msteams": {
+ "enabled": true,
+ "appId": "YOUR_APP_ID",
+ "appPassword": "YOUR_APP_SECRET",
+ "tenantId": "YOUR_TENANT_ID",
+ "host": "0.0.0.0",
+ "port": 3978,
+ "path": "/api/messages",
+ "allowFrom": ["*"],
+ "replyInThread": true,
+ "mentionOnlyResponse": "Hi โ what can I help with?",
+ "validateInboundAuth": true
+ }
+ }
+}
+```
+
+> - `replyInThread: true` replies to the triggering Teams activity when a stored `activity_id` is available.
+> - `mentionOnlyResponse` controls what Nanobot receives when a user sends only a bot mention (`Nanobot`). Set to `""` to ignore mention-only messages.
+> - `validateInboundAuth: true` enables inbound Bot Framework bearer-token validation (signature, issuer, audience, lifetime, `serviceUrl`). This is the safe default for public deployments. Only set it to `false` for local development or tightly controlled testing.
+
+**4. Run**
+
+```bash
+nanobot gateway
+```
+
+
\ No newline at end of file
diff --git a/docs/chat-commands.md b/docs/chat-commands.md
new file mode 100644
index 00000000..72707e76
--- /dev/null
+++ b/docs/chat-commands.md
@@ -0,0 +1,33 @@
+# In-Chat Commands
+
+These commands work inside chat channels and interactive agent sessions:
+
+| Command | Description |
+|---------|-------------|
+| `/new` | Start a new conversation |
+| `/stop` | Stop the current task |
+| `/restart` | Restart the bot |
+| `/status` | Show bot status |
+| `/dream` | Run Dream memory consolidation now |
+| `/dream-log` | Show the latest Dream memory change |
+| `/dream-log ` | Show a specific Dream memory change |
+| `/dream-restore` | List recent Dream memory versions |
+| `/dream-restore ` | Restore memory to the state before a specific change |
+| `/help` | Show available in-chat commands |
+
+## Periodic Tasks
+
+The gateway wakes up every 30 minutes and checks `HEARTBEAT.md` in your workspace (`~/.nanobot/workspace/HEARTBEAT.md`). If the file has tasks, the agent executes them and delivers results to your most recently active chat channel.
+
+**Setup:** edit `~/.nanobot/workspace/HEARTBEAT.md` (created automatically by `nanobot onboard`):
+
+```markdown
+## Periodic Tasks
+
+- [ ] Check weather forecast and send a summary
+- [ ] Scan inbox for urgent emails
+```
+
+The agent can also manage this file itself โ ask it to "add a periodic task" and it will update `HEARTBEAT.md` for you.
+
+> **Note:** The gateway must be running (`nanobot gateway`) and you must have chatted with the bot at least once so it knows which channel to deliver to.
diff --git a/docs/cli-reference.md b/docs/cli-reference.md
new file mode 100644
index 00000000..667f8c13
--- /dev/null
+++ b/docs/cli-reference.md
@@ -0,0 +1,21 @@
+# CLI Reference
+
+| Command | Description |
+|---------|-------------|
+| `nanobot onboard` | Initialize config & workspace at `~/.nanobot/` |
+| `nanobot onboard --wizard` | Launch the interactive onboarding wizard |
+| `nanobot onboard -c -w ` | Initialize or refresh a specific instance config and workspace |
+| `nanobot agent -m "..."` | Chat with the agent |
+| `nanobot agent -w ` | Chat against a specific workspace |
+| `nanobot agent -w -c ` | Chat against a specific workspace/config |
+| `nanobot agent` | Interactive chat mode |
+| `nanobot agent --no-markdown` | Show plain-text replies |
+| `nanobot agent --logs` | Show runtime logs during chat |
+| `nanobot serve` | Start the OpenAI-compatible API |
+| `nanobot gateway` | Start the gateway |
+| `nanobot status` | Show status |
+| `nanobot provider login openai-codex` | OAuth login for providers |
+| `nanobot channels login ` | Authenticate a channel interactively |
+| `nanobot channels status` | Show channel status |
+
+Interactive mode exits: `exit`, `quit`, `/exit`, `/quit`, `:q`, or `Ctrl+D`.
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 00000000..96b5fa5b
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,809 @@
+# Configuration
+
+Config file: `~/.nanobot/config.json`
+
+> [!NOTE]
+> If your config file is older than the current schema, you can refresh it without overwriting your existing values:
+> run `nanobot onboard`, then answer `N` when asked whether to overwrite the config.
+> nanobot will merge in missing default fields and keep your current settings.
+
+## Environment Variables for Secrets
+
+Instead of storing secrets directly in `config.json`, you can use `${VAR_NAME}` references that are resolved from environment variables at startup:
+
+```json
+{
+ "channels": {
+ "telegram": { "token": "${TELEGRAM_TOKEN}" },
+ "email": {
+ "imapPassword": "${IMAP_PASSWORD}",
+ "smtpPassword": "${SMTP_PASSWORD}"
+ }
+ },
+ "providers": {
+ "groq": { "apiKey": "${GROQ_API_KEY}" }
+ }
+}
+```
+
+For **systemd** deployments, use `EnvironmentFile=` in the service unit to load variables from a file that only the deploying user can read:
+
+```ini
+# /etc/systemd/system/nanobot.service (excerpt)
+[Service]
+EnvironmentFile=/home/youruser/nanobot_secrets.env
+User=nanobot
+ExecStart=...
+```
+
+```bash
+# /home/youruser/nanobot_secrets.env (mode 600, owned by youruser)
+TELEGRAM_TOKEN=your-token-here
+IMAP_PASSWORD=your-password-here
+```
+
+## Providers
+
+> [!TIP]
+> - **Voice transcription**: Voice messages (Telegram, WhatsApp) are automatically transcribed using Whisper. By default Groq is used (free tier). Set `"transcriptionProvider": "openai"` under `channels` to use OpenAI Whisper instead โ the API key is picked from the matching provider config.
+> - **MiniMax Coding Plan**: Exclusive discount links for the nanobot community: [Overseas](https://platform.minimax.io/subscribe/coding-plan?code=9txpdXw04g&source=link) ยท [Mainland China](https://platform.minimaxi.com/subscribe/token-plan?code=GILTJpMTqZ&source=link)
+> - **MiniMax (Mainland China)**: If your API key is from MiniMax's mainland China platform (minimaxi.com), set `"apiBase": "https://api.minimaxi.com/v1"` in your minimax provider config.
+> - **MiniMax thinking mode**: Use `providers.minimaxAnthropic` when you want `reasoningEffort` / thinking mode. MiniMax exposes that capability through its Anthropic-compatible endpoint, so nanobot keeps it as a separate provider instead of guessing MiniMax-specific thinking parameters on the generic OpenAI-compatible `minimax` endpoint. It uses the same `MINIMAX_API_KEY`. Default Anthropic-compatible base URL: `https://api.minimax.io/anthropic`; for mainland China use `https://api.minimaxi.com/anthropic`.
+> - **VolcEngine / BytePlus Coding Plan**: Use dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan` instead of the pay-per-use `volcengine` / `byteplus` providers.
+> - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config.
+> - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config.
+> - **Step Fun (Mainland China)**: If your API key is from Step Fun's mainland China platform (stepfun.com), set `"apiBase": "https://api.stepfun.com/v1"` in your stepfun provider config.
+
+| Provider | Purpose | Get API Key |
+|----------|---------|-------------|
+| `custom` | Any OpenAI-compatible endpoint | โ |
+| `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
+| `volcengine` | LLM (VolcEngine, pay-per-use) | [Coding Plan](https://www.volcengine.com/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) ยท [volcengine.com](https://www.volcengine.com) |
+| `byteplus` | LLM (VolcEngine international, pay-per-use) | [Coding Plan](https://www.byteplus.com/en/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) ยท [byteplus.com](https://www.byteplus.com) |
+| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
+| `azure_openai` | LLM (Azure OpenAI) | [portal.azure.com](https://portal.azure.com) |
+| `openai` | LLM + Voice transcription (Whisper) | [platform.openai.com](https://platform.openai.com) |
+| `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
+| `groq` | LLM + Voice transcription (Whisper, default) | [console.groq.com](https://console.groq.com) |
+| `minimax` | LLM (MiniMax direct) | [platform.minimaxi.com](https://platform.minimaxi.com) |
+| `minimax_anthropic` | LLM (MiniMax Anthropic-compatible endpoint, thinking mode) | [platform.minimaxi.com](https://platform.minimaxi.com) |
+| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
+| `aihubmix` | LLM (API gateway, access to all models) | [aihubmix.com](https://aihubmix.com) |
+| `siliconflow` | LLM (SiliconFlow/็ก
ๅบๆตๅจ) | [siliconflow.cn](https://siliconflow.cn) |
+| `dashscope` | LLM (Qwen) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
+| `moonshot` | LLM (Moonshot/Kimi) | [platform.moonshot.cn](https://platform.moonshot.cn) |
+| `zhipu` | LLM (Zhipu GLM) | [open.bigmodel.cn](https://open.bigmodel.cn) |
+| `mimo` | LLM (MiMo) | [platform.xiaomimimo.com](https://platform.xiaomimimo.com) |
+| `ollama` | LLM (local, Ollama) | โ |
+| `lm_studio` | LLM (local, LM Studio) | โ |
+| `mistral` | LLM | [docs.mistral.ai](https://docs.mistral.ai/) |
+| `stepfun` | LLM (Step Fun/้ถ่ทๆ่พฐ) | [platform.stepfun.com](https://platform.stepfun.com) |
+| `ovms` | LLM (local, OpenVINO Model Server) | [docs.openvino.ai](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html) |
+| `vllm` | LLM (local, any OpenAI-compatible server) | โ |
+| `openai_codex` | LLM (Codex, OAuth) | `nanobot provider login openai-codex` |
+| `github_copilot` | LLM (GitHub Copilot, OAuth) | `nanobot provider login github-copilot` |
+| `qianfan` | LLM (Baidu Qianfan) | [cloud.baidu.com](https://cloud.baidu.com/doc/qianfan/s/Hmh4suq26) |
+
+
+
+OpenAI Codex (OAuth)
+
+Codex uses OAuth instead of API keys. Requires a ChatGPT Plus or Pro account.
+No `providers.openaiCodex` block is needed in `config.json`; `nanobot provider login` stores the OAuth session outside config.
+
+**1. Login:**
+```bash
+nanobot provider login openai-codex
+```
+
+**2. Set model** (merge into `~/.nanobot/config.json`):
+```json
+{
+ "agents": {
+ "defaults": {
+ "model": "openai-codex/gpt-5.1-codex"
+ }
+ }
+}
+```
+
+**3. Chat:**
+```bash
+nanobot agent -m "Hello!"
+
+# Target a specific workspace/config locally
+nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello!"
+
+# One-off workspace override on top of that config
+nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test -m "Hello!"
+```
+
+> Docker users: use `docker run -it` for interactive OAuth login.
+
+
+
+
+
+GitHub Copilot (OAuth)
+
+GitHub Copilot uses OAuth instead of API keys. Requires a [GitHub account with a plan](https://github.com/features/copilot/plans) configured.
+No `providers.githubCopilot` block is needed in `config.json`; `nanobot provider login` stores the OAuth session outside config.
+
+**1. Login:**
+```bash
+nanobot provider login github-copilot
+```
+
+**2. Set model** (merge into `~/.nanobot/config.json`):
+```json
+{
+ "agents": {
+ "defaults": {
+ "model": "github-copilot/gpt-4.1"
+ }
+ }
+}
+```
+
+**3. Chat:**
+```bash
+nanobot agent -m "Hello!"
+
+# Target a specific workspace/config locally
+nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello!"
+
+# One-off workspace override on top of that config
+nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test -m "Hello!"
+```
+
+> Docker users: use `docker run -it` for interactive OAuth login.
+
+
+
+
+Custom Provider (Any OpenAI-compatible API)
+
+Connects directly to any OpenAI-compatible endpoint โ llama.cpp, Together AI, Fireworks, Azure OpenAI, or any self-hosted server. Model name is passed as-is.
+
+```json
+{
+ "providers": {
+ "custom": {
+ "apiKey": "your-api-key",
+ "apiBase": "https://api.your-provider.com/v1"
+ }
+ },
+ "agents": {
+ "defaults": {
+ "model": "your-model-name"
+ }
+ }
+}
+```
+
+> For local servers that don't require authentication, set `apiKey` to `null`.
+>
+> `custom` is the right choice for providers that expose an OpenAI-compatible **chat completions** API. It does **not** force third-party endpoints onto the OpenAI/Azure **Responses API**.
+>
+> If your proxy or gateway is specifically Responses-API-compatible, use the `azure_openai` provider shape instead and point `apiBase` at that endpoint:
+>
+> ```json
+> {
+> "providers": {
+> "azure_openai": {
+> "apiKey": "your-api-key",
+> "apiBase": "https://api.your-provider.com",
+> "defaultModel": "your-model-name"
+> }
+> },
+> "agents": {
+> "defaults": {
+> "provider": "azure_openai",
+> "model": "your-model-name"
+> }
+> }
+> }
+> ```
+>
+> In short: **chat-completions-compatible endpoint โ `custom`**; **Responses-compatible endpoint โ `azure_openai`**.
+
+
+
+
+Ollama (local)
+
+Run a local model with Ollama, then add to config:
+
+**1. Start Ollama** (example):
+```bash
+ollama run llama3.2
+```
+
+**2. Add to config** (partial โ merge into `~/.nanobot/config.json`):
+```json
+{
+ "providers": {
+ "ollama": {
+ "apiBase": "http://localhost:11434"
+ }
+ },
+ "agents": {
+ "defaults": {
+ "provider": "ollama",
+ "model": "llama3.2"
+ }
+ }
+}
+```
+
+> `provider: "auto"` also works when `providers.ollama.apiBase` is configured, but setting `"provider": "ollama"` is the clearest option.
+
+
+
+
+LM Studio (local)
+
+[LM Studio](https://lmstudio.ai/) provides a local OpenAI-compatible server for running LLMs. Download models through the LM Studio UI, then start the local server.
+
+**1. Start LM Studio server:**
+- Launch LM Studio
+- Go to the "Local Server" tab
+- Load a model (e.g., Llama, Mistral, Qwen)
+- Click "Start Server" (default port: 1234)
+
+**2. Add to config** (partial โ merge into `~/.nanobot/config.json`):
+```json
+{
+ "providers": {
+ "lm_studio": {
+ "apiKey": null,
+ "apiBase": "http://localhost:1234/v1"
+ }
+ },
+ "agents": {
+ "defaults": {
+ "provider": "lm_studio",
+ "model": "local-model"
+ }
+ }
+}
+```
+
+> **Note:** Set `apiKey` to `null` for LM Studio since it runs locally and doesn't require authentication. The model name should match what's shown in the LM Studio UI.
+> `provider: "auto"` also works when `providers.lm_studio.apiBase` is configured, but setting `"provider": "lm_studio"` is the clearest option.
+
+
+
+
+OpenVINO Model Server (local / OpenAI-compatible)
+
+Run LLMs locally on Intel GPUs using [OpenVINO Model Server](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html). OVMS exposes an OpenAI-compatible API at `/v3`.
+
+> Requires Docker and an Intel GPU with driver access (`/dev/dri`).
+
+**1. Pull the model** (example):
+
+```bash
+mkdir -p ov/models && cd ov
+
+docker run -d \
+ --rm \
+ --user $(id -u):$(id -g) \
+ -v $(pwd)/models:/models \
+ openvino/model_server:latest-gpu \
+ --pull \
+ --model_name openai/gpt-oss-20b \
+ --model_repository_path /models \
+ --source_model OpenVINO/gpt-oss-20b-int4-ov \
+ --task text_generation \
+ --tool_parser gptoss \
+ --reasoning_parser gptoss \
+ --enable_prefix_caching true \
+ --target_device GPU
+```
+
+> This downloads the model weights. Wait for the container to finish before proceeding.
+
+**2. Start the server** (example):
+
+```bash
+docker run -d \
+ --rm \
+ --name ovms \
+ --user $(id -u):$(id -g) \
+ -p 8000:8000 \
+ -v $(pwd)/models:/models \
+ --device /dev/dri \
+ --group-add=$(stat -c "%g" /dev/dri/render* | head -n 1) \
+ openvino/model_server:latest-gpu \
+ --rest_port 8000 \
+ --model_name openai/gpt-oss-20b \
+ --model_repository_path /models \
+ --source_model OpenVINO/gpt-oss-20b-int4-ov \
+ --task text_generation \
+ --tool_parser gptoss \
+ --reasoning_parser gptoss \
+ --enable_prefix_caching true \
+ --target_device GPU
+```
+
+**3. Add to config** (partial โ merge into `~/.nanobot/config.json`):
+
+```json
+{
+ "providers": {
+ "ovms": {
+ "apiBase": "http://localhost:8000/v3"
+ }
+ },
+ "agents": {
+ "defaults": {
+ "provider": "ovms",
+ "model": "openai/gpt-oss-20b"
+ }
+ }
+}
+```
+
+> OVMS is a local server โ no API key required. Supports tool calling (`--tool_parser gptoss`), reasoning (`--reasoning_parser gptoss`), and streaming.
+> See the [official OVMS docs](https://docs.openvino.ai/2026/model-server/ovms_docs_llm_quickstart.html) for more details.
+
+
+
+vLLM (local / OpenAI-compatible)
+
+Run your own model with vLLM or any OpenAI-compatible server, then add to config:
+
+**1. Start the server** (example):
+```bash
+vllm serve meta-llama/Llama-3.1-8B-Instruct --port 8000
+```
+
+**2. Add to config** (partial โ merge into `~/.nanobot/config.json`):
+
+*Provider (set API key to null for local servers):*
+```json
+{
+ "providers": {
+ "vllm": {
+ "apiKey": null,
+ "apiBase": "http://localhost:8000/v1"
+ }
+ }
+}
+```
+
+*Model:*
+```json
+{
+ "agents": {
+ "defaults": {
+ "model": "meta-llama/Llama-3.1-8B-Instruct"
+ }
+ }
+}
+```
+
+
+
+
+Adding a New Provider (Developer Guide)
+
+nanobot uses a **Provider Registry** (`nanobot/providers/registry.py`) as the single source of truth.
+Adding a new provider only takes **2 steps** โ no if-elif chains to touch.
+
+**Step 1.** Add a `ProviderSpec` entry to `PROVIDERS` in `nanobot/providers/registry.py`:
+
+```python
+ProviderSpec(
+ name="myprovider", # config field name
+ keywords=("myprovider", "mymodel"), # model-name keywords for auto-matching
+ env_key="MYPROVIDER_API_KEY", # env var name
+ display_name="My Provider", # shown in `nanobot status`
+ default_api_base="https://api.myprovider.com/v1", # OpenAI-compatible endpoint
+)
+```
+
+**Step 2.** Add a field to `ProvidersConfig` in `nanobot/config/schema.py`:
+
+```python
+class ProvidersConfig(BaseModel):
+ ...
+ myprovider: ProviderConfig = ProviderConfig()
+```
+
+That's it! Environment variables, model routing, config matching, and `nanobot status` display will all work automatically.
+
+**Common `ProviderSpec` options:**
+
+| Field | Description | Example |
+|-------|-------------|---------|
+| `default_api_base` | OpenAI-compatible base URL | `"https://api.deepseek.com"` |
+| `env_extras` | Additional env vars to set | `(("ZHIPUAI_API_KEY", "{api_key}"),)` |
+| `model_overrides` | Per-model parameter overrides | `(("kimi-k2.5", {"temperature": 1.0}),)` |
+| `is_gateway` | Can route any model (like OpenRouter) | `True` |
+| `detect_by_key_prefix` | Detect gateway by API key prefix | `"sk-or-"` |
+| `detect_by_base_keyword` | Detect gateway by API base URL | `"openrouter"` |
+| `strip_model_prefix` | Strip provider prefix before sending to gateway | `True` (for AiHubMix) |
+| `supports_max_completion_tokens` | Use `max_completion_tokens` instead of `max_tokens`; required for providers that reject both being set simultaneously (e.g. VolcEngine) | `True` |
+
+
+
+## Channel Settings
+
+Global settings that apply to all channels. Configure under the `channels` section in `~/.nanobot/config.json`:
+
+```json
+{
+ "channels": {
+ "sendProgress": true,
+ "sendToolHints": false,
+ "sendMaxRetries": 3,
+ "transcriptionProvider": "groq",
+ "telegram": { ... }
+ }
+}
+```
+
+| Setting | Default | Description |
+|---------|---------|-------------|
+| `sendProgress` | `true` | Stream agent's text progress to the channel |
+| `sendToolHints` | `false` | Stream tool-call hints (e.g. `read_file("โฆ")`) |
+| `sendMaxRetries` | `3` | Max delivery attempts per outbound message, including the initial send (0-10 configured, minimum 1 actual attempt) |
+| `transcriptionProvider` | `"groq"` | Voice transcription backend: `"groq"` (free tier, default) or `"openai"`. API key is auto-resolved from the matching provider config. |
+
+### Retry Behavior
+
+Retry is intentionally simple.
+
+When a channel `send()` raises, nanobot retries at the channel-manager layer. By default, `channels.sendMaxRetries` is `3`, and that count includes the initial send.
+
+- **Attempt 1**: Send immediately
+- **Attempt 2**: Retry after `1s`
+- **Attempt 3**: Retry after `2s`
+- **Higher retry budgets**: Backoff continues as `1s`, `2s`, `4s`, then stays capped at `4s`
+- **Transient failures**: Network hiccups and temporary API limits often recover on the next attempt
+- **Permanent failures**: Invalid tokens, revoked access, or banned channels will exhaust the retry budget and fail cleanly
+
+> [!NOTE]
+> This design is deliberate: channel implementations should raise on delivery failure, and the channel manager owns the shared retry policy.
+>
+> Some channels may still apply small API-specific retries internally. For example, Telegram separately retries timeout and flood-control errors before surfacing a final failure to the manager.
+>
+> If a channel is completely unreachable, nanobot cannot notify the user through that same channel. Watch logs for `Failed to send to {channel} after N attempts` to spot persistent delivery failures.
+
+## Web Search
+
+> [!TIP]
+> Use `proxy` in `tools.web` to route all web requests (search + fetch) through a proxy:
+> ```json
+> { "tools": { "web": { "proxy": "http://127.0.0.1:7890" } } }
+> ```
+
+nanobot supports multiple web search providers. Configure in `~/.nanobot/config.json` under `tools.web.search`.
+
+By default, web tools are enabled and web search uses `duckduckgo`, so search works out of the box without an API key.
+
+If you want to disable all built-in web tools entirely, set `tools.web.enable` to `false`. This removes both `web_search` and `web_fetch` from the tool list sent to the LLM.
+
+If you need to allow trusted private ranges such as Tailscale / CGNAT addresses, you can explicitly exempt them from SSRF blocking with `tools.ssrfWhitelist`:
+
+```json
+{
+ "tools": {
+ "ssrfWhitelist": ["100.64.0.0/10"]
+ }
+}
+```
+
+| Provider | Config fields | Env var fallback | Free |
+|----------|--------------|------------------|------|
+| `brave` | `apiKey` | `BRAVE_API_KEY` | No |
+| `tavily` | `apiKey` | `TAVILY_API_KEY` | No |
+| `jina` | `apiKey` | `JINA_API_KEY` | Free tier (10M tokens) |
+| `kagi` | `apiKey` | `KAGI_API_KEY` | No |
+| `searxng` | `baseUrl` | `SEARXNG_BASE_URL` | Yes (self-hosted) |
+| `duckduckgo` (default) | โ | โ | Yes |
+
+**Disable all built-in web tools:**
+```json
+{
+ "tools": {
+ "web": {
+ "enable": false
+ }
+ }
+}
+```
+
+**Brave:**
+```json
+{
+ "tools": {
+ "web": {
+ "search": {
+ "provider": "brave",
+ "apiKey": "BSA..."
+ }
+ }
+ }
+}
+```
+
+**Tavily:**
+```json
+{
+ "tools": {
+ "web": {
+ "search": {
+ "provider": "tavily",
+ "apiKey": "tvly-..."
+ }
+ }
+ }
+}
+```
+
+**Jina** (free tier with 10M tokens):
+```json
+{
+ "tools": {
+ "web": {
+ "search": {
+ "provider": "jina",
+ "apiKey": "jina_..."
+ }
+ }
+ }
+}
+```
+
+**Kagi:**
+```json
+{
+ "tools": {
+ "web": {
+ "search": {
+ "provider": "kagi",
+ "apiKey": "your-kagi-api-key"
+ }
+ }
+ }
+}
+```
+
+**SearXNG** (self-hosted, no API key needed):
+```json
+{
+ "tools": {
+ "web": {
+ "search": {
+ "provider": "searxng",
+ "baseUrl": "https://searx.example"
+ }
+ }
+ }
+}
+```
+
+**DuckDuckGo** (zero config):
+```json
+{
+ "tools": {
+ "web": {
+ "search": {
+ "provider": "duckduckgo"
+ }
+ }
+ }
+}
+```
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enable` | boolean | `true` | Enable or disable all built-in web tools (`web_search` + `web_fetch`) |
+| `proxy` | string or null | `null` | Proxy for all web requests, for example `http://127.0.0.1:7890` |
+
+### `tools.web.search`
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `provider` | string | `"duckduckgo"` | Search backend: `brave`, `tavily`, `jina`, `searxng`, `duckduckgo` |
+| `apiKey` | string | `""` | API key for Brave or Tavily |
+| `baseUrl` | string | `""` | Base URL for SearXNG |
+| `maxResults` | integer | `5` | Results per search (1โ10) |
+
+## MCP (Model Context Protocol)
+
+> [!TIP]
+> The config format is compatible with Claude Desktop / Cursor. You can copy MCP server configs directly from any MCP server's README.
+
+nanobot supports [MCP](https://modelcontextprotocol.io/) โ connect external tool servers and use them as native agent tools.
+
+Add MCP servers to your `config.json`:
+
+```json
+{
+ "tools": {
+ "mcpServers": {
+ "filesystem": {
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
+ },
+ "my-remote-mcp": {
+ "url": "https://example.com/mcp/",
+ "headers": {
+ "Authorization": "Bearer xxxxx"
+ }
+ }
+ }
+ }
+}
+```
+
+Two transport modes are supported:
+
+| Mode | Config | Example |
+|------|--------|---------|
+| **Stdio** | `command` + `args` | Local process via `npx` / `uvx` |
+| **HTTP** | `url` + `headers` (optional) | Remote endpoint (`https://mcp.example.com/sse`) |
+
+Use `toolTimeout` to override the default 30s per-call timeout for slow servers:
+
+```json
+{
+ "tools": {
+ "mcpServers": {
+ "my-slow-server": {
+ "url": "https://example.com/mcp/",
+ "toolTimeout": 120
+ }
+ }
+ }
+}
+```
+
+Use `enabledTools` to register only a subset of tools from an MCP server:
+
+```json
+{
+ "tools": {
+ "mcpServers": {
+ "filesystem": {
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
+ "enabledTools": ["read_file", "mcp_filesystem_write_file"]
+ }
+ }
+ }
+}
+```
+
+`enabledTools` accepts either the raw MCP tool name (for example `read_file`) or the wrapped nanobot tool name (for example `mcp_filesystem_write_file`).
+
+- Omit `enabledTools`, or set it to `["*"]`, to register all tools.
+- Set `enabledTools` to `[]` to register no tools from that server.
+- Set `enabledTools` to a non-empty list of names to register only that subset.
+
+MCP tools are automatically discovered and registered on startup. The LLM can use them alongside built-in tools โ no extra configuration needed.
+
+
+
+
+## Security
+
+> [!TIP]
+> For production deployments, set `"restrictToWorkspace": true` and `"tools.exec.sandbox": "bwrap"` in your config to sandbox the agent.
+> In `v0.1.4.post3` and earlier, an empty `allowFrom` allowed all senders. Since `v0.1.4.post4`, empty `allowFrom` denies all access by default. To allow all senders, set `"allowFrom": ["*"]`.
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `tools.restrictToWorkspace` | `false` | When `true`, restricts **all** agent tools (shell, file read/write/edit, list) to the workspace directory. Prevents path traversal and out-of-scope access. |
+| `tools.exec.sandbox` | `""` | Sandbox backend for shell commands. Set to `"bwrap"` to wrap exec calls in a [bubblewrap](https://github.com/containers/bubblewrap) sandbox โ the process can only see the workspace (read-write) and media directory (read-only); config files and API keys are hidden. Automatically enables `restrictToWorkspace` for file tools. **Linux only** โ requires `bwrap` installed (`apt install bubblewrap`; pre-installed in the Docker image). Not available on macOS or Windows (bwrap depends on Linux kernel namespaces). |
+| `tools.exec.enable` | `true` | When `false`, the shell `exec` tool is not registered at all. Use this to completely disable shell command execution. |
+| `tools.exec.pathAppend` | `""` | Extra directories to append to `PATH` when running shell commands (e.g. `/usr/sbin` for `ufw`). |
+| `channels.*.allowFrom` | `[]` (deny all) | Whitelist of user IDs. Empty denies all; use `["*"]` to allow everyone. |
+
+**Docker security**: The official Docker image runs as a non-root user (`nanobot`, UID 1000) with bubblewrap pre-installed. When using `docker-compose.yml`, the container drops all Linux capabilities except `SYS_ADMIN` (required for bwrap's namespace isolation).
+
+
+## Auto Compact
+
+When a user is idle for longer than a configured threshold, nanobot **proactively** compresses the older part of the session context into a summary while keeping a recent legal suffix of live messages. This reduces token cost and first-token latency when the user returns โ instead of re-processing a long stale context with an expired KV cache, the model receives a compact summary, the most recent live context, and fresh input.
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "idleCompactAfterMinutes": 15
+ }
+ }
+}
+```
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `agents.defaults.idleCompactAfterMinutes` | `0` (disabled) | Minutes of idle time before auto-compaction starts. Set to `0` to disable. Recommended: `15` โ close to a typical LLM KV cache expiry window, so stale sessions get compacted before the user returns. |
+
+`sessionTtlMinutes` remains accepted as a legacy alias for backward compatibility, but `idleCompactAfterMinutes` is the preferred config key going forward.
+
+How it works:
+1. **Idle detection**: On each idle tick (~1 s), checks all sessions for expiration.
+2. **Background compaction**: Idle sessions summarize the older live prefix via LLM and keep the most recent legal suffix (currently 8 messages).
+3. **Summary injection**: When the user returns, the summary is injected as runtime context (one-shot, not persisted) alongside the retained recent suffix.
+4. **Restart-safe resume**: The summary is also mirrored into session metadata so it can still be recovered after a process restart.
+
+> [!NOTE]
+> Mental model: "summarize older context, keep the freshest live turns, **and overwrite the session file with the compact form.**" It is not a full `session.clear()`, but it is a write โ not a soft cursor move.
+>
+> Concretely, auto compact rewrites `sessions/.jsonl` in place: older messages (including their structured `tool_calls` / `tool_call_id` / `reasoning_content`) are replaced by just the retained recent suffix (currently 8 messages), while the archived prefix is preserved only as a plain-text summary appended to `memory/history.jsonl` (or a `[RAW] ...` flattened dump if LLM summarization fails). The original structured JSON of those turns is no longer recoverable from the session file.
+>
+> This differs from the **token-driven soft consolidation** that fires when a prompt exceeds the context budget: that path only advances an internal `last_consolidated` cursor and leaves the session file untouched, so the raw tool-call trail stays on disk and can still be replayed or audited. If you rely on that trail for debugging or auditing, leave `idleCompactAfterMinutes` at the default `0` and let only the token-driven path run.
+
+## Timezone
+
+Time is context. Context should be precise.
+
+By default, nanobot uses `UTC` for runtime time context. If you want the agent to think in your local time, set `agents.defaults.timezone` to a valid [IANA timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones):
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "timezone": "Asia/Shanghai"
+ }
+ }
+}
+```
+
+This affects runtime time strings shown to the model, such as runtime context and heartbeat prompts. It also becomes the default timezone for cron schedules when a cron expression omits `tz`, and for one-shot `at` times when the ISO datetime has no explicit offset.
+
+Common examples: `UTC`, `America/New_York`, `America/Los_Angeles`, `Europe/London`, `Europe/Berlin`, `Asia/Tokyo`, `Asia/Shanghai`, `Asia/Singapore`, `Australia/Sydney`.
+
+> Need another timezone? Browse the full [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
+
+## Unified Session
+
+By default, each channel ร chat ID combination gets its own session. If you use nanobot across multiple channels (e.g. Telegram + Discord + CLI) and want them to share the same conversation, enable `unifiedSession`:
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "unifiedSession": true
+ }
+ }
+}
+```
+
+When enabled, all incoming messages โ regardless of which channel they arrive on โ are routed into a single shared session. Switching from Telegram to Discord (or any other channel) continues the same conversation seamlessly.
+
+| Behavior | `false` (default) | `true` |
+|----------|-------------------|--------|
+| Session key | `channel:chat_id` | `unified:default` |
+| Cross-channel continuity | No | Yes |
+| `/new` clears | Current channel session | Shared session |
+| `/stop` finds tasks | By channel session | By shared session |
+| Existing `session_key_override` (e.g. Telegram thread) | Respected | Still respected โ not overwritten |
+
+> This is designed for single-user, multi-device setups. It is **off by default** โ existing users see zero behavior change.
+
+## Disabled Skills
+
+nanobot ships with built-in skills, and your workspace can also define custom skills under `skills/`. If you want to hide specific skills from the agent, set `agents.defaults.disabledSkills` to a list of skill directory names:
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "disabledSkills": ["github", "weather"]
+ }
+ }
+}
+```
+
+Disabled skills are excluded from the main agent's skill summary, from always-on skill injection, and from subagent skill summaries. This is useful when some bundled skills are unnecessary for your deployment or should not be exposed to end users.
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `agents.defaults.disabledSkills` | `[]` | List of skill directory names to exclude from loading. Applies to both built-in skills and workspace skills. |
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 00000000..ad6283c0
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,94 @@
+# Deployment
+
+## Docker
+
+> [!TIP]
+> The `-v ~/.nanobot:/home/nanobot/.nanobot` flag mounts your local config directory into the container, so your config and workspace persist across container restarts.
+> The container runs as user `nanobot` (UID 1000). If you get **Permission denied**, fix ownership on the host first: `sudo chown -R 1000:1000 ~/.nanobot`, or pass `--user $(id -u):$(id -g)` to match your host UID. Podman users can use `--userns=keep-id` instead.
+
+### Docker Compose
+
+```bash
+docker compose run --rm nanobot-cli onboard # first-time setup
+vim ~/.nanobot/config.json # add API keys
+docker compose up -d nanobot-gateway # start gateway
+```
+
+```bash
+docker compose run --rm nanobot-cli agent -m "Hello!" # run CLI
+docker compose logs -f nanobot-gateway # view logs
+docker compose down # stop
+```
+
+### Docker
+
+```bash
+# Build the image
+docker build -t nanobot .
+
+# Initialize config (first time only)
+docker run -v ~/.nanobot:/home/nanobot/.nanobot --rm nanobot onboard
+
+# Edit config on host to add API keys
+vim ~/.nanobot/config.json
+
+# Run gateway (connects to enabled channels, e.g. Telegram/Discord/Mochat)
+docker run -v ~/.nanobot:/home/nanobot/.nanobot -p 18790:18790 nanobot gateway
+
+# Or run a single command
+docker run -v ~/.nanobot:/home/nanobot/.nanobot --rm nanobot agent -m "Hello!"
+docker run -v ~/.nanobot:/home/nanobot/.nanobot --rm nanobot status
+```
+
+## Linux Service
+
+Run the gateway as a systemd user service so it starts automatically and restarts on failure.
+
+**1. Find the nanobot binary path:**
+
+```bash
+which nanobot # e.g. /home/user/.local/bin/nanobot
+```
+
+**2. Create the service file** at `~/.config/systemd/user/nanobot-gateway.service` (replace `ExecStart` path if needed):
+
+```ini
+[Unit]
+Description=Nanobot Gateway
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=%h/.local/bin/nanobot gateway
+Restart=always
+RestartSec=10
+NoNewPrivileges=yes
+ProtectSystem=strict
+ReadWritePaths=%h
+
+[Install]
+WantedBy=default.target
+```
+
+**3. Enable and start:**
+
+```bash
+systemctl --user daemon-reload
+systemctl --user enable --now nanobot-gateway
+```
+
+**Common operations:**
+
+```bash
+systemctl --user status nanobot-gateway # check status
+systemctl --user restart nanobot-gateway # restart after config changes
+journalctl --user -u nanobot-gateway -f # follow logs
+```
+
+If you edit the `.service` file itself, run `systemctl --user daemon-reload` before restarting.
+
+> **Note:** User services only run while you are logged in. To keep the gateway running after logout, enable lingering:
+>
+> ```bash
+> loginctl enable-linger $USER
+> ```
diff --git a/docs/features-showcase.md b/docs/features-showcase.md
new file mode 100644
index 00000000..bb988b06
--- /dev/null
+++ b/docs/features-showcase.md
@@ -0,0 +1,22 @@
+# Feature Showcase
+
+
+
+ ๐ 24/7 Real-Time Market Analysis |
+ ๐ Full-Stack Software Engineer |
+ ๐
Smart Daily Routine Manager |
+ ๐ Personal Knowledge Assistant |
+
+
+ 
|
+ 
|
+ 
|
+ 
|
+
+
+ | Discovery โข Insights โข Trends |
+ Develop โข Deploy โข Scale |
+ Schedule โข Automate โข Organize |
+ Learn โข Memory โข Reasoning |
+
+
diff --git a/docs/MEMORY.md b/docs/memory.md
similarity index 99%
rename from docs/MEMORY.md
rename to docs/memory.md
index 414fcdca..2fde1c68 100644
--- a/docs/MEMORY.md
+++ b/docs/memory.md
@@ -65,7 +65,7 @@ This is why nanobot's memory is not just archival. It is interpretive.
## The Files
-```
+```text
workspace/
โโโ SOUL.md # The bot's long-term voice and communication style
โโโ USER.md # Stable knowledge about the user
diff --git a/docs/multiple-instances.md b/docs/multiple-instances.md
new file mode 100644
index 00000000..d7c54cc0
--- /dev/null
+++ b/docs/multiple-instances.md
@@ -0,0 +1,126 @@
+# Multiple Instances
+
+Run multiple nanobot instances simultaneously with separate configs and runtime data. Use `--config` as the main entrypoint. Optionally pass `--workspace` during `onboard` when you want to initialize or update the saved workspace for a specific instance.
+
+## Quick Start
+
+If you want each instance to have its own dedicated workspace from the start, pass both `--config` and `--workspace` during onboarding.
+
+**Initialize instances:**
+
+```bash
+# Create separate instance configs and workspaces
+nanobot onboard --config ~/.nanobot-telegram/config.json --workspace ~/.nanobot-telegram/workspace
+nanobot onboard --config ~/.nanobot-discord/config.json --workspace ~/.nanobot-discord/workspace
+nanobot onboard --config ~/.nanobot-feishu/config.json --workspace ~/.nanobot-feishu/workspace
+```
+
+**Configure each instance:**
+
+Edit `~/.nanobot-telegram/config.json`, `~/.nanobot-discord/config.json`, etc. with different channel settings. The workspace you passed during `onboard` is saved into each config as that instance's default workspace.
+
+**Run instances:**
+
+```bash
+# Instance A - Telegram bot
+nanobot gateway --config ~/.nanobot-telegram/config.json
+
+# Instance B - Discord bot
+nanobot gateway --config ~/.nanobot-discord/config.json
+
+# Instance C - Feishu bot with custom port
+nanobot gateway --config ~/.nanobot-feishu/config.json --port 18792
+```
+
+## Path Resolution
+
+When using `--config`, nanobot derives its runtime data directory from the config file location. The workspace still comes from `agents.defaults.workspace` unless you override it with `--workspace`.
+
+To open a CLI session against one of these instances locally:
+
+```bash
+nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello from Telegram instance"
+nanobot agent -c ~/.nanobot-discord/config.json -m "Hello from Discord instance"
+
+# Optional one-off workspace override
+nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test
+```
+
+> `nanobot agent` starts a local CLI agent using the selected workspace/config. It does not attach to or proxy through an already running `nanobot gateway` process.
+
+| Component | Resolved From | Example |
+|-----------|---------------|---------|
+| **Config** | `--config` path | `~/.nanobot-A/config.json` |
+| **Workspace** | `--workspace` or config | `~/.nanobot-A/workspace/` |
+| **Cron Jobs** | config directory | `~/.nanobot-A/cron/` |
+| **Media / runtime state** | config directory | `~/.nanobot-A/media/` |
+
+## How It Works
+
+- `--config` selects which config file to load
+- By default, the workspace comes from `agents.defaults.workspace` in that config
+- If you pass `--workspace`, it overrides the workspace from the config file
+
+## Minimal Setup
+
+1. Copy your base config into a new instance directory.
+2. Set a different `agents.defaults.workspace` for that instance.
+3. Start the instance with `--config`.
+
+Example config:
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "workspace": "~/.nanobot-telegram/workspace",
+ "model": "anthropic/claude-sonnet-4-6"
+ }
+ },
+ "channels": {
+ "telegram": {
+ "enabled": true,
+ "token": "YOUR_TELEGRAM_BOT_TOKEN"
+ }
+ },
+ "gateway": {
+ "host": "127.0.0.1",
+ "port": 18790
+ }
+}
+```
+
+Start separate instances:
+
+```bash
+nanobot gateway --config ~/.nanobot-telegram/config.json
+nanobot gateway --config ~/.nanobot-discord/config.json
+```
+
+Each gateway instance also exposes a lightweight HTTP health endpoint on
+`gateway.host:gateway.port`. By default, the gateway binds to `127.0.0.1`,
+so the endpoint stays local unless you explicitly set `gateway.host` to a
+public or LAN-facing address.
+
+- `GET /health` returns `{"status":"ok"}`
+- Other paths return `404`
+
+Override workspace for one-off runs when needed:
+
+```bash
+nanobot gateway --config ~/.nanobot-telegram/config.json --workspace /tmp/nanobot-telegram-test
+```
+
+## Common Use Cases
+
+- Run separate bots for Telegram, Discord, Feishu, and other platforms
+- Keep testing and production instances isolated
+- Use different models or providers for different teams
+- Serve multiple tenants with separate configs and runtime data
+
+## Notes
+
+- Each instance must use a different port if they run at the same time
+- Use a different workspace per instance if you want isolated memory, sessions, and skills
+- `--workspace` overrides the workspace defined in the config file
+- Cron jobs and runtime media/state are derived from the config directory
diff --git a/docs/MY_TOOL.md b/docs/my-tool.md
similarity index 98%
rename from docs/MY_TOOL.md
rename to docs/my-tool.md
index a8a273d1..bc22ed5a 100644
--- a/docs/MY_TOOL.md
+++ b/docs/my-tool.md
@@ -36,7 +36,7 @@ All modifications are held in memory only โ restart restores defaults.
Without parameters, returns a key config overview:
-```
+```text
my(action="check")
# โ max_iterations: 40
# context_window_tokens: 65536
@@ -51,7 +51,7 @@ my(action="check")
With a key parameter, drill into a specific config:
-```
+```text
my(action="check", key="_last_usage.prompt_tokens")
# โ How many prompt tokens I've used so far
@@ -79,7 +79,7 @@ my(action="check", key="web_config.enable")
Changes take effect immediately, no restart required.
-```
+```text
my(action="set", key="max_iterations", value=80)
# โ Bump iteration limit from 40 to 80
@@ -92,7 +92,7 @@ my(action="set", key="context_window_tokens", value=131072)
You can also store custom state in your scratchpad:
-```
+```text
my(action="set", key="current_project", value="nanobot")
my(action="set", key="user_style_preference", value="concise")
my(action="set", key="task_complexity", value="high")
@@ -117,21 +117,21 @@ Other parameters (e.g. `workspace`, `provider_retry_mode`, `max_tool_result_char
### "This task is complex, I need more room"
-```
+```text
Agent: This codebase is large, let me expand my context window to handle it.
โ my(action="set", key="context_window_tokens", value=131072)
```
### "Simple question, don't waste compute"
-```
+```text
Agent: This is a straightforward question, let me switch to a faster model.
โ my(action="set", key="model", value="fast-model")
```
### "Remember user preferences across turns"
-```
+```text
Turn 1: my(action="set", key="user_prefers_concise", value=True)
Turn 2: my(action="check", key="user_prefers_concise")
# โ True (still remembers the user likes concise replies)
@@ -139,7 +139,7 @@ Turn 2: my(action="check", key="user_prefers_concise")
### "Self-diagnosis"
-```
+```text
User: "Why aren't you searching the web?"
Agent: Let me check my web config.
โ my(action="check", key="web_config.enable")
@@ -149,7 +149,7 @@ Agent: Web search is disabled โ please set web.enable: true in your config.
### "Token budget management"
-```
+```text
Agent: Let me check how much budget I have left.
โ my(action="check", key="_last_usage")
# โ {"prompt_tokens": 45000, "completion_tokens": 8000}
@@ -158,7 +158,7 @@ Agent: I've used ~53k tokens total so far. I'll keep my remaining replies concis
### "Subagent monitoring"
-```
+```text
Agent: Let me check on the background tasks.
โ my(action="check", key="subagents")
# โ 2 subagent(s):
diff --git a/docs/openai-api.md b/docs/openai-api.md
new file mode 100644
index 00000000..c88a8bed
--- /dev/null
+++ b/docs/openai-api.md
@@ -0,0 +1,121 @@
+# OpenAI-Compatible API
+
+nanobot can expose a minimal OpenAI-compatible endpoint for local integrations:
+
+```bash
+pip install "nanobot-ai[api]"
+nanobot serve
+```
+
+By default, the API binds to `127.0.0.1:8900`. You can change this in `config.json`.
+
+## Behavior
+
+- Session isolation: pass `"session_id"` in the request body to isolate conversations; omit for a shared default session (`api:default`)
+- Single-message input: each request must contain exactly one `user` message
+- Fixed model: omit `model`, or pass the same model shown by `/v1/models`
+- Streaming: set `stream=true` to receive Server-Sent Events (`text/event-stream`) with OpenAI-compatible delta chunks, terminated by `data: [DONE]`; omit or set `stream=false` for a single JSON response
+- **File uploads**: supports images, PDF, Word (.docx), Excel (.xlsx), PowerPoint (.pptx) via JSON base64 or `multipart/form-data` (max 10MB per file)
+- API requests run in the synthetic `api` channel, so the `message` tool does **not** automatically deliver to Telegram/Discord/etc. To proactively send to another chat, call `message` with an explicit `channel` and `chat_id` for an enabled channel.
+
+Example tool call for cross-channel delivery from an API session:
+
+```json
+{
+ "content": "Build finished successfully.",
+ "channel": "telegram",
+ "chat_id": "123456789"
+}
+```
+
+If `channel` points to a channel that is not enabled in your config, nanobot will queue the outbound event but no platform delivery will occur.
+
+## Endpoints
+
+- `GET /health`
+- `GET /v1/models`
+- `POST /v1/chat/completions`
+
+## curl
+
+```bash
+curl http://127.0.0.1:8900/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -d '{
+ "messages": [{"role": "user", "content": "hi"}],
+ "session_id": "my-session"
+ }'
+```
+
+## File Upload (JSON base64)
+
+Send images inline using the OpenAI multimodal content format:
+
+```bash
+curl http://127.0.0.1:8900/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -d '{
+ "messages": [{"role": "user", "content": [
+ {"type": "text", "text": "Describe this image"},
+ {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR..."}}
+ ]}]
+ }'
+```
+
+## File Upload (multipart/form-data)
+
+Upload any supported file type (images, PDF, Word, Excel, PPT) via multipart:
+
+```bash
+# Single file
+curl http://127.0.0.1:8900/v1/chat/completions \
+ -F "message=Summarize this report" \
+ -F "files=@report.docx"
+
+# Multiple files with session isolation
+curl http://127.0.0.1:8900/v1/chat/completions \
+ -F "message=Compare these files" \
+ -F "files=@chart.png" \
+ -F "files=@data.xlsx" \
+ -F "session_id=my-session"
+```
+
+Supported file types:
+- **Images**: PNG, JPEG, GIF, WebP (sent to AI as base64 for vision analysis)
+- **Documents**: PDF, Word (.docx), Excel (.xlsx), PowerPoint (.pptx) (text extracted and sent to AI)
+- **Text**: TXT, Markdown, CSV, JSON, etc. (read directly)
+
+## Python (`requests`)
+
+```python
+import requests
+
+resp = requests.post(
+ "http://127.0.0.1:8900/v1/chat/completions",
+ json={
+ "messages": [{"role": "user", "content": "hi"}],
+ "session_id": "my-session", # optional: isolate conversation
+ },
+ timeout=120,
+)
+resp.raise_for_status()
+print(resp.json()["choices"][0]["message"]["content"])
+```
+
+## Python (`openai`)
+
+```python
+from openai import OpenAI
+
+client = OpenAI(
+ base_url="http://127.0.0.1:8900/v1",
+ api_key="dummy",
+)
+
+resp = client.chat.completions.create(
+ model="MiniMax-M2.7",
+ messages=[{"role": "user", "content": "hi"}],
+ extra_body={"session_id": "my-session"}, # optional: isolate conversation
+)
+print(resp.choices[0].message.content)
+```
diff --git a/docs/python-sdk.md b/docs/python-sdk.md
new file mode 100644
index 00000000..d035238d
--- /dev/null
+++ b/docs/python-sdk.md
@@ -0,0 +1,31 @@
+# Python SDK
+
+Use nanobot as a library โ no CLI, no gateway, just Python:
+
+```python
+from nanobot import Nanobot
+
+bot = Nanobot.from_config()
+result = await bot.run("Summarize the README")
+print(result.content)
+```
+
+Each call carries a `session_key` for conversation isolation โ different keys get independent history:
+
+```python
+await bot.run("hi", session_key="user-alice")
+await bot.run("hi", session_key="task-42")
+```
+
+Add lifecycle hooks to observe or customize the agent:
+
+```python
+from nanobot.agent import AgentHook, AgentHookContext
+
+class AuditHook(AgentHook):
+ async def before_execute_tools(self, ctx: AgentHookContext) -> None:
+ for tc in ctx.tool_calls:
+ print(f"[tool] {tc.name}")
+
+result = await bot.run("Hello", hooks=[AuditHook()])
+```
diff --git a/docs/quick-start.md b/docs/quick-start.md
new file mode 100644
index 00000000..7112ba8c
--- /dev/null
+++ b/docs/quick-start.md
@@ -0,0 +1,104 @@
+# Install and Quick Start
+
+## Install
+
+> [!IMPORTANT]
+> This README may describe features that are available first in the latest source code.
+> If you want the newest features and experiments, install from source.
+> If you want the most stable day-to-day experience, install from PyPI or with `uv`.
+
+**Install from source** (latest features, experimental changes may land here first; recommended for development)
+
+```bash
+git clone https://github.com/HKUDS/nanobot.git
+cd nanobot
+pip install -e .
+```
+
+**Install with [uv](https://github.com/astral-sh/uv)** (stable release, fast)
+
+```bash
+uv tool install nanobot-ai
+```
+
+**Install from PyPI** (stable release)
+
+```bash
+pip install nanobot-ai
+```
+
+### Update to latest version
+
+**PyPI / pip**
+
+```bash
+pip install -U nanobot-ai
+nanobot --version
+```
+
+**uv**
+
+```bash
+uv tool upgrade nanobot-ai
+nanobot --version
+```
+
+**Using WhatsApp?** Rebuild the local bridge after upgrading:
+
+```bash
+rm -rf ~/.nanobot/bridge
+nanobot channels login whatsapp
+```
+
+## Quick Start
+
+> [!TIP]
+> Set your API key in `~/.nanobot/config.json`.
+> Get API keys: [OpenRouter](https://openrouter.ai/keys) (Global)
+>
+> For other LLM providers, please see [`configuration.md`](./configuration.md).
+>
+> For web search capability setup, please see the web-search section in [`configuration.md`](./configuration.md#web-search).
+
+**1. Initialize**
+
+```bash
+nanobot onboard
+```
+
+Use `nanobot onboard --wizard` if you want the interactive setup wizard.
+
+**2. Configure** (`~/.nanobot/config.json`)
+
+Configure these **two parts** in your config (other options have defaults).
+
+*Set your API key* (e.g. OpenRouter, recommended for global users):
+```json
+{
+ "providers": {
+ "openrouter": {
+ "apiKey": "sk-or-v1-xxx"
+ }
+ }
+}
+```
+
+*Set your model* (optionally pin a provider โ defaults to auto-detection):
+```json
+{
+ "agents": {
+ "defaults": {
+ "model": "anthropic/claude-opus-4-5",
+ "provider": "openrouter"
+ }
+ }
+}
+```
+
+**3. Chat**
+
+```bash
+nanobot agent
+```
+
+That's it! You have a working AI agent in 2 minutes.
diff --git a/docs/WEBSOCKET.md b/docs/websocket.md
similarity index 99%
rename from docs/WEBSOCKET.md
rename to docs/websocket.md
index 1e0ddbbb..ed1652be 100644
--- a/docs/WEBSOCKET.md
+++ b/docs/websocket.md
@@ -42,7 +42,7 @@ nanobot gateway
You should see:
-```
+```text
WebSocket server listening on ws://127.0.0.1:8765/
```
@@ -68,7 +68,7 @@ asyncio.run(main())
## Connection URL
-```
+```text
ws://{host}:{port}{path}?client_id={id}&token={token}
```
diff --git a/images/GitHub_README.png b/images/GitHub_README.png
new file mode 100644
index 00000000..a76f36df
Binary files /dev/null and b/images/GitHub_README.png differ
diff --git a/images/nanobot_arch.png b/images/nanobot_arch.png
new file mode 100644
index 00000000..db4a42cd
Binary files /dev/null and b/images/nanobot_arch.png differ
diff --git a/nanobot_logo.png b/images/nanobot_logo.png
similarity index 100%
rename from nanobot_logo.png
rename to images/nanobot_logo.png
diff --git a/nanobot_arch.png b/nanobot_arch.png
deleted file mode 100644
index 09251771..00000000
Binary files a/nanobot_arch.png and /dev/null differ