feat: initial release — ZeroClaw v0.1.0
- 22 AI providers (OpenRouter, Anthropic, OpenAI, Mistral, etc.) - 7 channels (CLI, Telegram, Discord, Slack, iMessage, Matrix, Webhook) - 5-step onboarding wizard with Project Context personalization - OpenClaw-aligned system prompt (SOUL.md, IDENTITY.md, USER.md, AGENTS.md, etc.) - SQLite memory backend with auto-save - Skills system with on-demand loading - Security: autonomy levels, command allowlists, cost limits - 532 tests passing, 0 clippy warnings
This commit is contained in:
commit
05cb353f7f
71 changed files with 15757 additions and 0 deletions
124
examples/custom_channel.rs
Normal file
124
examples/custom_channel.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
//! Example: Implementing a custom Channel for ZeroClaw
|
||||
//!
|
||||
//! Channels let ZeroClaw communicate through any messaging platform.
|
||||
//! Implement the Channel trait, register it, and the agent works everywhere.
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Mirrors src/channels/traits.rs
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChannelMessage {
|
||||
pub id: String,
|
||||
pub sender: String,
|
||||
pub content: String,
|
||||
pub channel: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Channel: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
async fn send(&self, message: &str, recipient: &str) -> Result<()>;
|
||||
async fn listen(&self, tx: mpsc::Sender<ChannelMessage>) -> Result<()>;
|
||||
async fn health_check(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Example: Telegram channel via Bot API
|
||||
pub struct TelegramChannel {
|
||||
bot_token: String,
|
||||
allowed_users: Vec<String>,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl TelegramChannel {
|
||||
pub fn new(bot_token: &str, allowed_users: Vec<String>) -> Self {
|
||||
Self {
|
||||
bot_token: bot_token.to_string(),
|
||||
allowed_users,
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn api_url(&self, method: &str) -> String {
|
||||
format!("https://api.telegram.org/bot{}/{method}", self.bot_token)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for TelegramChannel {
|
||||
fn name(&self) -> &str {
|
||||
"telegram"
|
||||
}
|
||||
|
||||
async fn send(&self, message: &str, chat_id: &str) -> Result<()> {
|
||||
self.client
|
||||
.post(&self.api_url("sendMessage"))
|
||||
.json(&serde_json::json!({
|
||||
"chat_id": chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "Markdown",
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen(&self, tx: mpsc::Sender<ChannelMessage>) -> Result<()> {
|
||||
let mut offset: i64 = 0;
|
||||
|
||||
loop {
|
||||
let resp = self
|
||||
.client
|
||||
.get(&self.api_url("getUpdates"))
|
||||
.query(&[("offset", offset.to_string()), ("timeout", "30".into())])
|
||||
.send()
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
|
||||
if let Some(updates) = resp["result"].as_array() {
|
||||
for update in updates {
|
||||
if let Some(msg) = update.get("message") {
|
||||
let sender = msg["from"]["username"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
if !self.allowed_users.is_empty() && !self.allowed_users.contains(&sender) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let channel_msg = ChannelMessage {
|
||||
id: msg["message_id"].to_string(),
|
||||
sender,
|
||||
content: msg["text"].as_str().unwrap_or("").to_string(),
|
||||
channel: "telegram".into(),
|
||||
timestamp: msg["date"].as_u64().unwrap_or(0),
|
||||
};
|
||||
|
||||
if tx.send(channel_msg).await.is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
offset = update["update_id"].as_i64().unwrap_or(offset) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> bool {
|
||||
self.client
|
||||
.get(&self.api_url("getMe"))
|
||||
.send()
|
||||
.await
|
||||
.map(|r| r.status().is_success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("This is an example — see CONTRIBUTING.md for integration steps.");
|
||||
println!("Add your channel config to ChannelsConfig in src/config/schema.rs");
|
||||
}
|
||||
160
examples/custom_memory.rs
Normal file
160
examples/custom_memory.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
//! Example: Implementing a custom Memory backend for ZeroClaw
|
||||
//!
|
||||
//! This demonstrates how to create a Redis-backed memory backend.
|
||||
//! The Memory trait is async and pluggable — implement it for any storage.
|
||||
//!
|
||||
//! Run: cargo run --example custom_memory
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// ── Re-define the trait types (in your app, import from zeroclaw::memory) ──
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum MemoryCategory {
|
||||
Core,
|
||||
Daily,
|
||||
Conversation,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MemoryEntry {
|
||||
pub id: String,
|
||||
pub key: String,
|
||||
pub content: String,
|
||||
pub category: MemoryCategory,
|
||||
pub timestamp: String,
|
||||
pub score: Option<f64>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Memory: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
async fn store(&self, key: &str, content: &str, category: MemoryCategory)
|
||||
-> anyhow::Result<()>;
|
||||
async fn recall(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryEntry>>;
|
||||
async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>>;
|
||||
async fn forget(&self, key: &str) -> anyhow::Result<bool>;
|
||||
async fn count(&self) -> anyhow::Result<usize>;
|
||||
}
|
||||
|
||||
// ── Your custom implementation ─────────────────────────────────────
|
||||
|
||||
/// In-memory HashMap backend (great for testing or ephemeral sessions)
|
||||
pub struct InMemoryBackend {
|
||||
store: Mutex<HashMap<String, MemoryEntry>>,
|
||||
}
|
||||
|
||||
impl InMemoryBackend {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
store: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Memory for InMemoryBackend {
|
||||
fn name(&self) -> &str {
|
||||
"in-memory"
|
||||
}
|
||||
|
||||
async fn store(
|
||||
&self,
|
||||
key: &str,
|
||||
content: &str,
|
||||
category: MemoryCategory,
|
||||
) -> anyhow::Result<()> {
|
||||
let entry = MemoryEntry {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
key: key.to_string(),
|
||||
content: content.to_string(),
|
||||
category,
|
||||
timestamp: chrono::Local::now().to_rfc3339(),
|
||||
score: None,
|
||||
};
|
||||
self.store
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?
|
||||
.insert(key.to_string(), entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recall(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryEntry>> {
|
||||
let store = self.store.lock().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let query_lower = query.to_lowercase();
|
||||
|
||||
let mut results: Vec<MemoryEntry> = store
|
||||
.values()
|
||||
.filter(|e| e.content.to_lowercase().contains(&query_lower))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
results.truncate(limit);
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>> {
|
||||
let store = self.store.lock().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(store.get(key).cloned())
|
||||
}
|
||||
|
||||
async fn forget(&self, key: &str) -> anyhow::Result<bool> {
|
||||
let mut store = self.store.lock().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(store.remove(key).is_some())
|
||||
}
|
||||
|
||||
async fn count(&self) -> anyhow::Result<usize> {
|
||||
let store = self.store.lock().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(store.len())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Demo usage ─────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let brain = InMemoryBackend::new();
|
||||
|
||||
println!("🧠 ZeroClaw Memory Demo — InMemoryBackend\n");
|
||||
|
||||
// Store some memories
|
||||
brain
|
||||
.store("user_lang", "User prefers Rust", MemoryCategory::Core)
|
||||
.await?;
|
||||
brain
|
||||
.store("user_tz", "Timezone is EST", MemoryCategory::Core)
|
||||
.await?;
|
||||
brain
|
||||
.store(
|
||||
"today_note",
|
||||
"Completed memory system implementation",
|
||||
MemoryCategory::Daily,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Stored {} memories", brain.count().await?);
|
||||
|
||||
// Recall by keyword
|
||||
let results = brain.recall("Rust", 5).await?;
|
||||
println!("\nRecall 'Rust' → {} results:", results.len());
|
||||
for entry in &results {
|
||||
println!(" [{:?}] {}: {}", entry.category, entry.key, entry.content);
|
||||
}
|
||||
|
||||
// Get by key
|
||||
if let Some(entry) = brain.get("user_tz").await? {
|
||||
println!("\nGet 'user_tz' → {}", entry.content);
|
||||
}
|
||||
|
||||
// Forget
|
||||
let removed = brain.forget("user_tz").await?;
|
||||
println!("Forget 'user_tz' → removed: {removed}");
|
||||
println!("Remaining: {} memories", brain.count().await?);
|
||||
|
||||
println!("\n✅ Memory backend works! Implement the Memory trait for any storage.");
|
||||
Ok(())
|
||||
}
|
||||
65
examples/custom_provider.rs
Normal file
65
examples/custom_provider.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
//! Example: Implementing a custom Provider for ZeroClaw
|
||||
//!
|
||||
//! This shows how to add a new LLM backend in ~30 lines of code.
|
||||
//! Copy this file, modify the API call, and register in `src/providers/mod.rs`.
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
// In a real implementation, you'd import from the crate:
|
||||
// use zeroclaw::providers::traits::Provider;
|
||||
|
||||
/// Minimal Provider trait (mirrors src/providers/traits.rs)
|
||||
#[async_trait]
|
||||
pub trait Provider: Send + Sync {
|
||||
async fn chat(&self, message: &str, model: &str, temperature: f64) -> Result<String>;
|
||||
}
|
||||
|
||||
/// Example: Ollama local provider
|
||||
pub struct OllamaProvider {
|
||||
base_url: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl OllamaProvider {
|
||||
pub fn new(base_url: Option<&str>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.unwrap_or("http://localhost:11434").to_string(),
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for OllamaProvider {
|
||||
async fn chat(&self, message: &str, model: &str, temperature: f64) -> Result<String> {
|
||||
let url = format!("{}/api/generate", self.base_url);
|
||||
|
||||
let body = serde_json::json!({
|
||||
"model": model,
|
||||
"prompt": message,
|
||||
"temperature": temperature,
|
||||
"stream": false,
|
||||
});
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
|
||||
resp["response"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("No response field in Ollama reply"))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("This is an example — see CONTRIBUTING.md for integration steps.");
|
||||
println!("Register your provider in src/providers/mod.rs:");
|
||||
println!(" \"ollama\" => Ok(Box::new(ollama::OllamaProvider::new(None))),");
|
||||
}
|
||||
76
examples/custom_tool.rs
Normal file
76
examples/custom_tool.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//! Example: Implementing a custom Tool for ZeroClaw
|
||||
//!
|
||||
//! This shows how to add a new tool the agent can use.
|
||||
//! Tools are the agent's hands — they let it interact with the world.
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// Mirrors src/tools/traits.rs
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ToolResult {
|
||||
pub success: bool,
|
||||
pub output: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Tool: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn description(&self) -> &str;
|
||||
fn parameters_schema(&self) -> Value;
|
||||
async fn execute(&self, args: Value) -> Result<ToolResult>;
|
||||
}
|
||||
|
||||
/// Example: A tool that fetches a URL and returns the status code
|
||||
pub struct HttpGetTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for HttpGetTool {
|
||||
fn name(&self) -> &str {
|
||||
"http_get"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Fetch a URL and return the HTTP status code and content length"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": { "type": "string", "description": "URL to fetch" }
|
||||
},
|
||||
"required": ["url"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value) -> Result<ToolResult> {
|
||||
let url = args["url"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'url' parameter"))?;
|
||||
|
||||
match reqwest::get(url).await {
|
||||
Ok(resp) => {
|
||||
let status = resp.status().as_u16();
|
||||
let len = resp.content_length().unwrap_or(0);
|
||||
Ok(ToolResult {
|
||||
success: status < 400,
|
||||
output: format!("HTTP {status} — {len} bytes"),
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
Err(e) => Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!("Request failed: {e}")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("This is an example — see CONTRIBUTING.md for integration steps.");
|
||||
println!("Register your tool in src/tools/mod.rs default_tools()");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue