style: cargo fmt — fix all formatting for CI

Ran cargo fmt across entire codebase to pass CI's cargo fmt --check.
No logic changes, only whitespace/formatting.
This commit is contained in:
argenis de la rosa 2026-02-13 16:03:50 -05:00
parent a5887ad2dc
commit bc31e4389b
24 changed files with 613 additions and 242 deletions

View file

@ -46,8 +46,10 @@ pub async fn run(
));
// ── Memory (the brain) ────────────────────────────────────────
let mem: Arc<dyn Memory> =
Arc::from(memory::create_memory(&config.memory, &config.workspace_dir)?);
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory(
&config.memory,
&config.workspace_dir,
)?);
tracing::info!(backend = mem.name(), "Memory initialized");
// ── Tools (including memory tools) ────────────────────────────

View file

@ -39,8 +39,7 @@ impl DiscordChannel {
}
}
const BASE64_ALPHABET: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const BASE64_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Minimal base64 decode (no extra dep) — only needs to decode the user ID portion
#[allow(clippy::cast_possible_truncation)]
@ -155,8 +154,7 @@ impl Channel for DiscordChannel {
let (hb_tx, mut hb_rx) = tokio::sync::mpsc::channel::<()>(1);
let hb_interval = heartbeat_interval;
tokio::spawn(async move {
let mut interval =
tokio::time::interval(std::time::Duration::from_millis(hb_interval));
let mut interval = tokio::time::interval(std::time::Duration::from_millis(hb_interval));
loop {
interval.tick().await;
if hb_tx.send(()).await.is_err() {

View file

@ -23,9 +23,9 @@ impl IMessageChannel {
if self.allowed_contacts.iter().any(|u| u == "*") {
return true;
}
self.allowed_contacts.iter().any(|u| {
u.eq_ignore_ascii_case(sender)
})
self.allowed_contacts
.iter()
.any(|u| u.eq_ignore_ascii_case(sender))
}
}

View file

@ -93,10 +93,7 @@ impl MatrixChannel {
}
async fn get_my_user_id(&self) -> anyhow::Result<String> {
let url = format!(
"{}/_matrix/client/v3/account/whoami",
self.homeserver
);
let url = format!("{}/_matrix/client/v3/account/whoami", self.homeserver);
let resp = self
.client
.get(&url)
@ -250,10 +247,7 @@ impl Channel for MatrixChannel {
}
async fn health_check(&self) -> bool {
let url = format!(
"{}/_matrix/client/v3/account/whoami",
self.homeserver
);
let url = format!("{}/_matrix/client/v3/account/whoami", self.homeserver);
let Ok(resp) = self
.client
.get(&url)
@ -413,8 +407,14 @@ mod tests {
let room = resp.rooms.join.get("!room:matrix.org").unwrap();
assert_eq!(room.timeline.events.len(), 1);
assert_eq!(room.timeline.events[0].sender, "@user:matrix.org");
assert_eq!(room.timeline.events[0].content.body.as_deref(), Some("Hello!"));
assert_eq!(room.timeline.events[0].content.msgtype.as_deref(), Some("m.text"));
assert_eq!(
room.timeline.events[0].content.body.as_deref(),
Some("Hello!")
);
assert_eq!(
room.timeline.events[0].content.msgtype.as_deref(),
Some("m.text")
);
}
#[test]

View file

@ -75,8 +75,15 @@ pub fn build_system_prompt(
for skill in skills {
let _ = writeln!(prompt, " <skill>");
let _ = writeln!(prompt, " <name>{}</name>", skill.name);
let _ = writeln!(prompt, " <description>{}</description>", skill.description);
let location = workspace_dir.join("skills").join(&skill.name).join("SKILL.md");
let _ = writeln!(
prompt,
" <description>{}</description>",
skill.description
);
let location = workspace_dir
.join("skills")
.join(&skill.name)
.join("SKILL.md");
let _ = writeln!(prompt, " <location>{}</location>", location.display());
let _ = writeln!(prompt, " </skill>");
}
@ -84,11 +91,16 @@ pub fn build_system_prompt(
}
// ── 4. Workspace ────────────────────────────────────────────
let _ = writeln!(prompt, "## Workspace\n\nWorking directory: `{}`\n", workspace_dir.display());
let _ = writeln!(
prompt,
"## Workspace\n\nWorking directory: `{}`\n",
workspace_dir.display()
);
// ── 5. Bootstrap files (injected into context) ──────────────
prompt.push_str("## Project Context\n\n");
prompt.push_str("The following workspace files define your identity, behavior, and context.\n\n");
prompt
.push_str("The following workspace files define your identity, behavior, and context.\n\n");
let bootstrap_files = [
"AGENTS.md",
@ -118,8 +130,8 @@ pub fn build_system_prompt(
let _ = writeln!(prompt, "## Current Date & Time\n\nTimezone: {tz}\n");
// ── 7. Runtime ──────────────────────────────────────────────
let host = hostname::get()
.map_or_else(|_| "unknown".into(), |h| h.to_string_lossy().to_string());
let host =
hostname::get().map_or_else(|_| "unknown".into(), |h| h.to_string_lossy().to_string());
let _ = writeln!(
prompt,
"## Runtime\n\nHost: {host} | OS: {} | Model: {model_name}\n",
@ -180,10 +192,7 @@ pub fn handle_command(command: super::ChannelCommands, config: &Config) -> Resul
("iMessage", config.channels_config.imessage.is_some()),
("Matrix", config.channels_config.matrix.is_some()),
] {
println!(
" {} {name}",
if configured { "" } else { "" }
);
println!(" {} {name}", if configured { "" } else { "" });
}
println!("\nTo start channels: zeroclaw channel start");
println!("To configure: zeroclaw onboard");
@ -193,7 +202,9 @@ pub fn handle_command(command: super::ChannelCommands, config: &Config) -> Resul
channel_type,
config: _,
} => {
anyhow::bail!("Channel type '{channel_type}' — use `zeroclaw onboard` to configure channels");
anyhow::bail!(
"Channel type '{channel_type}' — use `zeroclaw onboard` to configure channels"
);
}
super::ChannelCommands::Remove { name } => {
anyhow::bail!("Remove channel '{name}' — edit ~/.zeroclaw/config.toml directly");
@ -213,8 +224,10 @@ pub async fn start_channels(config: Config) -> Result<()> {
.clone()
.unwrap_or_else(|| "anthropic/claude-sonnet-4-20250514".into());
let temperature = config.default_temperature;
let mem: Arc<dyn Memory> =
Arc::from(memory::create_memory(&config.memory, &config.workspace_dir)?);
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory(
&config.memory,
&config.workspace_dir,
)?);
// Build system prompt from workspace identity files + skills
let workspace = config.workspace_dir.clone();
@ -233,7 +246,14 @@ pub async fn start_channels(config: Config) -> Result<()> {
let system_prompt = build_system_prompt(&workspace, &model, &tool_descs, &skills);
if !skills.is_empty() {
println!(" 🧩 Skills: {}", skills.iter().map(|s| s.name.as_str()).collect::<Vec<_>>().join(", "));
println!(
" 🧩 Skills: {}",
skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
// Collect active channels
@ -263,9 +283,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
}
if let Some(ref im) = config.channels_config.imessage {
channels.push(Arc::new(IMessageChannel::new(
im.allowed_contacts.clone(),
)));
channels.push(Arc::new(IMessageChannel::new(im.allowed_contacts.clone())));
}
if let Some(ref mx) = config.channels_config.matrix {
@ -284,8 +302,19 @@ pub async fn start_channels(config: Config) -> Result<()> {
println!("🦀 ZeroClaw Channel Server");
println!(" 🤖 Model: {model}");
println!(" 🧠 Memory: {} (auto-save: {})", config.memory.backend, if config.memory.auto_save { "on" } else { "off" });
println!(" 📡 Channels: {}", channels.iter().map(|c| c.name()).collect::<Vec<_>>().join(", "));
println!(
" 🧠 Memory: {} (auto-save: {})",
config.memory.backend,
if config.memory.auto_save { "on" } else { "off" }
);
println!(
" 📡 Channels: {}",
channels
.iter()
.map(|c| c.name())
.collect::<Vec<_>>()
.join(", ")
);
println!();
println!(" Listening for messages... (Ctrl+C to stop)");
println!();
@ -331,7 +360,10 @@ pub async fn start_channels(config: Config) -> Result<()> {
}
// Call the LLM with system prompt (identity + soul + tools)
match provider.chat_with_system(Some(&system_prompt), &msg.content, &model, temperature).await {
match provider
.chat_with_system(Some(&system_prompt), &msg.content, &model, temperature)
.await
{
Ok(response) => {
println!(
" 🤖 Reply: {}",
@ -355,9 +387,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
eprintln!(" ❌ LLM error: {e}");
for ch in &channels {
if ch.name() == msg.channel {
let _ = ch
.send(&format!("⚠️ Error: {e}"), &msg.sender)
.await;
let _ = ch.send(&format!("⚠️ Error: {e}"), &msg.sender).await;
break;
}
}
@ -384,9 +414,17 @@ mod tests {
std::fs::write(tmp.path().join("SOUL.md"), "# Soul\nBe helpful.").unwrap();
std::fs::write(tmp.path().join("IDENTITY.md"), "# Identity\nName: ZeroClaw").unwrap();
std::fs::write(tmp.path().join("USER.md"), "# User\nName: Test User").unwrap();
std::fs::write(tmp.path().join("AGENTS.md"), "# Agents\nFollow instructions.").unwrap();
std::fs::write(
tmp.path().join("AGENTS.md"),
"# Agents\nFollow instructions.",
)
.unwrap();
std::fs::write(tmp.path().join("TOOLS.md"), "# Tools\nUse shell carefully.").unwrap();
std::fs::write(tmp.path().join("HEARTBEAT.md"), "# Heartbeat\nCheck status.").unwrap();
std::fs::write(
tmp.path().join("HEARTBEAT.md"),
"# Heartbeat\nCheck status.",
)
.unwrap();
std::fs::write(tmp.path().join("MEMORY.md"), "# Memory\nUser likes Rust.").unwrap();
tmp
}
@ -401,15 +439,24 @@ mod tests {
assert!(prompt.contains("## Tools"), "missing Tools section");
assert!(prompt.contains("## Safety"), "missing Safety section");
assert!(prompt.contains("## Workspace"), "missing Workspace section");
assert!(prompt.contains("## Project Context"), "missing Project Context");
assert!(prompt.contains("## Current Date & Time"), "missing Date/Time");
assert!(
prompt.contains("## Project Context"),
"missing Project Context"
);
assert!(
prompt.contains("## Current Date & Time"),
"missing Date/Time"
);
assert!(prompt.contains("## Runtime"), "missing Runtime section");
}
#[test]
fn prompt_injects_tools() {
let ws = make_workspace();
let tools = vec![("shell", "Run commands"), ("memory_recall", "Search memory")];
let tools = vec![
("shell", "Run commands"),
("memory_recall", "Search memory"),
];
let prompt = build_system_prompt(ws.path(), "gpt-4o", &tools, &[]);
assert!(prompt.contains("**shell**"));
@ -435,7 +482,10 @@ mod tests {
assert!(prompt.contains("### SOUL.md"), "missing SOUL.md header");
assert!(prompt.contains("Be helpful"), "missing SOUL content");
assert!(prompt.contains("### IDENTITY.md"), "missing IDENTITY.md");
assert!(prompt.contains("Name: ZeroClaw"), "missing IDENTITY content");
assert!(
prompt.contains("Name: ZeroClaw"),
"missing IDENTITY content"
);
assert!(prompt.contains("### USER.md"), "missing USER.md");
assert!(prompt.contains("### AGENTS.md"), "missing AGENTS.md");
assert!(prompt.contains("### TOOLS.md"), "missing TOOLS.md");
@ -460,12 +510,18 @@ mod tests {
let ws = make_workspace();
// No BOOTSTRAP.md — should not appear
let prompt = build_system_prompt(ws.path(), "model", &[], &[]);
assert!(!prompt.contains("### BOOTSTRAP.md"), "BOOTSTRAP.md should not appear when missing");
assert!(
!prompt.contains("### BOOTSTRAP.md"),
"BOOTSTRAP.md should not appear when missing"
);
// Create BOOTSTRAP.md — should appear
std::fs::write(ws.path().join("BOOTSTRAP.md"), "# Bootstrap\nFirst run.").unwrap();
let prompt2 = build_system_prompt(ws.path(), "model", &[], &[]);
assert!(prompt2.contains("### BOOTSTRAP.md"), "BOOTSTRAP.md should appear when present");
assert!(
prompt2.contains("### BOOTSTRAP.md"),
"BOOTSTRAP.md should appear when present"
);
assert!(prompt2.contains("First run"));
}
@ -475,13 +531,23 @@ mod tests {
let memory_dir = ws.path().join("memory");
std::fs::create_dir_all(&memory_dir).unwrap();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
std::fs::write(memory_dir.join(format!("{today}.md")), "# Daily\nSome note.").unwrap();
std::fs::write(
memory_dir.join(format!("{today}.md")),
"# Daily\nSome note.",
)
.unwrap();
let prompt = build_system_prompt(ws.path(), "model", &[], &[]);
// Daily notes should NOT be in the system prompt (on-demand via tools)
assert!(!prompt.contains("Daily Notes"), "daily notes should not be auto-injected");
assert!(!prompt.contains("Some note"), "daily content should not be in prompt");
assert!(
!prompt.contains("Daily Notes"),
"daily notes should not be auto-injected"
);
assert!(
!prompt.contains("Some note"),
"daily content should not be in prompt"
);
}
#[test]
@ -513,7 +579,10 @@ mod tests {
assert!(prompt.contains("<name>code-review</name>"));
assert!(prompt.contains("<description>Review code for bugs</description>"));
assert!(prompt.contains("SKILL.md</location>"));
assert!(prompt.contains("loaded on demand"), "should mention on-demand loading");
assert!(
prompt.contains("loaded on demand"),
"should mention on-demand loading"
);
// Full prompt content should NOT be dumped
assert!(!prompt.contains("Long prompt content that should NOT appear"));
}
@ -527,8 +596,14 @@ mod tests {
let prompt = build_system_prompt(ws.path(), "model", &[], &[]);
assert!(prompt.contains("truncated at"), "large files should be truncated");
assert!(!prompt.contains(&big_content), "full content should not appear");
assert!(
prompt.contains("truncated at"),
"large files should be truncated"
);
assert!(
!prompt.contains(&big_content),
"full content should not appear"
);
}
#[test]
@ -539,7 +614,10 @@ mod tests {
let prompt = build_system_prompt(ws.path(), "model", &[], &[]);
// Empty file should not produce a header
assert!(!prompt.contains("### TOOLS.md"), "empty files should be skipped");
assert!(
!prompt.contains("### TOOLS.md"),
"empty files should be skipped"
);
}
#[test]

View file

@ -84,10 +84,7 @@ impl Channel for SlackChannel {
loop {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let mut params = vec![
("channel", channel_id.clone()),
("limit", "10".to_string()),
];
let mut params = vec![("channel", channel_id.clone()), ("limit", "10".to_string())];
if !last_ts.is_empty() {
params.push(("oldest", last_ts.clone()));
}

View file

@ -102,7 +102,9 @@ impl Channel for TelegramChannel {
.unwrap_or("unknown");
if !self.is_user_allowed(username) {
tracing::warn!("Telegram: ignoring message from unauthorized user: {username}");
tracing::warn!(
"Telegram: ignoring message from unauthorized user: {username}"
);
continue;
}

View file

@ -570,10 +570,7 @@ default_temperature = 0.7
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
assert!(parsed.imessage.is_some());
assert!(parsed.matrix.is_some());
assert_eq!(
parsed.imessage.unwrap().allowed_contacts,
vec!["+1"]
);
assert_eq!(parsed.imessage.unwrap().allowed_contacts, vec!["+1"]);
assert_eq!(parsed.matrix.unwrap().homeserver, "https://m.org");
}

View file

@ -22,8 +22,10 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
.clone()
.unwrap_or_else(|| "anthropic/claude-sonnet-4-20250514".into());
let temperature = config.default_temperature;
let mem: Arc<dyn Memory> =
Arc::from(memory::create_memory(&config.memory, &config.workspace_dir)?);
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory(
&config.memory,
&config.workspace_dir,
)?);
// Extract webhook secret for authentication
let webhook_secret: Option<Arc<str>> = config
@ -39,7 +41,9 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
if webhook_secret.is_some() {
println!(" 🔒 Webhook authentication: ENABLED (X-Webhook-Secret header required)");
} else {
println!(" ⚠️ Webhook authentication: DISABLED (set [channels.webhook] secret to enable)");
println!(
" ⚠️ Webhook authentication: DISABLED (set [channels.webhook] secret to enable)"
);
}
println!(" Press Ctrl+C to stop.\n");
@ -64,7 +68,19 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
if let [method, path, ..] = parts.as_slice() {
tracing::info!("{peer} → {method} {path}");
handle_request(&mut stream, method, path, &request, &provider, &model, temperature, &mem, auto_save, secret.as_ref()).await;
handle_request(
&mut stream,
method,
path,
&request,
&provider,
&model,
temperature,
&mem,
auto_save,
secret.as_ref(),
)
.await;
} else {
let _ = send_response(&mut stream, 400, "Bad Request").await;
}
@ -116,14 +132,25 @@ async fn handle_request(
match header_val {
Some(val) if val == secret.as_ref() => {}
_ => {
tracing::warn!("Webhook: rejected request — invalid or missing X-Webhook-Secret");
tracing::warn!(
"Webhook: rejected request — invalid or missing X-Webhook-Secret"
);
let err = serde_json::json!({"error": "Unauthorized — invalid or missing X-Webhook-Secret header"});
let _ = send_json(stream, 401, &err).await;
return;
}
}
}
handle_webhook(stream, request, provider, model, temperature, mem, auto_save).await;
handle_webhook(
stream,
request,
provider,
model,
temperature,
mem,
auto_save,
)
.await;
}
_ => {
@ -206,7 +233,8 @@ mod tests {
#[test]
fn extract_header_finds_value() {
let req = "POST /webhook HTTP/1.1\r\nHost: localhost\r\nX-Webhook-Secret: my-secret\r\n\r\n{}";
let req =
"POST /webhook HTTP/1.1\r\nHost: localhost\r\nX-Webhook-Secret: my-secret\r\n\r\n{}";
assert_eq!(extract_header(req, "X-Webhook-Secret"), Some("my-secret"));
}
@ -244,13 +272,19 @@ mod tests {
fn extract_header_colon_in_value() {
let req = "POST /webhook HTTP/1.1\r\nAuthorization: Bearer sk-abc:123\r\n\r\n{}";
// split_once on ':' means only the first colon splits key/value
assert_eq!(extract_header(req, "Authorization"), Some("Bearer sk-abc:123"));
assert_eq!(
extract_header(req, "Authorization"),
Some("Bearer sk-abc:123")
);
}
#[test]
fn extract_header_different_header() {
let req = "POST /webhook HTTP/1.1\r\nContent-Type: application/json\r\nX-Webhook-Secret: mysecret\r\n\r\n{}";
assert_eq!(extract_header(req, "Content-Type"), Some("application/json"));
assert_eq!(
extract_header(req, "Content-Type"),
Some("application/json")
);
assert_eq!(extract_header(req, "X-Webhook-Secret"), Some("mysecret"));
}

View file

@ -130,7 +130,9 @@ fn list_integrations(config: &Config, filter_category: Option<&str>) -> Result<(
let total = available + active + coming;
println!();
println!(" {total} integrations: {active} active, {available} available, {coming} coming soon");
println!(
" {total} integrations: {active} active, {available} available, {coming} coming soon"
);
println!();
println!(" Configure: zeroclaw onboard");
println!(" Details: zeroclaw integrations info <name>");
@ -144,9 +146,7 @@ fn show_integration_info(config: &Config, name: &str) -> Result<()> {
let name_lower = name.to_lowercase();
let Some(entry) = entries.iter().find(|e| e.name.to_lowercase() == name_lower) else {
anyhow::bail!(
"Unknown integration: {name}. Run `zeroclaw integrations list` to see all."
);
anyhow::bail!("Unknown integration: {name}. Run `zeroclaw integrations list` to see all.");
};
let status = (entry.status_fn)(config);
@ -157,7 +157,12 @@ fn show_integration_info(config: &Config, name: &str) -> Result<()> {
};
println!();
println!(" {} {}{}", icon, console::style(entry.name).white().bold(), entry.description);
println!(
" {} {} — {}",
icon,
console::style(entry.name).white().bold(),
entry.description
);
println!(" Category: {}", entry.category.label());
println!(" Status: {label}");
println!();

View file

@ -161,7 +161,10 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
description: "Gemini 2.5 Pro/Flash",
category: IntegrationCategory::AiModel,
status_fn: |c| {
if c.default_model.as_deref().is_some_and(|m| m.starts_with("google/")) {
if c.default_model
.as_deref()
.is_some_and(|m| m.starts_with("google/"))
{
IntegrationStatus::Active
} else {
IntegrationStatus::Available
@ -173,7 +176,10 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
description: "DeepSeek V3 & R1",
category: IntegrationCategory::AiModel,
status_fn: |c| {
if c.default_model.as_deref().is_some_and(|m| m.starts_with("deepseek/")) {
if c.default_model
.as_deref()
.is_some_and(|m| m.starts_with("deepseek/"))
{
IntegrationStatus::Active
} else {
IntegrationStatus::Available
@ -185,7 +191,10 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
description: "Grok 3 & 4",
category: IntegrationCategory::AiModel,
status_fn: |c| {
if c.default_model.as_deref().is_some_and(|m| m.starts_with("x-ai/")) {
if c.default_model
.as_deref()
.is_some_and(|m| m.starts_with("x-ai/"))
{
IntegrationStatus::Active
} else {
IntegrationStatus::Available
@ -197,7 +206,10 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
description: "Mistral Large & Codestral",
category: IntegrationCategory::AiModel,
status_fn: |c| {
if c.default_model.as_deref().is_some_and(|m| m.starts_with("mistral")) {
if c.default_model
.as_deref()
.is_some_and(|m| m.starts_with("mistral"))
{
IntegrationStatus::Active
} else {
IntegrationStatus::Available
@ -655,15 +667,17 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::schema::{ChannelsConfig, IMessageConfig, MatrixConfig, TelegramConfig};
use crate::config::Config;
use crate::config::schema::{
ChannelsConfig, IMessageConfig, MatrixConfig, TelegramConfig,
};
#[test]
fn registry_has_entries() {
let entries = all_integrations();
assert!(entries.len() >= 50, "Expected 50+ integrations, got {}", entries.len());
assert!(
entries.len() >= 50,
"Expected 50+ integrations, got {}",
entries.len()
);
}
#[test]
@ -727,7 +741,10 @@ mod tests {
let config = Config::default();
let entries = all_integrations();
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();
assert!(matches!((tg.status_fn)(&config), IntegrationStatus::Available));
assert!(matches!(
(tg.status_fn)(&config),
IntegrationStatus::Available
));
}
#[test]
@ -746,7 +763,10 @@ mod tests {
let config = Config::default();
let entries = all_integrations();
let im = entries.iter().find(|e| e.name == "iMessage").unwrap();
assert!(matches!((im.status_fn)(&config), IntegrationStatus::Available));
assert!(matches!(
(im.status_fn)(&config),
IntegrationStatus::Available
));
}
#[test]
@ -768,7 +788,10 @@ mod tests {
let config = Config::default();
let entries = all_integrations();
let mx = entries.iter().find(|e| e.name == "Matrix").unwrap();
assert!(matches!((mx.status_fn)(&config), IntegrationStatus::Available));
assert!(matches!(
(mx.status_fn)(&config),
IntegrationStatus::Available
));
}
#[test]
@ -813,9 +836,21 @@ mod tests {
#[test]
fn category_counts_reasonable() {
let entries = all_integrations();
let chat_count = entries.iter().filter(|e| e.category == IntegrationCategory::Chat).count();
let ai_count = entries.iter().filter(|e| e.category == IntegrationCategory::AiModel).count();
assert!(chat_count >= 5, "Expected 5+ chat integrations, got {chat_count}");
assert!(ai_count >= 5, "Expected 5+ AI model integrations, got {ai_count}");
let chat_count = entries
.iter()
.filter(|e| e.category == IntegrationCategory::Chat)
.count();
let ai_count = entries
.iter()
.filter(|e| e.category == IntegrationCategory::AiModel)
.count();
assert!(
chat_count >= 5,
"Expected 5+ chat integrations, got {chat_count}"
);
assert!(
ai_count >= 5,
"Expected 5+ AI model integrations, got {ai_count}"
);
}
}

View file

@ -19,13 +19,13 @@ mod config;
mod cron;
mod gateway;
mod heartbeat;
mod integrations;
mod memory;
mod observability;
mod onboard;
mod providers;
mod runtime;
mod security;
mod integrations;
mod skills;
mod tools;
@ -298,7 +298,11 @@ async fn main() -> Result<()> {
] {
println!(
" {name:9} {}",
if configured { "✅ configured" } else { "❌ not configured" }
if configured {
"✅ configured"
} else {
"❌ not configured"
}
);
}
}

View file

@ -222,7 +222,10 @@ fn setup_provider() -> Result<(String, String, String)> {
let providers: Vec<(&str, &str)> = match tier_idx {
0 => vec![
("openrouter", "OpenRouter — 200+ models, 1 API key (recommended)"),
(
"openrouter",
"OpenRouter — 200+ models, 1 API key (recommended)",
),
("venice", "Venice AI — privacy-first (Llama, Opus)"),
("anthropic", "Anthropic — Claude Sonnet & Opus (direct)"),
("openai", "OpenAI — GPT-4o, o1, GPT-5 (direct)"),
@ -251,9 +254,7 @@ fn setup_provider() -> Result<(String, String, String)> {
("opencode", "OpenCode Zen — code-focused AI"),
("cohere", "Cohere — Command R+ & embeddings"),
],
_ => vec![
("ollama", "Ollama — local models (Llama, Mistral, Phi)"),
],
_ => vec![("ollama", "Ollama — local models (Llama, Mistral, Phi)")],
};
let provider_labels: Vec<&str> = providers.iter().map(|(_, label)| *label).collect();
@ -321,18 +322,36 @@ fn setup_provider() -> Result<(String, String, String)> {
// ── Model selection ──
let models: Vec<(&str, &str)> = match provider_name {
"openrouter" => vec![
("anthropic/claude-sonnet-4-20250514", "Claude Sonnet 4 (balanced, recommended)"),
("anthropic/claude-3.5-sonnet", "Claude 3.5 Sonnet (fast, affordable)"),
(
"anthropic/claude-sonnet-4-20250514",
"Claude Sonnet 4 (balanced, recommended)",
),
(
"anthropic/claude-3.5-sonnet",
"Claude 3.5 Sonnet (fast, affordable)",
),
("openai/gpt-4o", "GPT-4o (OpenAI flagship)"),
("openai/gpt-4o-mini", "GPT-4o Mini (fast, cheap)"),
("google/gemini-2.0-flash-001", "Gemini 2.0 Flash (Google, fast)"),
("meta-llama/llama-3.3-70b-instruct", "Llama 3.3 70B (open source)"),
(
"google/gemini-2.0-flash-001",
"Gemini 2.0 Flash (Google, fast)",
),
(
"meta-llama/llama-3.3-70b-instruct",
"Llama 3.3 70B (open source)",
),
("deepseek/deepseek-chat", "DeepSeek Chat (affordable)"),
],
"anthropic" => vec![
("claude-sonnet-4-20250514", "Claude Sonnet 4 (balanced, recommended)"),
(
"claude-sonnet-4-20250514",
"Claude Sonnet 4 (balanced, recommended)",
),
("claude-3-5-sonnet-20241022", "Claude 3.5 Sonnet (fast)"),
("claude-3-5-haiku-20241022", "Claude 3.5 Haiku (fastest, cheapest)"),
(
"claude-3-5-haiku-20241022",
"Claude 3.5 Haiku (fastest, cheapest)",
),
],
"openai" => vec![
("gpt-4o", "GPT-4o (flagship)"),
@ -345,7 +364,10 @@ fn setup_provider() -> Result<(String, String, String)> {
("llama-3.1-405b", "Llama 3.1 405B (largest open source)"),
],
"groq" => vec![
("llama-3.3-70b-versatile", "Llama 3.3 70B (fast, recommended)"),
(
"llama-3.3-70b-versatile",
"Llama 3.3 70B (fast, recommended)",
),
("llama-3.1-8b-instant", "Llama 3.1 8B (instant)"),
("mixtral-8x7b-32768", "Mixtral 8x7B (32K context)"),
],
@ -367,12 +389,24 @@ fn setup_provider() -> Result<(String, String, String)> {
("sonar", "Sonar (search, fast)"),
],
"fireworks" => vec![
("accounts/fireworks/models/llama-v3p3-70b-instruct", "Llama 3.3 70B"),
("accounts/fireworks/models/mixtral-8x22b-instruct", "Mixtral 8x22B"),
(
"accounts/fireworks/models/llama-v3p3-70b-instruct",
"Llama 3.3 70B",
),
(
"accounts/fireworks/models/mixtral-8x22b-instruct",
"Mixtral 8x22B",
),
],
"together" => vec![
("meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", "Llama 3.1 70B Turbo"),
("meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", "Llama 3.1 8B Turbo"),
(
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
"Llama 3.1 70B Turbo",
),
(
"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
"Llama 3.1 8B Turbo",
),
("mistralai/Mixtral-8x22B-Instruct-v0.1", "Mixtral 8x22B"),
],
"cohere" => vec![
@ -397,9 +431,7 @@ fn setup_provider() -> Result<(String, String, String)> {
("codellama", "Code Llama"),
("phi3", "Phi-3 (small, fast)"),
],
_ => vec![
("default", "Default model"),
],
_ => vec![("default", "Default model")],
};
let model_labels: Vec<&str> = models.iter().map(|(_, label)| *label).collect();
@ -518,7 +550,9 @@ fn setup_project_context() -> Result<ProjectContext> {
0 => "Be direct and concise. Skip pleasantries. Get to the point.".to_string(),
1 => "Be friendly and casual. Warm but efficient.".to_string(),
2 => "Be technical and detailed. Thorough explanations, code-first.".to_string(),
_ => "Adapt to the situation. Be concise when needed, thorough when it matters.".to_string(),
_ => {
"Adapt to the situation. Be concise when needed, thorough when it matters.".to_string()
}
};
println!(
@ -560,27 +594,51 @@ fn setup_channels() -> Result<ChannelsConfig> {
let options = vec![
format!(
"Telegram {}",
if config.telegram.is_some() { "✅ connected" } else { "— connect your bot" }
if config.telegram.is_some() {
"✅ connected"
} else {
"— connect your bot"
}
),
format!(
"Discord {}",
if config.discord.is_some() { "✅ connected" } else { "— connect your bot" }
if config.discord.is_some() {
"✅ connected"
} else {
"— connect your bot"
}
),
format!(
"Slack {}",
if config.slack.is_some() { "✅ connected" } else { "— connect your bot" }
if config.slack.is_some() {
"✅ connected"
} else {
"— connect your bot"
}
),
format!(
"iMessage {}",
if config.imessage.is_some() { "✅ configured" } else { "— macOS only" }
if config.imessage.is_some() {
"✅ configured"
} else {
"— macOS only"
}
),
format!(
"Matrix {}",
if config.matrix.is_some() { "✅ connected" } else { "— self-hosted chat" }
if config.matrix.is_some() {
"✅ connected"
} else {
"— self-hosted chat"
}
),
format!(
"Webhook {}",
if config.webhook.is_some() { "✅ configured" } else { "— HTTP endpoint" }
if config.webhook.is_some() {
"✅ configured"
} else {
"— HTTP endpoint"
}
),
"Done — finish setup".to_string(),
];
@ -670,9 +728,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
print_bullet("4. Invite bot to your server with messages permission");
println!();
let token: String = Input::new()
.with_prompt(" Bot token")
.interact_text()?;
let token: String = Input::new().with_prompt(" Bot token").interact_text()?;
if token.trim().is_empty() {
println!(" {} Skipped", style("").dim());
@ -750,7 +806,10 @@ fn setup_channels() -> Result<ChannelsConfig> {
{
Ok(resp) if resp.status().is_success() => {
let data: serde_json::Value = resp.json().unwrap_or_default();
let ok = data.get("ok").and_then(serde_json::Value::as_bool).unwrap_or(false);
let ok = data
.get("ok")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let team = data
.get("team")
.and_then(serde_json::Value::as_str)
@ -761,11 +820,11 @@ fn setup_channels() -> Result<ChannelsConfig> {
style("").green().bold()
);
} else {
let err = data.get("error").and_then(serde_json::Value::as_str).unwrap_or("unknown error");
println!(
"\r {} Slack error: {err}",
style("").red().bold()
);
let err = data
.get("error")
.and_then(serde_json::Value::as_str)
.unwrap_or("unknown error");
println!("\r {} Slack error: {err}", style("").red().bold());
continue;
}
}
@ -790,8 +849,16 @@ fn setup_channels() -> Result<ChannelsConfig> {
config.slack = Some(SlackConfig {
bot_token: token,
app_token: if app_token.is_empty() { None } else { Some(app_token) },
channel_id: if channel.is_empty() { None } else { Some(channel) },
app_token: if app_token.is_empty() {
None
} else {
Some(app_token)
},
channel_id: if channel.is_empty() {
None
} else {
Some(channel)
},
allowed_users: vec![],
});
}
@ -813,7 +880,9 @@ fn setup_channels() -> Result<ChannelsConfig> {
}
print_bullet("ZeroClaw reads your iMessage database and replies via AppleScript.");
print_bullet("You need to grant Full Disk Access to your terminal in System Settings.");
print_bullet(
"You need to grant Full Disk Access to your terminal in System Settings.",
);
println!();
let contacts_str: String = Input::new()
@ -824,7 +893,10 @@ fn setup_channels() -> Result<ChannelsConfig> {
let allowed_contacts = if contacts_str.trim() == "*" {
vec!["*".into()]
} else {
contacts_str.split(',').map(|s| s.trim().to_string()).collect()
contacts_str
.split(',')
.map(|s| s.trim().to_string())
.collect()
};
config.imessage = Some(IMessageConfig { allowed_contacts });
@ -855,9 +927,8 @@ fn setup_channels() -> Result<ChannelsConfig> {
continue;
}
let access_token: String = Input::new()
.with_prompt(" Access token")
.interact_text()?;
let access_token: String =
Input::new().with_prompt(" Access token").interact_text()?;
if access_token.trim().is_empty() {
println!(" {} Skipped — token required", style("").dim());
@ -936,7 +1007,11 @@ fn setup_channels() -> Result<ChannelsConfig> {
config.webhook = Some(WebhookConfig {
port: port.parse().unwrap_or(8080),
secret: if secret.is_empty() { None } else { Some(secret) },
secret: if secret.is_empty() {
None
} else {
Some(secret)
},
});
println!(
" {} Webhook on port {}",
@ -1330,9 +1405,7 @@ fn print_summary(config: &Config) {
let mut step = 1u8;
if config.api_key.is_none() {
let env_var = provider_env_var(
config.default_provider.as_deref().unwrap_or("openrouter"),
);
let env_var = provider_env_var(config.default_provider.as_deref().unwrap_or("openrouter"));
println!(
" {} Set your API key:",
style(format!("{step}.")).cyan().bold()
@ -1352,10 +1425,7 @@ fn print_summary(config: &Config) {
style(format!("{step}.")).cyan().bold(),
style("Launch your channels").white().bold()
);
println!(
" {}",
style("zeroclaw channel start").yellow()
);
println!(" {}", style("zeroclaw channel start").yellow());
println!();
step += 1;
}
@ -1440,10 +1510,7 @@ mod tests {
scaffold_workspace(tmp.path(), &ctx).unwrap();
for dir in &["sessions", "memory", "state", "cron", "skills"] {
assert!(
tmp.path().join(dir).is_dir(),
"missing subdirectory: {dir}"
);
assert!(tmp.path().join(dir).is_dir(), "missing subdirectory: {dir}");
}
}
@ -1459,7 +1526,10 @@ mod tests {
scaffold_workspace(tmp.path(), &ctx).unwrap();
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap();
assert!(user_md.contains("**Name:** Alice"), "USER.md should contain user name");
assert!(
user_md.contains("**Name:** Alice"),
"USER.md should contain user name"
);
let bootstrap = fs::read_to_string(tmp.path().join("BOOTSTRAP.md")).unwrap();
assert!(

View file

@ -53,9 +53,7 @@ impl Provider for AnthropicProvider {
temperature: f64,
) -> anyhow::Result<String> {
let api_key = self.api_key.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"Anthropic API key not set. Set ANTHROPIC_API_KEY or edit config.toml."
)
anyhow::anyhow!("Anthropic API key not set. Set ANTHROPIC_API_KEY or edit config.toml.")
})?;
let request = ChatRequest {
@ -122,10 +120,15 @@ mod tests {
#[tokio::test]
async fn chat_fails_without_key() {
let p = AnthropicProvider::new(None);
let result = p.chat_with_system(None, "hello", "claude-3-opus", 0.7).await;
let result = p
.chat_with_system(None, "hello", "claude-3-opus", 0.7)
.await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("API key not set"), "Expected key error, got: {err}");
assert!(
err.contains("API key not set"),
"Expected key error, got: {err}"
);
}
#[tokio::test]
@ -150,7 +153,10 @@ mod tests {
temperature: 0.7,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("system"), "system field should be skipped when None");
assert!(
!json.contains("system"),
"system field should be skipped when None"
);
assert!(json.contains("claude-3-opus"));
assert!(json.contains("hello"));
}
@ -188,7 +194,8 @@ mod tests {
#[test]
fn chat_response_multiple_blocks() {
let json = r#"{"content":[{"type":"text","text":"First"},{"type":"text","text":"Second"}]}"#;
let json =
r#"{"content":[{"type":"text","text":"First"},{"type":"text","text":"Second"}]}"#;
let resp: ChatResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.content.len(), 2);
assert_eq!(resp.content[0].text, "First");

View file

@ -170,9 +170,14 @@ mod tests {
#[tokio::test]
async fn chat_fails_without_key() {
let p = make_provider("Venice", "https://api.venice.ai", None);
let result = p.chat_with_system(None, "hello", "llama-3.3-70b", 0.7).await;
let result = p
.chat_with_system(None, "hello", "llama-3.3-70b", 0.7)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Venice API key not set"));
assert!(result
.unwrap_err()
.to_string()
.contains("Venice API key not set"));
}
#[test]
@ -180,8 +185,14 @@ mod tests {
let req = ChatRequest {
model: "llama-3.3-70b".to_string(),
messages: vec![
Message { role: "system".to_string(), content: "You are ZeroClaw".to_string() },
Message { role: "user".to_string(), content: "hello".to_string() },
Message {
role: "system".to_string(),
content: "You are ZeroClaw".to_string(),
},
Message {
role: "user".to_string(),
content: "hello".to_string(),
},
],
temperature: 0.7,
};
@ -208,7 +219,10 @@ mod tests {
#[test]
fn x_api_key_auth_style() {
let p = OpenAiCompatibleProvider::new(
"moonshot", "https://api.moonshot.cn", Some("ms-key"), AuthStyle::XApiKey,
"moonshot",
"https://api.moonshot.cn",
Some("ms-key"),
AuthStyle::XApiKey,
);
assert!(matches!(p.auth_header, AuthStyle::XApiKey));
}
@ -216,7 +230,10 @@ mod tests {
#[test]
fn custom_auth_style() {
let p = OpenAiCompatibleProvider::new(
"custom", "https://api.example.com", Some("key"), AuthStyle::Custom("X-Custom-Key".into()),
"custom",
"https://api.example.com",
Some("key"),
AuthStyle::Custom("X-Custom-Key".into()),
);
assert!(matches!(p.auth_header, AuthStyle::Custom(_)));
}
@ -238,7 +255,8 @@ mod tests {
assert!(result.is_err(), "{} should fail without key", p.name);
assert!(
result.unwrap_err().to_string().contains("API key not set"),
"{} error should mention key", p.name
"{} error should mention key",
p.name
);
}
}

View file

@ -250,11 +250,29 @@ mod tests {
#[test]
fn factory_all_providers_create_successfully() {
let providers = [
"openrouter", "anthropic", "openai", "ollama",
"venice", "vercel", "cloudflare", "moonshot", "synthetic",
"opencode", "zai", "glm", "minimax", "bedrock", "qianfan",
"groq", "mistral", "xai", "deepseek", "together",
"fireworks", "perplexity", "cohere",
"openrouter",
"anthropic",
"openai",
"ollama",
"venice",
"vercel",
"cloudflare",
"moonshot",
"synthetic",
"opencode",
"zai",
"glm",
"minimax",
"bedrock",
"qianfan",
"groq",
"mistral",
"xai",
"deepseek",
"together",
"fireworks",
"perplexity",
"cohere",
];
for name in providers {
assert!(

View file

@ -85,7 +85,9 @@ impl Provider for OllamaProvider {
if !response.status().is_success() {
let error = response.text().await?;
anyhow::bail!("Ollama error: {error}. Is Ollama running? (brew install ollama && ollama serve)");
anyhow::bail!(
"Ollama error: {error}. Is Ollama running? (brew install ollama && ollama serve)"
);
}
let chat_response: ChatResponse = response.json().await?;
@ -126,8 +128,14 @@ mod tests {
let req = ChatRequest {
model: "llama3".to_string(),
messages: vec![
Message { role: "system".to_string(), content: "You are ZeroClaw".to_string() },
Message { role: "user".to_string(), content: "hello".to_string() },
Message {
role: "system".to_string(),
content: "You are ZeroClaw".to_string(),
},
Message {
role: "user".to_string(),
content: "hello".to_string(),
},
],
stream: false,
options: Options { temperature: 0.7 },
@ -143,9 +151,10 @@ mod tests {
fn request_serializes_without_system() {
let req = ChatRequest {
model: "mistral".to_string(),
messages: vec![
Message { role: "user".to_string(), content: "test".to_string() },
],
messages: vec![Message {
role: "user".to_string(),
content: "test".to_string(),
}],
stream: false,
options: Options { temperature: 0.0 },
};

View file

@ -146,8 +146,14 @@ mod tests {
let req = ChatRequest {
model: "gpt-4o".to_string(),
messages: vec![
Message { role: "system".to_string(), content: "You are ZeroClaw".to_string() },
Message { role: "user".to_string(), content: "hello".to_string() },
Message {
role: "system".to_string(),
content: "You are ZeroClaw".to_string(),
},
Message {
role: "user".to_string(),
content: "hello".to_string(),
},
],
temperature: 0.7,
};
@ -161,9 +167,10 @@ mod tests {
fn request_serializes_without_system() {
let req = ChatRequest {
model: "gpt-4o".to_string(),
messages: vec![
Message { role: "user".to_string(), content: "hello".to_string() },
],
messages: vec![Message {
role: "user".to_string(),
content: "hello".to_string(),
}],
temperature: 0.0,
};
let json = serde_json::to_string(&req).unwrap();

View file

@ -2,12 +2,7 @@ use async_trait::async_trait;
#[async_trait]
pub trait Provider: Send + Sync {
async fn chat(
&self,
message: &str,
model: &str,
temperature: f64,
) -> anyhow::Result<String> {
async fn chat(&self, message: &str, model: &str, temperature: f64) -> anyhow::Result<String> {
self.chat_with_system(None, message, model, temperature)
.await
}

View file

@ -37,8 +37,13 @@ impl ActionTracker {
/// Record an action and return the current count within the window.
pub fn record(&self) -> usize {
let mut actions = self.actions.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let cutoff = Instant::now().checked_sub(std::time::Duration::from_secs(3600)).unwrap_or_else(Instant::now);
let mut actions = self
.actions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let cutoff = Instant::now()
.checked_sub(std::time::Duration::from_secs(3600))
.unwrap_or_else(Instant::now);
actions.retain(|t| *t > cutoff);
actions.push(Instant::now());
actions.len()
@ -46,8 +51,13 @@ impl ActionTracker {
/// Count of actions in the current window without recording.
pub fn count(&self) -> usize {
let mut actions = self.actions.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let cutoff = Instant::now().checked_sub(std::time::Duration::from_secs(3600)).unwrap_or_else(Instant::now);
let mut actions = self
.actions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let cutoff = Instant::now()
.checked_sub(std::time::Duration::from_secs(3600))
.unwrap_or_else(Instant::now);
actions.retain(|t| *t > cutoff);
actions.len()
}
@ -55,7 +65,10 @@ impl ActionTracker {
impl Clone for ActionTracker {
fn clone(&self) -> Self {
let actions = self.actions.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let actions = self
.actions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Self {
actions: Mutex::new(actions.clone()),
}
@ -582,7 +595,7 @@ mod tests {
max_actions_per_hour: 1,
..SecurityPolicy::default()
};
assert!(p.record_action()); // 1 — exactly at limit
assert!(p.record_action()); // 1 — exactly at limit
assert!(!p.record_action()); // 2 — over
assert!(!p.record_action()); // 3 — still over
}

View file

@ -158,7 +158,11 @@ pub fn skills_to_prompt(skills: &[Skill]) -> String {
if !skill.tools.is_empty() {
prompt.push_str("Tools:\n");
for tool in &skill.tools {
let _ = writeln!(prompt, "- **{}**: {} ({})", tool.name, tool.description, tool.kind);
let _ = writeln!(
prompt,
"- **{}**: {} ({})",
tool.name, tool.description, tool.kind
);
}
}
@ -242,14 +246,16 @@ pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Re
if !skill.tools.is_empty() {
println!(
" Tools: {}",
skill.tools.iter().map(|t| t.name.as_str()).collect::<Vec<_>>().join(", ")
skill
.tools
.iter()
.map(|t| t.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
if !skill.tags.is_empty() {
println!(
" Tags: {}",
skill.tags.join(", ")
);
println!(" Tags: {}", skill.tags.join(", "));
}
}
}
@ -270,7 +276,10 @@ pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Re
.output()?;
if output.status.success() {
println!(" {} Skill installed successfully!", console::style("").green().bold());
println!(
" {} Skill installed successfully!",
console::style("").green().bold()
);
println!(" Restart `zeroclaw channel start` to activate.");
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
@ -293,7 +302,11 @@ pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Re
anyhow::bail!("Symlink not supported on this platform. Copy the skill directory manually.");
}
println!(" {} Skill linked: {}", console::style("").green().bold(), dest.display());
println!(
" {} Skill linked: {}",
console::style("").green().bold(),
dest.display()
);
}
Ok(())
@ -305,7 +318,11 @@ pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Re
}
std::fs::remove_dir_all(&skill_path)?;
println!(" {} Skill '{}' removed.", console::style("").green().bold(), name);
println!(
" {} Skill '{}' removed.",
console::style("").green().bold(),
name
);
Ok(())
}
}

View file

@ -64,7 +64,9 @@ impl Tool for MemoryRecallTool {
Ok(entries) => {
let mut output = format!("Found {} memories:\n", entries.len());
for entry in &entries {
let score = entry.score.map_or_else(String::new, |s| format!(" [{s:.0}%]"));
let score = entry
.score
.map_or_else(String::new, |s| format!(" [{s:.0}%]"));
let _ = writeln!(
output,
"- [{}] {}: {}{score}",
@ -102,10 +104,7 @@ mod tests {
async fn recall_empty() {
let (_tmp, mem) = seeded_mem();
let tool = MemoryRecallTool::new(mem);
let result = tool
.execute(json!({"query": "anything"}))
.await
.unwrap();
let result = tool.execute(json!({"query": "anything"})).await.unwrap();
assert!(result.success);
assert!(result.output.contains("No memories found"));
}
@ -131,9 +130,13 @@ mod tests {
async fn recall_respects_limit() {
let (_tmp, mem) = seeded_mem();
for i in 0..10 {
mem.store(&format!("k{i}"), &format!("Rust fact {i}"), MemoryCategory::Core)
.await
.unwrap();
mem.store(
&format!("k{i}"),
&format!("Rust fact {i}"),
MemoryCategory::Core,
)
.await
.unwrap();
}
let tool = MemoryRecallTool::new(mem);

View file

@ -32,10 +32,7 @@ pub fn default_tools(security: Arc<SecurityPolicy>) -> Vec<Box<dyn Tool>> {
}
/// Create full tool registry including memory tools
pub fn all_tools(
security: Arc<SecurityPolicy>,
memory: Arc<dyn Memory>,
) -> Vec<Box<dyn Tool>> {
pub fn all_tools(security: Arc<SecurityPolicy>, memory: Arc<dyn Memory>) -> Vec<Box<dyn Tool>> {
vec![
Box::new(ShellTool::new(security.clone())),
Box::new(FileReadTool::new(security.clone())),
@ -51,8 +48,10 @@ pub async fn handle_command(command: super::ToolCommands, config: Config) -> Res
workspace_dir: config.workspace_dir.clone(),
..SecurityPolicy::default()
});
let mem: Arc<dyn Memory> =
Arc::from(crate::memory::create_memory(&config.memory, &config.workspace_dir)?);
let mem: Arc<dyn Memory> = Arc::from(crate::memory::create_memory(
&config.memory,
&config.workspace_dir,
)?);
let tools_list = all_tools(security, mem);
match command {