# Technical Documentation: memovai/mimiclaw

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/memovai/mimiclaw)

### Overview (`README.md`)

# MimiClaw: Pocket AI Assistant on a $5 Chip

<p align="center">
  <img src="assets/banner.png" alt="MimiClaw" width="500" />
</p>

<p align="center">
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
  <a href="https://deepwiki.com/memovai/mimiclaw"><img src="https://img.shields.io/badge/DeepWiki-mimiclaw-blue.svg" alt="DeepWiki"></a>
  <a href="https://discord.gg/r8ZxSvB8Yr"><img src="https://img.shields.io/badge/Discord-mimiclaw-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
  <a href="https://x.com/ssslvky"><img src="https://img.shields.io/badge/X-@ssslvky-black?logo=x" alt="X"></a>
</p>

<p align="center">
  <strong><a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <a href="README_JA.md">日本語</a></strong>
</p>

**The world's first AI assistant(OpenClaw) on a $5 chip. No Linux. No Node.js. Just pure C**

MimiClaw turns a tiny ESP32-S3 board into a personal AI assistant. Plug it into USB power, connect to WiFi, and talk to it through Telegram — it handles any task you throw at it and evolves over time with local memory — all on a chip the size of a thumb.

## Meet MimiClaw

- **Tiny** — No Linux, no Node.js, no bloat — just pure C
- **Handy** — Message it from Telegram, it handles the rest
- **Loyal** — Learns from memory, remembers across reboots
- **Energetic** — USB power, 0.5 W, runs 24/7
- **Lovable** — One ESP32-S3 board, $5, nothing else

## How It Works



You send a message on Telegram. The ESP32-S3 picks it up over WiFi, feeds it into an agent loop — the LLM thinks, calls tools, reads memory — and sends the reply back. Supports both **Anthropic (Claude)** and **OpenAI (GPT)** as providers, switchable at runtime. Everything runs on a single $5 chip with all your data stored locally on flash.

## Quick Start

### What You Need

