# RayClaw Developer Guide
## Quick start
```sh
git clone <repo-url>
cd rayclaw
cp rayclaw.config.example.yaml rayclaw.config.yaml
# Edit rayclaw.config.yaml with your credentials
cargo run -- start
```
## Prerequisites
- Rust 1.70+ (2021 edition)
- At least one enabled channel adapter (Telegram bot token from @BotFather, Discord bot token from Discord Developer Portal, Slack app/bot tokens, Feishu/Lark app credentials, or Web UI)
- An Anthropic API key
No other external dependencies. SQLite is bundled via `rusqlite`.
## Project structure
```
src/
main.rs # Entry point. Parses CLI args, initializes subsystems, starts platform runtimes.
config.rs # Config struct. All settings loaded from rayclaw.config.yaml.
error.rs # RayClawError enum (thiserror). Centralized error types.
telegram.rs # Core orchestration:
# - Telegram message handler
# - Agentic tool-use loop (process_with_claude)
# - Session resume (load/save full message state)
# - Context compaction (summarize old messages)
# - Continuous typing indicator
# - Group chat catch-up logic
# - Response splitting
discord.rs # Discord message handler (serenity gateway), reuses process_with_claude
channels/slack.rs # Slack adapter (Socket Mode WebSocket)
channels/feishu.rs # Feishu/Lark adapter (WebSocket long connection or webhook)
claude.rs # Anthropic Messages API client:
# - Request/response types with serde
# - HTTP calls with retry on 429
# - Content block enums (Text, ToolUse, ToolResult)
db.rs # SQLite database:
# - Schema creation (chats, messages, scheduled_tasks, sessions)
# - Message storage and retrieval
# - Session save/load/delete for resume
# - Scheduled task CRUD
# - Catch-up query (messages since last bot response)
memory.rs # MemoryManager:
# - Reads/writes CLAUDE.md files (global + per-chat)
# - Builds memory context for system prompts
scheduler.rs # Background scheduler:
# - Spawns a tokio task that polls every 60s
# - Finds due tasks, runs agent loop, sends results
# - Computes next run time for cron tasks
tools/
mod.rs # Tool trait, ToolRegistry, ToolResult.
# Registry takes data_dir, Bot, Arc<Database>.
bash.rs # Shell command execution (tokio::process::Command)
read_file.rs # File reading with line numbers, offset/limit
write_file.rs # File creation/overwrite, auto-creates directories
edit_file.rs # Find/replace editing, validates uniqueness
glob.rs # File pattern matching (glob crate)
grep.rs # Recursive regex search, directory traversal
memory.rs # read_memory / write_memory tools
web_search.rs # DuckDuckGo HTML search, regex result parsing
web_fetch.rs # URL fetching, HTML tag stripping, 20KB limit
send_message.rs # Mid-conversation messaging (Telegram/Discord)
schedule.rs # 5 scheduling tools (create/list/pause/resume/cancel)
sub_agent.rs # Sub-agent tool with restricted tool registry (9 tools)
```
## Architecture overview
### Data flow
```
Platform message (via adapter)
|
v
Store in SQLite (message + chat metadata)
|
v
Determine response: private=always, group=@mention only
|
v
Start typing indicator (tokio::spawn, every 4s)
|
v
Load session or history:
- Try loading saved session (full message state with tool blocks)
- If session exists: append new user messages since last save
- If no session: fall back to DB history
- Private: last N messages
- Group: all messages since last bot response (catch-up)
|
v
Build system prompt (bot identity + memory context + chat_id)
|
v
Compact if needed (messages > max_session_messages):
- Summarize old messages via Claude
- Keep recent messages verbatim
|
v
Agentic loop (up to max_tool_iterations):
1. Call Claude API with messages + tool definitions
2. If stop_reason == "tool_use" -> execute tools -> append results -> loop
3. If stop_reason == "end_turn" -> extract text -> return
|
v
Strip image base64 data, save session to SQLite
|
v
Abort typing indicator
|
v
Send response (split at channel limits: Telegram 4096 / Discord 2000 / Slack 4000 / Feishu 4000)
|
v
Store bot response in SQLite
```
The same core loop is reused across adapters. Adding a new platform should primarily require a new ingress/egress adapter that maps platform events into the shared `process_with_claude` flow.
### Key types
| `AppState` | `telegram.rs` | Shared state: config, bot, db, memory, claude client, tool registry |
| `Database` | `db.rs` | SQLite wrapper with `Mutex<Connection>` |
| `ToolRegistry` | `tools/mod.rs` | Holds all `Box<dyn Tool>`, dispatches by name |
| `Tool` trait | `tools/mod.rs` | `name()`, `definition()`, `execute()` |
| `ClaudeClient` | `claude.rs` | HTTP client for Anthropic API |
| `MemoryManager` | `memory.rs` | CLAUDE.md file reader/writer |
### Shared state
`AppState` is wrapped in `Arc` and shared:
- Telegram handler has `Arc<AppState>` via dptree dependencies
- Discord handler has `Arc<AppState>` via serenity event handler state
- Scheduler gets `Arc<AppState>` at spawn time
- Tools that need `Bot` or `Database` hold their own clones/arcs (passed at construction)
### Multi-chat permission model
- `control_chat_ids` in `rayclaw.config.yaml` defines privileged chats.
- Tool execution receives trusted caller context from `process_with_claude` (not from model-provided args).
- Non-control chats can only operate on their own `chat_id`.
- Control chats can perform cross-chat actions.
- `write_memory` with `scope: "global"` is restricted to control chats.
- Enforcement currently applies to `send_message`, scheduler tools, `export_chat`, `todo_*`, and chat-scoped memory operations.
### Database tables
**chats:**
| chat_id | INTEGER PK | Channel-scoped chat ID |
| chat_title | TEXT | Chat title (nullable) |
| chat_type | TEXT | "private" or "group" |
| last_message_time | TEXT | ISO 8601 timestamp |
**messages:**
| id | TEXT | Message ID (PK with chat_id) |
| chat_id | INTEGER | Channel-scoped chat ID |
| sender_name | TEXT | Username or first name |
| content | TEXT | Message text |
| is_from_bot | INTEGER | 0 or 1 |
| timestamp | TEXT | ISO 8601 timestamp |
**scheduled_tasks:**
| id | INTEGER PK | Auto-increment task ID |
| chat_id | INTEGER | Target chat for results |
| prompt | TEXT | Instruction to execute |
| schedule_type | TEXT | "cron" or "once" |
| schedule_value | TEXT | Cron expression or ISO timestamp |
| next_run | TEXT | Next scheduled execution time |
| last_run | TEXT | Last execution time (nullable) |
| status | TEXT | "active", "paused", "completed", "cancelled" |
| created_at | TEXT | Creation timestamp |
**sessions:**
| chat_id | INTEGER PK | Channel-scoped chat ID |
| messages_json | TEXT | Serialized Vec<Message> JSON (full conversation state) |
| updated_at | TEXT | ISO 8601 timestamp of last save |
**task_run_logs:**
| id | INTEGER PK | Auto-increment log ID |
| task_id | INTEGER | Associated scheduled task |
| chat_id | INTEGER | Chat the task ran in |
| started_at | TEXT | Run start timestamp |
| finished_at | TEXT | Run end timestamp |
| duration_ms | INTEGER | Run duration in milliseconds |
| success | INTEGER | 0 or 1 |
| result_summary | TEXT | Summary of run result (nullable) |
## Adding a new tool
1. Create `src/tools/my_tool.rs`:
```rust
use async_trait::async_trait;
use serde_json::json;
use super::{schema_object, Tool, ToolResult};
use crate::claude::ToolDefinition;
pub struct MyTool;
#[async_trait]
impl Tool for MyTool {
fn name(&self) -> &str {
"my_tool"
}
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "my_tool".into(),
description: "What this tool does".into(),
input_schema: schema_object(
json!({
"param1": {
"type": "string",
"description": "Description of param1"
}
}),
&["param1"],
),
}
}
async fn execute(&self, input: serde_json::Value) -> ToolResult {
let param1 = match input.get("param1").and_then(|v| v.as_str()) {
Some(p) => p,
None => return ToolResult::error("Missing param1".into()),
};
// Do work...
ToolResult::success(format!("Result: {param1}"))
}
}
```
2. Add `pub mod my_tool;` to `src/tools/mod.rs`
3. Register in `ToolRegistry::new()`:
```rust
Box::new(my_tool::MyTool),
```
If your tool needs shared state (like `Bot` or `Arc<Database>`), add a constructor:
```rust
pub struct MyTool {
db: Arc<Database>,
}
impl MyTool {
pub fn new(db: Arc<Database>) -> Self {
MyTool { db }
}
}
```
And pass it in `ToolRegistry::new()`:
```rust
Box::new(my_tool::MyTool::new(db.clone())),
```
## Adding a new platform adapter
The core agent flow is already shared. New platform support should be implemented as an adapter around that core:
1. Add a new runtime module (for example `src/<platform>.rs`) that listens to platform events.
2. Normalize incoming platform messages into the canonical fields used by persistence and `process_with_claude`:
- stable `chat_id`
- `chat_type` (`private`/`group`-like semantics)
- sender display name
- text/media blocks
3. Reuse `process_with_claude(state, chat_id, sender, chat_type, override_prompt)` for inference/tool loop/session handling.
4. Implement outbound reply sending with:
- platform length splitting policy
- mention semantics for group/server channels
- attachment support parity with `send_message` where applicable
5. Keep chat identity stable across restarts so `sessions`, `messages`, scheduler, and memory scopes remain consistent.
6. Enforce existing authorization boundaries (for example `control_chat_ids`) in any platform-specific entry points.
7. Add end-to-end tests to `TEST.md` mirroring existing platform suites (DM/private, group mention, reset, limits, failure handling).
## Scheduler internals
The scheduler is a `tokio::spawn` task started in `run_bot()`. Every 60 seconds it:
1. Queries `scheduled_tasks` for rows where `status = 'active' AND next_run <= NOW()`
2. For each due task, calls `process_with_claude()` with `override_prompt = Some(task.prompt)`
3. Sends the agent's response to the task's `chat_id`
4. For cron tasks: computes next run from the cron expression and updates the row
5. For one-shot tasks: sets `status = 'completed'`
Cron expressions use the `cron` crate's 6-field format: `sec min hour dom month dow`.
## Debugging
```sh
# Verbose logging
RUST_LOG=debug cargo run -- start
# Just rayclaw logs
RUST_LOG=rayclaw=debug cargo run -- start
# Check database directly
sqlite3 rayclaw.data/runtime/rayclaw.db
sqlite> SELECT * FROM messages ORDER BY timestamp DESC LIMIT 10;
sqlite> SELECT * FROM scheduled_tasks;
sqlite> SELECT * FROM chats;
```
## Common tasks
| Change the model | Set `model: "claude-sonnet-4-20250514"` in `rayclaw.config.yaml` |
| Increase context window | Set `max_history_messages: 100` in `rayclaw.config.yaml` (uses more tokens) |
| Increase tool iterations | Set `max_tool_iterations: 200` in `rayclaw.config.yaml` |
| Reset memory | Delete files under `rayclaw.data/runtime/groups/` |
| Reset all data | Delete the `rayclaw.data/` directory |
| Tune compaction threshold | Set `max_session_messages: 60` in `rayclaw.config.yaml` (higher = more context before compaction) |
| Keep more recent messages | Set `compact_keep_recent: 30` in `rayclaw.config.yaml` (more recent messages kept verbatim) |
| Reset a chat session | Send `/reset` in the chat, or: `sqlite3 rayclaw.data/runtime/rayclaw.db "DELETE FROM sessions WHERE chat_id=XXXX;"` |
| Cancel all scheduled tasks | `sqlite3 rayclaw.data/runtime/rayclaw.db "UPDATE scheduled_tasks SET status='cancelled' WHERE status='active';"` |
## Build
```sh
cargo build # Dev build (fast compile, slow runtime)
cargo build --release # Release build (slow compile, fast runtime)
cargo run -- start # Run dev build
cargo run -- help # Show CLI help
```
The release binary is fully self-contained -- no runtime dependencies, no database server, no config files beyond `rayclaw.config.yaml`.
## Dependencies
| teloxide | 0.17 | Telegram Bot API |
| serenity | 0.12 | Discord Gateway/API |
| tokio | 1 | Async runtime |
| reqwest | 0.12 | HTTP client (Anthropic API, web fetch/search) |
| rusqlite | 0.32 | SQLite (bundled) |
| serde / serde_json | 1 | Serialization |
| async-trait | 0.1 | Async trait support |
| chrono | 0.4 | Date/time handling |
| cron | 0.13 | Cron expression parsing |
| urlencoding | 2 | URL encoding for web search |
| regex | 1 | Regex for grep tool and HTML parsing |
| glob | 0.3 | File pattern matching |
| uuid | 1 | Message ID generation |
| thiserror | 2 | Error derive macro |
| anyhow | 1 | Error propagation |
| tracing | 0.1 | Logging |