- An **ESP32-S3 dev board** with 16 MB flash and 8 MB PSRAM (e.g. Xiaozhi AI board, ~$10)
- A **USB Type-C cable**
- A **Telegram bot token** — talk to [@BotFather](https://t.me/BotFather) on Telegram to create one
- An **Anthropic API key** — from [console.anthropic.com](https://console.anthropic.com), or an **OpenAI API key** — from [platform.openai.com](https://platform.openai.com)

### Install

```bash
# You need ESP-IDF v5.5+ installed first:
# https://docs.espressif.com/projects/esp-idf/en/v5.5.2/esp32s3/get-started/

git clone https://github.com/memovai/mimiclaw.git
cd mimiclaw

idf.py set-target esp32s3
```

<details>
<summary>Ubuntu Install</summary>

Recommended baseline:

- Ubuntu 22.04/24.04
- Python >= 3.10
- CMake >= 3.16
- Ninja >= 1.10
- Git >= 2.34
- flex >= 2.6
- bison >= 3.8
- gperf >= 3.1
- dfu-util >= 0.11
- `libusb-1.0-0`, `libffi-dev`, `libssl-dev`

Install and build on Ubuntu:

```bash
sudo apt-get update
sudo apt-get install -y git wget flex bison gperf python3 python3-pip python3-venv \
  cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0

./scripts/setup_idf_ubuntu.sh
./scripts/build_ubuntu.sh
```

</details>

<details>
<summary>macOS Install</summary>

Recommended baseline:

- macOS 12/13/14
- Xcode Command Line Tools
- Homebrew
- Python >= 3.10
- CMake >= 3.16
- Ninja >= 1.10
- Git >= 2.34
- flex >= 2.6
- bison >= 3.8
- gperf >= 3.1
- dfu-util >= 0.11
- `libusb`, `libffi`, `openssl`

Install and build on macOS:

```bash
xcode-select --install
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

./scripts/setup_idf_macos.sh
./scripts/build_macos.sh
```

</details>

### Configure

MimiClaw uses a **two-layer config** system: build-time defaults in `mimi_secrets.h`, with runtime overrides via the serial CLI. CLI values are stored in NVS flash and take priority over build-time values.

```bash
cp main/mimi_secrets.h.example main/mimi_secrets.h
```

Edit `main/mimi_secrets.h`:

```c
#define MIMI_SECRET_WIFI_SSID       "YourWiFiName"
#define MIMI_SECRET_WIFI_PASS       "YourWiFiPassword"
#define MIMI_SECRET_TG_TOKEN        "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
#define MIMI_SECRET_API_KEY         "sk-ant-api03-xxxxx"
#define MIMI_SECRET_MODEL_PROVIDER  "anthropic"     // "anthropic" or "openai"
#define MIMI_SECRET_SEARCH_KEY      ""              // optional: Brave Search API key
#define MIMI_SECRET_TAVILY_KEY      ""              // optional: Tavily API key (preferred)
#define MIMI_SECRET_PROXY_HOST      ""              // optional: e.g. "10.0.0.1"
#define MIMI_SECRET_PROXY_PORT      ""              // optional: e.g. "7897"
```

Then build and flash:

```bash
# Clean build (required after any mimi_secrets.h change)
idf.py fullclean && idf.py build

# Find your serial port
ls /dev/cu.usb*          # macOS
ls /dev/ttyACM*          # Linux

# Flash and monitor (replace PORT with your port)
# USB adapter: likely /dev/cu.usbmodem11401 (macOS) or /dev/ttyACM0 (Linux)
idf.py -p PORT flash monitor
```

> **Important: Plug into the correct USB port!** Most ESP32-S3 boards have two USB-C ports. You must use the one labeled **USB** (native USB Serial/JTAG), **not** the one labeled **COM** (external UART bridge). Plugging into the wrong port will cause flash/monitor failures.
>
> <details>
> <summary>Show reference photo</summary>
>
> <img src="assets/esp32s3-usb-port.jpg" alt="Plug into the USB port, not COM" width="480" />
>
> </details>

### CLI Commands (via UART/COM port)

Connect via serial to configure or debug. **Config commands** let you change settings without recompiling — just plug in a USB cable anywhere.

**Runtime config** (saved to NVS, overrides build-time defaults):

```
mimi> wifi_set MySSID MyPassword   # change WiFi network
mimi> set_tg_token 123456:ABC...   # change Telegram bot token
mimi> set_api_key sk-ant-api03-... # change API key (Anthropic or OpenAI)
mimi> set_model_provider openai    # switch provider (anthropic|openai)
mimi> set_model gpt-4o             # change LLM model
mimi> set_proxy 127.0.0.1 7897  # set HTTP proxy
mimi> clear_proxy                  # remove proxy
mimi> set_search_key BSA...        # set Brave Search API key
mimi> set_tavily_key tvly-...      # set Tavily API key (preferred)
mimi> config_show                  # show all config (masked)
mimi> config_reset                 # clear NVS, revert to build-time defaults
```

**Debug & maintenance:**

```
mimi> wifi_status              # am I connected?
mimi> memory_read              # see what the bot remembers
mimi> memory_write "content"   # write to MEMORY.md
mimi> heap_info                # how much RAM is free?
mimi> session_list             # list all chat sessions
mimi> session_clear 12345      # wipe a conversation
mimi> heartbeat_trigger           # manually trigger a heartbeat check
mimi> cron_start                  # start cron scheduler now
mimi> restart                     # reboot
```

### USB (JTAG) vs UART: Which Port for What

Most ESP32-S3 dev boards expose **two USB-C ports**:

| Port | Use for |
|------|---------|
| **USB** (JTAG) | `idf.py flash`, JTAG debugging |
| **COM** (UART) | **REPL CLI**, serial console |

> **REPL requires the UART (COM) port.** The USB (JTAG) port does not support interactive REPL input.

<details>
<summary>Port details & recommended workflow</summary>

| Port | Label | Protocol |
|------|-------|----------|
| **USB** | USB / JTAG | Native USB Serial/JTAG |
| **COM** | UART / COM | External UART bridge (CP2102/CH340) |

The ESP-IDF console/REPL is configured to use UART by default (`CONFIG_ESP_CONSOLE_UART_DEFAULT=y`).

**If you have both ports connected simultaneously:**

- USB (JTAG) handles flash/download and provides secondary serial output
- UART (COM) provides the primary interactive console for the REPL
- macOS: both appear as `/dev/cu.usbmodem*` or `/dev/cu.usbserial-*` — run `ls /dev/cu.usb*` to identify
- Linux: USB (JTAG) → `/dev/ttyACM0`, UART → `/dev/ttyUSB0`

**Recommended workflow:**

```bash
# Flash via USB (JTAG) port
idf.py -p /dev/cu.usbmodem11401 flash

# Open REPL via UART (COM) port
idf.py -p /dev/cu.usbserial-110 monitor
# or use any serial terminal: screen, minicom, PuTTY at 115200 baud
```

</details>

## Memory

MimiClaw stores everything as plain text files you can read and edit:

| File | What it is |
|------|------------|
| `SOUL.md` | The bot's personality — edit this to change how it behaves |
| `USER.md` | Info about you — name, preferences, language |
| `MEMORY.md` | Long-term memory — things the bot should always remember |
| `HEARTBEAT.md` | Task list the bot checks periodically and acts on autonomously |
| `cron.json` | Scheduled jobs — recurring or one-shot tasks created by the AI |
| `2026-02-05.md` | Daily notes — what happened today |
| `tg_12345.jsonl` | Chat history — your conversation with the bot |

## Tools

MimiClaw supports tool calling for both Anthropic and OpenAI — the LLM can call tools during a conversation and loop until the task is done (ReAct pattern).

| Tool | Description |
|------|-------------|
| `web_search` | Search the web via Tavily (preferred) or Brave for current information |
| `get_current_time` | Fetch current date/time via HTTP and set the system clock |
| `cron_add` | Schedule a recurring or one-shot task (the LLM creates cron jobs on its own) |
| `cron_list` | List all scheduled cron jobs |
| `cron_remove` | Remove a cron job by ID |

To enable web search, set a [Tavily API key](https://app.tavily.com/home) via `MIMI_SECRET_TAVILY_KEY` (preferred), or a [Brave Search API key](https://brave.com/search/api/) via `MIMI_SECRET_SEARCH_KEY` in `mimi_secrets.h`.

## Cron Tasks

MimiClaw has a built-in cron scheduler that lets the AI schedule its own tasks. The LLM can create recurring jobs ("every N seconds") or one-shot jobs ("at unix timestamp") via the `cron_add` tool. When a job fires, its message is injected into the agent loop — so the AI wakes up, processes the task, and responds.

Jobs are persisted to SPIFFS (`cron.json`) and survive reboots. Example use cases: daily summaries, periodic reminders, scheduled check-ins.

## Heartbeat

The heartbeat service periodically reads `HEARTBEAT.md` from SPIFFS and checks for actionable tasks. If uncompleted items are found (anything that isn't an empty line, a header, or a checked `- [x]` box), it sends a prompt to the agent loop so the AI can act on them autonomously.

This turns MimiClaw into a proactive assistant — write tasks to `HEARTBEAT.md` and the bot will pick them up on the next heartbeat cycle (default: every 30 minutes).

## Also Included

- **WebSocket gateway** on port 18789 — connect from your LAN with any WebSocket client
- **OTA updates** — flash new firmware over WiFi, no USB needed
- **Dual-core** — network I/O and AI processing run on separate CPU cores
- **HTTP proxy** — CONNECT tunnel support for restricted networks
- **Multi-provider** — supports both Anthropic (Claude) and OpenAI (GPT), switchable at runtime
- **Cron scheduler** — the AI can schedule its own recurring and one-shot tasks, persisted across reboots
- **Heartbeat** — periodically checks a task file and prompts the AI to act autonomously
- **Tool use** — ReAct agent loop with tool calling for both providers

## For Developers

Technical details live in the `docs/` folder:

- **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** — system design, module map, task layout, memory budget, protocols, flash partitions
- **[docs/TODO.md](docs/TODO.md)** — feature gap tracker and roadmap
- **[docs/WIFI_ONBOARDING_AP.md](docs/WIFI_ONBOARDING_AP.md)** — how the local `MimiClaw-XXXX` onboarding/admin AP flow works
- **[docs/tool-setup/](docs/tool-setup/README.md)** — configuration guides for external service integrations (Tavily, etc.)

## Contributing

Please read **[CONTRIBUTING.md](CONTRIBUTING.md)** before opening issues or pull requests.

## Contributors

Thanks to everyone who has contributed to MimiClaw.

<a href="https://github.com/memovai/mimiclaw/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=memovai/mimiclaw" alt="MimiClaw contributors" />
</a>

## License

MIT

## Acknowledgments

Inspired by [OpenClaw](https://github.com/openclaw/openclaw) and [Nanobot](https://github.com/HKUDS/nanobot). MimiClaw reimplements the core AI agent architecture for embedded hardware — no Linux, no server, just a $5 chip.

## Star History

<a href="https://www.star-history.com/?repos=memovai%2Fmimiclaw&type=date&legend=top-left">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=memovai/mimiclaw&type=date&theme=dark&legend=top-left" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=memovai/mimiclaw&type=date&legend=top-left" />
   <img alt="Star History Chart" src="https://api.star-history.com/image?repos=memovai/mimiclaw&type=date&legend=top-left" />
 </picture>
</a>

---

### Overview (`docs/im-integration/README.md`)

# IM Integration Guides

Configuration guides for MimiClaw's instant messaging channel integrations.

## Guides

| Guide | Service | Description |
|-------|---------|-------------|
| [Feishu Setup](FEISHU_SETUP.md) | [Feishu / Lark](https://open.feishu.cn/) | Feishu bot channel — receive and send messages via Feishu |

## Overview

MimiClaw supports multiple IM channels for interacting with the AI agent. Each guide below walks through obtaining API credentials, configuring MimiClaw (build-time or runtime), and verifying the integration.

All credentials can be set in two ways:

1. **Build-time** — define in `main/mimi_secrets.h` and rebuild
2. **Runtime** — use serial CLI commands (saved to NVS flash, no rebuild needed)

See [mimi_secrets.h.example](../../main/mimi_secrets.h.example) for the full list of configurable secrets.

---

### Overview (`docs/tool-setup/README.md`)

# Tool Setup Guides

Configuration guides for MimiClaw's external service integrations.

Related local setup guide:

- [Wi-Fi AP Onboarding Guide](../WIFI_ONBOARDING_AP.md) — configure firmware builds that expose the local `MimiClaw-XXXX` onboarding/admin access point

## Guides

| Guide | Service | Description |
|-------|---------|-------------|
| [Tavily Setup](TAVILY_SETUP.md) | [Tavily](https://tavily.com) | Web search API — preferred search provider for the `web_search` tool |

## Overview

MimiClaw integrates with external services to extend its capabilities. Each guide below walks through obtaining API credentials, configuring MimiClaw (build-time or runtime), and verifying the integration.

All credentials can be set in two ways:

1. **Build-time** — define in `main/mimi_secrets.h` and rebuild
2. **Runtime** — use serial CLI commands (saved to NVS flash, no rebuild needed)

See [mimi_secrets.h.example](../../main/mimi_secrets.h.example) for the full list of configurable secrets.

---

### Overview (`main/channels/feishu/README.md`)

# Feishu/Lark Bot Integration

This directory contains the Feishu bot integration for MimiClaw.

## Features

- Send text messages to Feishu chats
- Receive messages via WebSocket persistent connection (long-connection mode)
- Automatic message chunking (4096 chars per message)
- Tenant access token management with auto-refresh
- Message deduplication
- Reply to specific messages
- Support for both DM (p2p) and group chats

## Configuration

### Option 1: Build-time Configuration

1. Copy the secrets template:
```bash
cp main/mimi_secrets.h.example main/mimi_secrets.h
```

2. Edit `main/mimi_secrets.h`:
```c
#define MIMI_SECRET_FEISHU_APP_ID     "cli_xxxxxxxxxxxxxx"
#define MIMI_SECRET_FEISHU_APP_SECRET "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

3. Rebuild:
```bash
idf.py fullclean && idf.py build
```

### Option 2: Runtime Configuration (CLI)

```
mimi> set_feishu_creds cli_xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

## Feishu App Setup

1. Go to [Feishu Open Platform](https://open.feishu.cn/)
2. Create an app and get **App ID** / **App Secret**
3. Enable permissions:
   - `im:message` - Send and receive messages
   - `im:message:send_as_bot` - Send messages as bot
4. Configure Event Subscription:
   - Set subscription mode to **Persistent connection** (长连接)
   - Subscribe to: `im.message.receive_v1`
5. The ESP32 will connect to Feishu automatically on boot

## Architecture

```
Feishu Server (wss://open.feishu.cn)
    ^
    |  (WebSocket persistent connection, ESP32 initiates)
[feishu_ws_task]
    |
    v  (message_bus_push_inbound)
[Message Bus] --> [Agent Loop] --> [Message Bus]
    |                                    |
    v  (outbound dispatch)               |
[feishu_send_message] <-----------------+
    |
    v  (POST /im/v1/messages)
Feishu API
```

## API Reference

| Function | Description |
|----------|-------------|
| `feishu_bot_init()` | Load credentials from NVS/build-time |
| `feishu_bot_start()` | Start WebSocket persistent connection task |
| `feishu_send_message(chat_id, text)` | Send text message |
| `feishu_reply_message(message_id, text)` | Reply to a specific message |
| `feishu_set_credentials(app_id, secret)` | Save credentials to NVS |

## References

- [Feishu Open Platform Docs](https://open.feishu.cn/document/home/index)
- [Message API](https://open.feishu.cn/document/server-docs/im-v1/message/create)
- [Long-connection Event Subscription](https://open.feishu.cn/document/server-docs/event-subscription/long-connection)

---

### ARCHITECTURE (`docs/ARCHITECTURE.md`)

# MimiClaw Architecture

> ESP32-S3 AI Agent firmware — C/FreeRTOS implementation running on bare metal (no Linux).

---

## System Overview

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Data Flow

```
1. User sends message on Telegram (or WebSocket)
2. Channel poller receives message, wraps in mimi_msg_t
3. Message pushed to Inbound Queue (FreeRTOS xQueue)
4. Agent Loop (Core 1) pops message:
   a. Load session history from SPIFFS (JSONL)
   b. Build system prompt (SOUL.md + USER.md + MEMORY.md + recent notes + tool guidance)
   c. Build cJSON messages array (history + current message)
   d. ReAct loop (max 10 iterations):
      i.   Call Claude API via HTTPS (non-streaming, with tools array)
      ii.  Parse JSON response → text blocks + tool_use blocks
      iii. If stop_reason == "tool_use":
           - Execute each tool (e.g. web_search → Brave Search API)
           - Append assistant content + tool_result to messages
           - Continue loop
      iv.  If stop_reason == "end_turn": break with final text
   e. Save user message + final assistant text to session file
   f. Push response to Outbound Queue
5. Outbound Dispatch (Core 0) pops response:
   a. Route by channel field ("telegram" → sendMessage, "websocket" → WS frame)
6. User receives reply
```

---

## Module Map

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## FreeRTOS Task Layout

| Task               | Core | Priority | Stack  | Description                          |
|--------------------|------|----------|--------|--------------------------------------|
| `tg_poll`          | 0    | 5        | 12 KB  | Telegram long polling (30s timeout)  |
| `agent_loop`       | 1    | 6        | 12 KB  | Message processing + Claude API call |
| `outbound`         | 0    | 5        | 8 KB   | Route responses to Telegram / WS     |
| `serial_cli`       | 0    | 3        | 4 KB   | USB serial console REPL              |
| httpd (internal)   | 0    | 5        | —      | WebSocket server (esp_http_server)   |
| wifi_event (IDF)   | 0    | 8        | —      | WiFi event handling (ESP-IDF)        |

**Core allocation strategy**: Core 0 handles I/O (network, serial, WiFi). Core 1 is dedicated to the agent loop (CPU-bound JSON building + waiting on HTTPS).

---

## Memory Budget

| Purpose                            | Location       | Size     |
|------------------------------------|----------------|----------|
| FreeRTOS task stacks               | Internal SRAM  | ~40 KB   |
| WiFi buffers                       | Internal SRAM  | ~30 KB   |
| TLS connections x2 (Telegram + Claude) | PSRAM      | ~120 KB  |
| JSON parse buffers                 | PSRAM          | ~32 KB   |
| Session history cache              | PSRAM          | ~32 KB   |
| System prompt buffer               | PSRAM          | ~16 KB   |
| LLM response stream buffer         | PSRAM          | ~32 KB   |
| Remaining available                | PSRAM          | ~7.7 MB  |

Large buffers (32 KB+) are allocated from PSRAM via `heap_caps_calloc(1, size, MALLOC_CAP_SPIRAM)`.

---

## Flash Partition Layout

```
Offset      Size      Name        Purpose
─────────────────────────────────────────────
0x009000    24 KB     nvs         ESP-IDF internal use (WiFi calibration etc.)
0x00F000     8 KB     otadata     OTA boot state
0x011000     4 KB     phy_init    WiFi PHY calibration
0x020000     2 MB     ota_0       Firmware slot A
0x220000     2 MB     ota_1       Firmware slot B
0x420000    12 MB     spiffs      Markdown memory, sessions, config
0xFF0000    64 KB     coredump    Crash dump storage
```

Total: 16 MB flash.

---

## Storage Layout (SPIFFS)

SPIFFS is a flat filesystem — no real directories. Files use path-like names.

```
/spiffs/config/SOUL.md          AI personality definition
/spiffs/config/USER.md          User profile
/spiffs/memory/MEMORY.md        Long-term persistent memory
/spiffs/memory/2026-02-05.md    Daily notes (one file per day)
/spiffs/sessions/tg_12345.jsonl Session history (one file per Telegram chat)
```

Session files are JSONL (one JSON object per line):
```json
{"role":"user","content":"Hello","ts":1738764800}
{"role":"assistant","content":"Hi there!","ts":1738764802}
```

---

## Configuration

All configuration is done exclusively through `mimi_secrets.h` at build time. There is no runtime configuration — changing any setting requires `idf.py fullclean && idf.py build`.

| Define                       | Description                             |
|------------------------------|-----------------------------------------|
| `MIMI_SECRET_WIFI_SSID`     | WiFi SSID                               |
| `MIMI_SECRET_WIFI_PASS`     | WiFi password                           |
| `MIMI_SECRET_TG_TOKEN`      | Telegram Bot API token                  |
| `MIMI_SECRET_API_KEY`       | Anthropic API key                       |
| `MIMI_SECRET_MODEL`         | Model ID (default: claude-opus-4-6)     |
| `MIMI_SECRET_PROXY_HOST`    | HTTP proxy hostname/IP (optional)       |
| `MIMI_SECRET_PROXY_PORT`    | HTTP proxy port (optional)              |
| `MIMI_SECRET_SEARCH_KEY`    | Brave Search API key (optional)         |

NVS is still initialized (required by ESP-IDF WiFi internals) but is not used for application configuration.

---

## Message Bus Protocol

The internal message bus uses two FreeRTOS queues carrying `mimi_msg_t`:

```c
typedef struct {
    char channel[16];   // "telegram", "websocket", "cli"
    char chat_id[32];   // Telegram chat ID or WS client ID
    char *content;      // Heap-allocated text (ownership transferred)
} mimi_msg_t;
```

- **Inbound queue**: channels → agent loop (depth: 8)
- **Outbound queue**: agent loop → dispatch → channels (depth: 8)
- Content string ownership is transferred on push; receiver must `free()`.

---

## WebSocket Protocol

Port: **18789**. Max clients: **4**.

**Client → Server:**
```json
{"type": "message", "content": "Hello", "chat_id": "ws_client1"}
```

**Server → Client:**
```json
{"type": "response", "content": "Hi there!", "chat_id": "ws_client1"}
```

Client `chat_id` is auto-assigned on connection (`ws_<fd>`) but can be overridden in the first message.

---

## Claude API Integration

Endpoint: `POST https://api.anthropic.com/v1/messages`

Request format (Anthropic-native, non-streaming, with tools):
```json
{
  "model": "claude-opus-4-6",
  "max_tokens": 4096,
  "system": "<system prompt>",
  "tools": [
    {
      "name": "web_search",
      "description": "Search the web for current information.",
      "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
    }
  ],
  "messages": [
    {"role": "user", "content": "Hello"},
    {"role": "assistant", "content": "Hi!"},
    {"role": "user", "content": "What's the weather today?"}
  ]
}
```

Key difference from OpenAI: `system` is a top-level field, not inside the `messages` array.

Non-streaming JSON response:
```json
{
  "id": "msg_xxx",
  "type": "message",
  "role": "assistant",
  "content": [
    {"type": "text", "text": "Let me search for that."},
    {"type": "tool_use", "id": "toolu_xxx", "name": "web_search", "input": {"query": "weather today"}}
  ],
  "stop_reason": "tool_use"
}
```

When `stop_reason` is `"tool_use"`, the agent loop executes each tool and sends results back:
```json
{"role": "assistant", "content": [<text + tool_use blocks>]}
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_xxx", "content": "..."}]}
```

The loop repeats until `stop_reason` is `"end_turn"` (max 10 iterations).

---

## Startup Sequence

```
app_main()
  ├── init_nvs()                    NVS flash init (erase if corrupted)
  ├── esp_event_loop_create_default()
  ├── init_spiffs()                 Mount SPIFFS at /spiffs
  ├── message_bus_init()            Create inbound + outbound queues
  ├── memory_store_init()           Verify SPIFFS paths
  ├── session_mgr_init()
  ├── wifi_manager_init()           Init WiFi STA mode + event handlers
  ├── http_proxy_init()             Load proxy config from build-time secrets
  ├── telegram_bot_init()           Load bot token from build-time secrets
  ├── llm_proxy_init()              Load API key + model from build-time secrets
  ├── tool_registry_init()          Register tools, build tools JSON
  ├── agent_loop_init()
  ├── serial_cli_init()             Start REPL (works without WiFi)
  │
  ├── wifi_manager_start()          Connect using build-time credentials
  │   └── wifi_manager_wait_connected(30s)
  │
  └── [if WiFi connected]
      ├── telegram_bot_start()      Launch tg_poll task (Core 0)
      ├── agent_loop_start()        Launch agent_loop task (Core 1)
      ├── ws_server_start()         Start httpd on port 18789
      └── outbound_dispatch task    Launch outbound task (Core 0)
```

If WiFi credentials are missing or connection times out, the CLI remains available for diagnostics.

---

## Serial CLI Commands

The CLI provides debug and maintenance commands only. All configuration is done via `mimi_secrets.h`.

| Command                        | Description                          |
|--------------------------------|--------------------------------------|
| `wifi_status`                  | Show connection status and IP        |
| `memory_read`                  | Print MEMORY.md contents             |
| `memory_write <CONTENT>`       | Overwrite MEMORY.md                  |
| `session_list`                 | List all session files               |
| `session_clear <CHAT_ID>`      | Delete a session file                |
| `heap_info`                    | Show internal + PSRAM free bytes     |
| `restart`                      | Reboot the device                    |
| `help`                         | List all available commands           |

---

## Nanobot Reference Mapping

| Nanobot Module              | MimiClaw Equivalent            | Notes                        |
|-----------------------------|--------------------------------|------------------------------|
| `agent/loop.py`             | `agent/agent_loop.c`           | ReAct loop with tool use     |
| `agent/context.py`          | `agent/context_builder.c`      | Loads SOUL.md + USER.md + memory + tool guidance |
| `agent/memory.py`           | `memory/memory_store.c`        | MEMORY.md + daily notes      |
| `session/manager.py`        | `memory/session_mgr.c`         | JSONL per chat, ring buffer  |
| `channels/telegram.py`      | `telegram/telegram_bot.c`      | Raw HTTP, no python-telegram-bot |
| `bus/events.py` + `queue.py`| `bus/message_bus.c`            | FreeRTOS queues vs asyncio   |
| `providers/litellm_provider.py` | `llm/llm_proxy.c`         | Direct Anthropic API only    |
| `config/schema.py`          | `mimi_config.h` + `mimi_secrets.h` | Build-time secrets only  |
| `cli/commands.py`           | `cli/serial_cli.c`             | esp_console REPL             |
| `agent/tools/*`             | `tools/tool_registry.c` + `tool_web_search.c` | web_search via Brave API |
| `agent/subagent.py`         | *(not yet implemented)*        | See TODO.md                  |
| `agent/skills.py`           | *(not yet implemented)*        | See TODO.md                  |
| `cron/service.py`           | *(not yet implemented)*        | See TODO.md                  |
| `heartbeat/service.py`      | *(not yet implemented)*        | See TODO.md                  |

---

### TODO (`docs/TODO.md`)

# MimiClaw vs Nanobot — Feature Gap Tracker

> Comparing against `nanobot/` reference implementation. Tracks features MimiClaw has not yet aligned with.
> Priority: P0 = Core missing, P1 = Important enhancement, P2 = Nice to have

---

## P0 — Core Agent Capabilities

### [x] ~~Tool Use Loop (multi-turn agent iteration)~~
- Implemented: `agent_loop.c` ReAct loop with `llm_chat_tools()`, max 10 iterations, non-streaming JSON parsing

### [ ] Memory Write via Tool Use (agent-driven memory persistence)
- **openclaw**: Agent uses standard `write`/`edit` tools to write `MEMORY.md` and `memory/YYYY-MM-DD.md`; system prompt instructs agent to persist important information; pre-compaction memory flush triggers a silent agent turn to save durable memories before context window limit
- **MimiClaw**: `memory_write_long_term` and `memory_append_today` exist but are only called from CLI; agent loop never writes memory
- **Scope**: Expose `memory_write` and `memory_append_today` as tool_use tools for Claude; add system prompt guidance on when to persist memory; optionally add pre-compaction flush (trigger memory save when session history nears `MIMI_SESSION_MAX_MSGS`)
- **Depends on**: Tool Use Loop

### [x] ~~Tool Registry + web_search Tool~~
- Implemented: `tools/tool_registry.c` — tool registration, JSON schema builder, dispatch by name
- Implemented: `tools/tool_web_search.c` — Brave Search API via HTTPS (direct + proxy support)

### [ ] More Built-in Tools
- **nanobot built-in tools** not yet ported: `read_file`, `write_file`, `edit_file`, `list_dir`, `message`
- **Recommendation**: Reasonable tool subset for ESP32: `read_file`, `write_file`, `list_dir` (SPIFFS), `message`, `memory_write`

### [ ] Subagent / Spawn Background Tasks
- **nanobot**: `subagent.py` — SubagentManager spawns independent agent instances with isolated tool sets and system prompts, announces results back to main agent via system channel
- **MimiClaw**: Not implemented
- **Recommendation**: ESP32 memory is limited; simplify to a single background FreeRTOS task for long-running work, inject result into inbound queue on completion

---

## P1 — Important Features

### [ ] Telegram User Allowlist (allow_from)
- **nanobot**: `channels/base.py` L59-82 — `is_allowed()` checks sender_id against allow_list
- **MimiClaw**: No authentication; anyone can message the bot and consume API credits
- **Recommendation**: Store allow_from list in `mimi_secrets.h` as a build-time define, filter in `process_updates()`

### [ ] Telegram Markdown to HTML Conversion
- **nanobot**: `channels/telegram.py` L16-76 — `_markdown_to_telegram_html()` full converter: code blocks, inline code, bold, italic, links, strikethrough, lists
- **MimiClaw**: Uses `parse_mode: Markdown` directly; special characters can cause send failures (has fallback to plain text)
- **Recommendation**: Implement simplified Markdown-to-HTML converter, or switch to `parse_mode: HTML`

### [ ] Telegram /start Command
- **nanobot**: `telegram.py` L183-192 — handles `/start` command, replies with welcome message
- **MimiClaw**: Not handled; /start is sent to Claude as a regular message

### [ ] Telegram Media Handling (photos/voice/files)
- **nanobot**: `telegram.py` L194-289 — handles photo, voice, audio, document; downloads files; transcribes voice
- **MimiClaw**: Only processes `message.text`, ignores all media messages
- **Recommendation**: Images can be base64-encoded for Claude Vision; voice requires Whisper API (extra HTTPS request)

### [ ] Skills System (pluggable capabilities)
- **nanobot**: `agent/skills.py` — loads skills from SKILL.md files, supports always-loaded and on-demand, frontmatter metadata, requirements checking
- **MimiClaw**: Not implemented
- **Recommendation**: Simplified version: store SKILL.md files on SPIFFS, load into system prompt via context_builder

### [ ] Full Bootstrap File Alignment
- **nanobot**: Loads `AGENTS.md`, `SOUL.md`, `USER.md`, `TOOLS.md`, `IDENTITY.md` (5 files)
- **MimiClaw**: Only loads `SOUL.md` and `USER.md`
- **Recommendation**: Add AGENTS.md (behavior guidelines) and TOOLS.md (tool documentation)

### [ ] Longer Memory Lookback
- **nanobot**: `memory.py` L56-80 — `get_recent_memories(days=7)` defaults to 7 days
- **MimiClaw**: `context_builder.c` only reads last 3 days
- **Recommendation**: Make configurable, but mind token budget

### [x] ~~System Prompt Tool Guidance~~
- Implemented: `context_builder.c` includes tool usage guidance in system prompt

### [ ] Message Metadata (media, reply_to, metadata)
- **nanobot**: `bus/events.py` — InboundMessage has media, metadata fields; OutboundMessage has reply_to
- **MimiClaw**: `mimi_msg_t` only has channel + chat_id + content
- **Recommendation**: Extend msg struct, add media_path and metadata fields

### [ ] Outbound Subscription Pattern
- **nanobot**: `bus/queue.py` L41-49 — supports `subscribe_outbound(channel, callback)` subscription model
- **MimiClaw**: Hardcoded if-else dispatch
- **Recommendation**: Current approach is simple and reliable; not worth changing with few channels

---

## P2 — Advanced Features

### [ ] Cron Scheduled Task Service
- **nanobot**: `cron/service.py` — full cron scheduler supporting at/every/cron expressions, persistent storage, timed agent triggers
- **MimiClaw**: Not implemented
- **Recommendation**: Use FreeRTOS timer for simplified version, support "every N minutes" only

### [ ] Heartbeat Service
- **nanobot**: `heartbeat/service.py` — reads HEARTBEAT.md every 30 minutes, triggers agent if tasks are found
- **MimiClaw**: Not implemented
- **Recommendation**: Simple FreeRTOS timer that periodically checks HEARTBEAT.md

### [ ] Multi-LLM Provider Support
- **nanobot**: `providers/litellm_provider.py` — supports OpenRouter, Anthropic, OpenAI, Gemini, DeepSeek, Groq, Zhipu, vLLM via LiteLLM
- **MimiClaw**: Hardcoded to Anthropic Messages API
- **Recommendation**: Abstract LLM interface, support OpenAI-compatible API (most providers are compatible)

### [ ] Voice Transcription
- **nanobot**: `providers/transcription.py` — Groq Whisper API
- **MimiClaw**: Not implemented
- **Recommendation**: Requires extra HTTPS request to Whisper API: download Telegram voice -> forward -> get text

### [x] ~~Build-time Config File + Runtime NVS Override~~
- Implemented: `mimi_secrets.h` as build-time defaults, NVS as runtime override via CLI
- Two-layer config: build-time secrets → NVS fallback, CLI commands to set/show/reset

### [ ] WebSocket Gateway Protocol Enhancement
- **nanobot**: Gateway port 18790 + richer protocol
- **MimiClaw**: Basic JSON protocol, lacks streaming token push
- **Recommendation**: Add `{"type":"token","content":"..."}` streaming push

### [ ] Multi-Channel Manager
- **nanobot**: `channels/manager.py` — unified lifecycle management for multiple channels
- **MimiClaw**: Hardcoded in app_main()
- **Recommendation**: Not worth abstracting with few channels

### [ ] WhatsApp / Feishu Channels
- **nanobot**: `channels/whatsapp.py`, `channels/feishu.py`
- **MimiClaw**: Only Telegram + WebSocket
- **Recommendation**: Low priority, Telegram is sufficient

### [x] ~~Telegram Proxy Support (HTTP CONNECT)~~
- Implemented: HTTP CONNECT tunnel via `proxy/http_proxy.c`, configurable via `mimi_secrets.h` (`MIMI_SECRET_PROXY_HOST`/`MIMI_SECRET_PROXY_PORT`)

### [ ] Session Metadata Persistence
- **nanobot**: `session/manager.py` L136-153 — session file includes metadata line (created_at, updated_at)
- **MimiClaw**: JSONL only stores role/content/ts, no metadata header
- **Recommendation**: Low priority

---

## Completed Alignment

- [x] Telegram Bot long polling (getUpdates)
- [x] Message Bus (inbound/outbound queues)
- [x] Agent Loop with ReAct tool use (multi-turn, max 10 iterations)
- [x] Claude API (Anthropic Messages API, non-streaming, tool_use protocol)
- [x] Tool Registry + web_search tool (Brave Search API)
- [x] Context Builder (system prompt + bootstrap files + memory + tool guidance)
- [x] Memory Store (MEMORY.md + daily notes)
- [x] Session Manager (JSONL per chat_id, ring buffer history)
- [x] WebSocket Gateway (port 18789, JSON protocol)
- [x] Serial CLI (esp_console, debug/maintenance commands)
- [x] HTTP CONNECT Proxy (Telegram + Claude API + Brave Search via proxy tunnel)
- [x] OTA Update
- [x] WiFi Manager (build-time credentials, exponential backoff)
- [x] SPIFFS storage
- [x] Build-time config (`mimi_secrets.h`) + runtime NVS override via CLI

---

## Suggested Implementation Order

```
1. [done] Tool Use Loop + Tool Registry + web_search
2. Memory Write via Tool Use         <- makes the agent actually remember
3. Built-in Tools (read_file, write_file, message)
4. Telegram Allowlist (allow_from)   <- security essential
5. Bootstrap File Completion (AGENTS.md, TOOLS.md)
6. Subagent (simplified)
7. Telegram Markdown -> HTML
8. Media Handling
9. Cron / Heartbeat
10. Other enhancements
```

---

### WIFI ONBOARDING AP (`docs/WIFI_ONBOARDING_AP.md`)

# Wi-Fi AP Onboarding Guide

This guide documents the Wi-Fi onboarding flow for firmware builds that include the MimiClaw onboarding portal.

## What It Does

When onboarding is enabled, MimiClaw can expose a local Wi-Fi access point such as `MimiClaw-XXXX` and serve a configuration page at `http://192.168.4.1`.

Typical uses:

- first-time Wi-Fi setup without editing firmware
- updating saved Wi-Fi, model, proxy, or bot credentials later
- recovering a device that can no longer join the previous router

## Requirements

- a firmware build with the Wi-Fi onboarding portal enabled
- an ESP32-S3 board powered on and booted normally
- a phone or laptop that can join the temporary `MimiClaw-XXXX` network

## First-Time Setup

1. Power on the device.
2. Wait for the onboarding hotspot `MimiClaw-XXXX` to appear.
3. Join that hotspot from your phone or laptop.
4. Open `http://192.168.4.1` if the captive page does not open automatically.
5. Fill in at least:
   - `SSID`
   - `Password`
6. Add optional settings if needed:
   - `API Key`
   - `Model`
   - `Provider`
   - `Telegram`
   - `Feishu`
   - `Proxy`
   - `Search`
7. Click `Save & Restart`.
8. Wait for the device to reboot and join your normal Wi-Fi network.

## Updating Settings Later

If the firmware keeps the admin AP online after normal Wi-Fi connection, you can reconnect to `MimiClaw-XXXX` later and open `http://192.168.4.1` again.

The page should prefill the currently effective configuration so you can edit only the fields you want to change.

## Config Priority

The onboarding page follows the same config priority as the firmware:

1. saved NVS values
2. build-time defaults from `main/mimi_secrets.h`

That means:

- if a field exists in NVS, it overrides the build-time default
- if you clear a saved field and reboot, the device falls back to `main/mimi_secrets.h`
- if both NVS and build-time values are empty, the field stays empty

## Clearing Saved Values

On firmware versions that support clearing fields from the portal:

- leaving a field blank and saving removes the corresponding NVS key
- after reboot, the page shows the build-time fallback if one exists

If you need to wipe all saved runtime settings, use:

```text
mimi> config_reset
```

## Troubleshooting

### No `MimiClaw-XXXX` hotspot appears

- verify that the running firmware actually includes the onboarding portal
- if the device already connects successfully and does not keep the admin AP online, clear Wi-Fi config and reboot
- confirm the board has finished booting before scanning for Wi-Fi

### The page still shows old values

- refresh the page manually
- reconnect to the AP and open `http://192.168.4.1` again
- if needed, restart the browser once to clear stale captive portal state

### The build-time provider/model do not show up

If the device already has `model` or `provider` saved in NVS, the saved values win over `main/mimi_secrets.h`.

To return to build-time defaults:

- clear those fields in the portal and save, or
- run `config_reset`

## Notes

- The onboarding AP is typically local-only and intended for nearby configuration.
- Current onboarding implementations may use an open AP for simplicity, so avoid leaving it exposed longer than necessary.
- If your deployment needs stronger local protection, add an AP password before using the flow in production.

---

### Im Integration FEISHU SETUP (`docs/im-integration/FEISHU_SETUP.md`)

# Feishu / Lark Bot Configuration Guide

This guide walks through setting up a Feishu (or Lark) bot to work with MimiClaw, turning your ESP32-S3 into a Feishu-connected AI assistant.

## Table of Contents

- [Overview](#overview)
- [Prerequisites](#prerequisites)
- [Step 1: Create a Feishu App](#step-1-create-a-feishu-app)
- [Step 2: Configure App Permissions](#step-2-configure-app-permissions)
- [Step 3: Set Up Event Subscription](#step-3-set-up-event-subscription)
- [Step 4: Configure MimiClaw](#step-4-configure-mimiclaw)
- [Step 5: Network Setup](#step-5-network-setup)
- [Step 6: Publish and Test](#step-6-publish-and-test)
- [Architecture](#architecture)
- [CLI Commands](#cli-commands)
- [Troubleshooting](#troubleshooting)
- [References](#references)

## Overview

MimiClaw supports Feishu as a messaging channel alongside Telegram and WebSocket. The Feishu integration uses:

- **Webhook receiver** — the ESP32 runs an HTTP server on port 18790 to receive messages from Feishu
- **Send API** — MimiClaw sends replies via Feishu's REST API (`/im/v1/messages`)
- **Tenant access token** — automatic token management with background refresh

Both **direct messages (P2P)** and **group chats** are supported.

## Prerequisites

- A Feishu account (sign up at [feishu.cn](https://www.feishu.cn)) or a Lark account ([larksuite.com](https://www.larksuite.com))
- Admin access to create apps on [Feishu Open Platform](https://open.feishu.cn/) (or [Lark Developer](https://open.larksuite.com/))
- MimiClaw flashed on an ESP32-S3 with network access
- The ESP32 must be reachable from the internet (see [Network Setup](#step-5-network-setup))

## Step 1: Create a Feishu App

1. Go to [Feishu Open Platform](https://open.feishu.cn/) and sign in
2. Click **Create Custom App** (or "Create App" on Lark)
3. Fill in the app details:
   - **App Name**: Choose a name (e.g., "MimiClaw Bot")
   - **App Description**: Brief description of your bot
   - **App Icon**: Upload an icon (optional)
4. After creation, you will see your **App ID** and **App Secret** on the app's **Credentials & Basic Info** page

> **Important:** Save the **App ID** (`cli_xxxxxxxxxxxxxx`) and **App Secret** (`xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`). You will need these to configure MimiClaw.

## Step 2: Configure App Permissions

In your app's settings, go to **Permissions & Scopes** and add these required permissions:

| Permission | Scope ID | Description |
|-----------|----------|-------------|
| Read/Send messages | `im:message` | Receive and send messages |
| Send messages as bot | `im:message:send_as_bot` | Send messages as the bot identity |

To add permissions:

1. Navigate to **Permissions & Scopes** in the left sidebar
2. Search for each scope ID listed above
3. Click **Add** next to each permission
4. The permissions will take effect after you publish or update the app version

> **Note:** On Lark (international version), the permission names may differ slightly, but the scope IDs are the same.

## Step 3: Set Up Event Subscription

Event subscription allows Feishu to push new messages to your ESP32 in real-time.

### Configure the Webhook URL

1. In your app settings, go to **Event Subscriptions** (or "Events & Callbacks")
2. Set the **Request URL** to:

```
http://<ESP32_IP>:18790/feishu/events
```

Replace `<ESP32_IP>` with your ESP32's public IP or domain name.

3. Click **Save** — Feishu will send a verification challenge to the URL
4. MimiClaw automatically responds to the URL verification challenge, so this should succeed if the ESP32 is reachable

### Subscribe to Events

Add the following event:

| Event | Event ID | Description |
|-------|----------|-------------|
| Receive messages | `im.message.receive_v1` | Triggered when users send messages to the bot |

To add events:

1. In the **Event Subscriptions** page, click **Add Event**
2. Search for `im.message.receive_v1`
3. Select it and click **Confirm**

### Encryption Settings (Optional)

In the event subscription settings, you can optionally configure:

- **Verification Token** — used to verify that events come from Feishu
- **Encrypt Key** — encrypts event payloads

MimiClaw currently does not verify these tokens, so you can leave them empty for simplicity. For production use, consider implementing verification.

## Step 4: Configure MimiClaw

You need to provide the **App ID** and **App Secret** to MimiClaw.

### Option 1: Build-time Configuration

1. Copy the secrets template if you haven't already:

```bash
cp main/mimi_secrets.h.example main/mimi_secrets.h
```

2. Edit `main/mimi_secrets.h`:

```c
#define MIMI_SECRET_FEISHU_APP_ID     "cli_xxxxxxxxxxxxxx"
#define MIMI_SECRET_FEISHU_APP_SECRET "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

3. Rebuild and flash:

```bash
idf.py fullclean && idf.py build
idf.py -p PORT flash monitor
```

### Option 2: Runtime Configuration via Serial CLI

Connect to the UART (COM) port and run:

```
mimi> set_feishu_creds cli_xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

This saves credentials to NVS flash immediately — no rebuild needed.

### Verify Configuration

```
mimi> config_show
```

You should see `feishu_app_id: cli_****` and `feishu_app_secret: ****` in the output.

## Step 5: Network Setup

Feishu's servers need to reach your ESP32's webhook endpoint. There are several approaches:

### Option A: Direct Public IP

If your ESP32 is directly accessible from the internet:

```
Webhook URL: http://<PUBLIC_IP>:18790/feishu/events
```

### Option B: Port Forwarding

If the ESP32 is behind a router:

1. Log into your router's admin panel
2. Forward external port (e.g., 18790) to `<ESP32_LOCAL_IP>:18790`
3. Use your router's public IP in the webhook URL

### Option C: Reverse Proxy / Tunnel

For development or when port forwarding isn't possible:

- **ngrok**: `ngrok http <ESP32_IP>:18790`
- **frp**: Configure `frpc.toml` to proxy to the ESP32
- **Cloudflare Tunnel**: Route traffic through Cloudflare

Example with ngrok:

```bash
ngrok http 192.168.1.100:18790
# Use the generated URL: https://xxxx.ngrok.io/feishu/events
```

### Option D: Cloud Server Relay (Production)

For reliable production setups, deploy a lightweight reverse proxy on a cloud server (e.g., Volcengine ECS, AWS EC2) that forwards requests to your ESP32 via a VPN or WireGuard tunnel. This is the approach described in the [Volcengine OpenClaw deployment guide](https://www.volcengine.com/docs/6396/2189942).

> **Note:** Feishu requires the webhook URL to be accessible and respond within 3 seconds. Ensure your network path has low latency.

## Step 6: Publish and Test

### Enable the Bot

1. In your app settings, go to **Bot** in the left sidebar
2. Toggle **Enable Bot** to ON

### Publish the App

1. Go to **App Release** (or "Version Management & Release")
2. Click **Create Version**
3. Set a version number and description
4. Click **Submit for Review** (for enterprise apps) or **Publish** (for personal testing apps)

> **Tip:** For testing, you can use the app in "development mode" without publishing — just add yourself as a test user.

### Test the Bot

1. Open Feishu and search for your bot by name
2. Send a message to the bot in a direct chat
3. Check the ESP32 serial output — you should see the message being received and processed
4. The bot should reply through Feishu

For group chats:

1. Add the bot to a group
2. Mention the bot with `@BotName` followed by your message
3. The bot will process and reply in the group

## Architecture

```
Feishu Cloud
    |
    |  HTTP POST /feishu/events
    |  (im.message.receive_v1)
    v
[ESP32 Webhook Server :18790]
    |
    |  message_bus_push_inbound()
    v
[Message Bus] ──> [Agent Loop] ──> [Message Bus]
                   (Claude/GPT)         |
                                        |  outbound dispatch
                                        v
                              [feishu_send_message()]
                                        |
                                        |  POST /im/v1/messages
                                        v
                                   Feishu API
```

### Key Components

| Component | Description |
|-----------|-------------|
| **Webhook Server** | HTTP server on port 18790, handles event callbacks |
| **Token Manager** | Manages tenant access tokens, auto-refreshes before expiry |
| **Message Sender** | Sends text messages via Feishu REST API with auto-chunking |
| **Deduplication** | Prevents processing duplicate events from Feishu retries |

### Configuration Constants

These can be found in `main/mimi_config.h`:

| Constant | Default | Description |
|----------|---------|-------------|
| `MIMI_FEISHU_MAX_MSG_LEN` | 4096 | Max message length per chunk |
| `MIMI_FEISHU_WEBHOOK_PORT` | 18790 | Webhook HTTP server port |
| `MIMI_FEISHU_WEBHOOK_PATH` | `/feishu/events` | Webhook endpoint path |
| `MIMI_FEISHU_WEBHOOK_MAX_BODY` | 16 KB | Max webhook request body size |

## CLI Commands

| Command | Description |
|---------|-------------|
| `set_feishu_creds <app_id> <app_secret>` | Save Feishu credentials to NVS |
| `config_show` | Show all configuration (including Feishu, masked) |
| `config_reset` | Clear all NVS config, revert to build-time defaults |

## Troubleshooting

### Webhook URL verification fails

- Ensure the ESP32 is running and connected to WiFi: `wifi_status`
- Verify the webhook URL is reachable from the internet
- Check that port 18790 is not blocked by firewalls
- Look at ESP32 serial output for incoming HTTP requests

### Bot doesn't respond to messages

1. **Check credentials**: `config_show` should show Feishu app_id and app_secret
2. **Check event subscription**: Ensure `im.message.receive_v1` is subscribed in the Feishu app settings
3. **Check permissions**: Both `im:message` and `im:message:send_as_bot` must be granted
4. **Check serial output**: Look for message processing logs on the ESP32

### "Tenant access token" errors

- Verify your App ID and App Secret are correct
- The token auto-refreshes every 2 hours — if you just set credentials, wait a moment for the first token fetch
- Ensure the ESP32 can reach `https://open.feishu.cn` (check proxy settings if needed)

### Messages are truncated

Feishu has a 4096-character limit per message. MimiClaw automatically chunks long messages, but if you see issues, check the serial output for chunking errors.

### Bot works in DM but not in groups

- Ensure the bot is added to the group
- Users must `@mention` the bot in group chats for it to receive messages
- Check that group messaging permissions are enabled in the Feishu app settings

### Event subscription shows errors in Feishu console

- Feishu retries failed events up to 5 times with exponential backoff
- MimiClaw deduplicates retried events, so duplicate processing is not a concern
- If events consistently fail, check the ESP32's network connectivity

## References

- [Feishu Open Platform Documentation](https://open.feishu.cn/document/home/index)
- [Feishu Bot Development Guide](https://open.feishu.cn/document/client-docs/bot-v3/bot-overview)
- [Feishu Message API](https://open.feishu.cn/document/server-docs/im-v1/message/create)
- [Feishu Event Subscription Guide](https://open.feishu.cn/document/server-docs/event-subscription/event-subscription-guide)
- [Lark Developer Documentation](https://open.larksuite.com/document/home/index) (international version)
- [Volcengine OpenClaw Deployment Guide](https://www.volcengine.com/docs/6396/2189942) (Chinese)

---

### Tool Setup TAVILY SETUP (`docs/tool-setup/TAVILY_SETUP.md`)

# Tavily Web Search Configuration Guide

<p align="center">
  <img src="images/tavily-og.png" alt="Tavily - Search Engine for AI Agents" width="600" />
</p>

[Tavily](https://tavily.com) is a real-time search engine designed for AI agents and RAG workflows. MimiClaw uses Tavily as the **preferred** web search provider (with Brave Search as a fallback).

## Table of Contents

- [Overview](#overview)
- [Getting an API Key](#getting-an-api-key)
- [Configuring MimiClaw](#configuring-mimiclaw)
- [How It Works in MimiClaw](#how-it-works-in-mimiclaw)
- [API Details](#api-details)
- [Pricing](#pricing)
- [Rate Limits](#rate-limits)
- [Troubleshooting](#troubleshooting)

## Overview

MimiClaw's `web_search` tool allows the AI agent to search the internet for real-time information. When a Tavily API key is configured, MimiClaw will use Tavily as the search provider. If no Tavily key is set but a Brave Search key exists, it falls back to Brave Search.

**Why Tavily?**

- Purpose-built for AI agents — returns clean, structured results
- Includes relevance scoring for each result
- Supports advanced search depth for higher quality results
- 1,000 free API credits per month (no credit card required)

## Getting an API Key

1. **Sign up** at [app.tavily.com](https://app.tavily.com/home)
2. **Create an account** — you can sign in with Google or email
3. **Copy your API key** — it starts with `tvly-` and is available on the dashboard immediately
4. The free **Researcher** plan gives you **1,000 API credits per month** with no credit card required

Your API key will look like: `tvly-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`

## Configuring MimiClaw

There are two ways to configure the Tavily API key:

### Option 1: Build-time Configuration (Recommended for First Setup)

1. Copy the secrets template if you haven't already:

```bash
cp main/mimi_secrets.h.example main/mimi_secrets.h
```

2. Edit `main/mimi_secrets.h` and set your Tavily key:

```c
#define MIMI_SECRET_TAVILY_KEY      "tvly-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

3. Rebuild the firmware:

```bash
idf.py fullclean && idf.py build
```

4. Flash to your ESP32-S3:

```bash
idf.py -p PORT flash monitor
```

### Option 2: Runtime Configuration via Serial CLI

Connect to the UART (COM) port and run:

```
mimi> set_tavily_key tvly-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

This saves the key to NVS flash and takes effect immediately — no rebuild needed. Runtime values override build-time defaults.

### Verify Configuration

Check that the key is set:

```
mimi> config_show
```

You should see `tavily_key: tvly-****` (masked) in the output.

## How It Works in MimiClaw

When the AI agent needs to search the web, it calls the `web_search` tool. MimiClaw selects the search provider with this priority:

```
1. Tavily (if MIMI_SECRET_TAVILY_KEY is set)  <-- preferred
2. Brave Search (if MIMI_SECRET_SEARCH_KEY is set)
3. No search available (tool returns error)
```

The search flow:

```
User asks a question requiring web info
        |
        v
Agent decides to call web_search tool
        |
        v
MimiClaw sends POST to https://api.tavily.com/search
  - Authorization: Bearer tvly-xxxxx
  - Body: {"query": "...", "max_results": 5, "search_depth": "basic"}
        |
        v
Tavily returns structured results
        |
        v
Agent incorporates results into response
```

Each `basic` search costs **1 API credit**. With the free plan's 1,000 credits/month, that's about 33 searches per day.

## API Details

MimiClaw uses the Tavily Search API endpoint:

| Parameter | Value |
|-----------|-------|
| **Endpoint** | `POST https://api.tavily.com/search` |
| **Auth** | `Authorization: Bearer <API_KEY>` |
| **Content-Type** | `application/json` |

### Request Body (as sent by MimiClaw)

| Field | Type | Value | Description |
|-------|------|-------|-------------|
| `query` | string | (from agent) | The search query |
| `max_results` | integer | `5` | Number of results to return |
| `search_depth` | string | `"basic"` | Search depth (`basic` = 1 credit) |

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `query` | string | The executed search query |
| `results` | array | Ranked search results |
| `results[].title` | string | Result title |
| `results[].url` | string | Source URL |
| `results[].content` | string | Summary snippet |
| `results[].score` | float | Relevance score (0-1) |
| `response_time` | float | Execution time in seconds |

### HTTP Status Codes

| Code | Meaning |
|------|---------|
| 200 | Success |
| 400 | Bad Request — invalid parameters |
| 401 | Unauthorized — missing or invalid API key |
| 429 | Rate Limit — too many requests |
| 432 | Plan Limit — monthly quota exceeded |
| 500 | Server Error |

## Pricing

Tavily offers a generous free tier and several paid plans:

| Plan | Monthly Credits | Cost | Per-Credit Rate |
|------|-----------------|------|-----------------|
| **Researcher** | 1,000 | **Free** | -- |
| Project | 4,000 | $30 | $0.0075 |
| Bootstrap | 15,000 | $100 | $0.0067 |
| Startup | 38,000 | $220 | $0.0058 |
| Growth | 100,000 | $500 | $0.005 |
| Pay-as-you-go | Variable | $0.008/credit | $0.008 |

**Credit costs per search:**

- `basic` search depth: **1 credit**
- `advanced` search depth: **2 credits**

MimiClaw uses `basic` by default, so each search = 1 credit.

## Rate Limits

| Environment | Limit |
|-------------|-------|
| Development | 100 requests per minute |
| Production | 1,000 requests per minute |

For an ESP32-based agent, you will never hit these limits in practice.

## Troubleshooting

### "No search provider configured"

The agent returns this when neither Tavily nor Brave Search keys are set.

**Fix:** Set a Tavily API key via `set_tavily_key` CLI command or in `mimi_secrets.h`.

### Search returns empty results

- Verify your API key is correct: `config_show` should show `tavily_key: tvly-****`
- Check your monthly credit balance at [app.tavily.com](https://app.tavily.com/home)
- Ensure the ESP32 has internet access: `wifi_status`

### HTTP 401 Unauthorized

Your API key is invalid or expired. Generate a new one at [app.tavily.com](https://app.tavily.com/home).

### HTTP 432 Plan Limit

You've exceeded your monthly credit quota. Wait for the next billing cycle or upgrade your plan.

### Search works but results are poor

Tavily's `basic` search depth is optimized for speed. The quality is generally sufficient for an AI agent's needs. If you need higher quality, you can modify `SEARCH_RESULT_COUNT` in `tool_web_search.c`.

## References

- [Tavily Documentation](https://docs.tavily.com)
- [Tavily API Reference](https://docs.tavily.com/documentation/api-reference/endpoint/search)
- [Tavily API Credits](https://docs.tavily.com/documentation/api-credits)
- [Tavily Rate Limits](https://docs.tavily.com/documentation/rate-limits)
- [Tavily Playground](https://app.tavily.com/playground)

---

--- METRICS ---
- Files Extracted: 9
- Estimated Token Budget: ~14950 tokens
- Canonical Reference: https://codewiki.google/github.com/memovai/mimiclaw
