* test: deepen coverage for health doctor provider and tunnels * test: add broad trait and module re-export coverage
227 lines
7.3 KiB
Rust
227 lines
7.3 KiB
Rust
pub mod registry;
|
|
|
|
use crate::config::Config;
|
|
use anyhow::Result;
|
|
|
|
/// Integration status
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum IntegrationStatus {
|
|
/// Fully implemented and ready to use
|
|
Available,
|
|
/// Configured and active
|
|
Active,
|
|
/// Planned but not yet implemented
|
|
ComingSoon,
|
|
}
|
|
|
|
/// Integration category
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum IntegrationCategory {
|
|
Chat,
|
|
AiModel,
|
|
Productivity,
|
|
MusicAudio,
|
|
SmartHome,
|
|
ToolsAutomation,
|
|
MediaCreative,
|
|
Social,
|
|
Platform,
|
|
}
|
|
|
|
impl IntegrationCategory {
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Chat => "Chat Providers",
|
|
Self::AiModel => "AI Models",
|
|
Self::Productivity => "Productivity",
|
|
Self::MusicAudio => "Music & Audio",
|
|
Self::SmartHome => "Smart Home",
|
|
Self::ToolsAutomation => "Tools & Automation",
|
|
Self::MediaCreative => "Media & Creative",
|
|
Self::Social => "Social",
|
|
Self::Platform => "Platforms",
|
|
}
|
|
}
|
|
|
|
pub fn all() -> &'static [Self] {
|
|
&[
|
|
Self::Chat,
|
|
Self::AiModel,
|
|
Self::Productivity,
|
|
Self::MusicAudio,
|
|
Self::SmartHome,
|
|
Self::ToolsAutomation,
|
|
Self::MediaCreative,
|
|
Self::Social,
|
|
Self::Platform,
|
|
]
|
|
}
|
|
}
|
|
|
|
/// A registered integration
|
|
pub struct IntegrationEntry {
|
|
pub name: &'static str,
|
|
pub description: &'static str,
|
|
pub category: IntegrationCategory,
|
|
pub status_fn: fn(&Config) -> IntegrationStatus,
|
|
}
|
|
|
|
/// Handle the `integrations` CLI command
|
|
pub fn handle_command(command: crate::IntegrationCommands, config: &Config) -> Result<()> {
|
|
match command {
|
|
crate::IntegrationCommands::Info { name } => show_integration_info(config, &name),
|
|
}
|
|
}
|
|
|
|
fn show_integration_info(config: &Config, name: &str) -> Result<()> {
|
|
let entries = registry::all_integrations();
|
|
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}. Check README for supported integrations or run `zeroclaw onboard --interactive` to configure channels/providers."
|
|
);
|
|
};
|
|
|
|
let status = (entry.status_fn)(config);
|
|
let (icon, label) = match status {
|
|
IntegrationStatus::Active => ("✅", "Active"),
|
|
IntegrationStatus::Available => ("⚪", "Available"),
|
|
IntegrationStatus::ComingSoon => ("🔜", "Coming Soon"),
|
|
};
|
|
|
|
println!();
|
|
println!(
|
|
" {} {} — {}",
|
|
icon,
|
|
console::style(entry.name).white().bold(),
|
|
entry.description
|
|
);
|
|
println!(" Category: {}", entry.category.label());
|
|
println!(" Status: {label}");
|
|
println!();
|
|
|
|
// Show setup hints based on integration
|
|
match entry.name {
|
|
"Telegram" => {
|
|
println!(" Setup:");
|
|
println!(" 1. Message @BotFather on Telegram");
|
|
println!(" 2. Create a bot and copy the token");
|
|
println!(" 3. Run: zeroclaw onboard");
|
|
println!(" 4. Start: zeroclaw channel start");
|
|
}
|
|
"Discord" => {
|
|
println!(" Setup:");
|
|
println!(" 1. Go to https://discord.com/developers/applications");
|
|
println!(" 2. Create app → Bot → Copy token");
|
|
println!(" 3. Enable MESSAGE CONTENT intent");
|
|
println!(" 4. Run: zeroclaw onboard");
|
|
}
|
|
"Slack" => {
|
|
println!(" Setup:");
|
|
println!(" 1. Go to https://api.slack.com/apps");
|
|
println!(" 2. Create app → Bot Token Scopes → Install");
|
|
println!(" 3. Run: zeroclaw onboard");
|
|
}
|
|
"OpenRouter" => {
|
|
println!(" Setup:");
|
|
println!(" 1. Get API key at https://openrouter.ai/keys");
|
|
println!(" 2. Run: zeroclaw onboard");
|
|
println!(" Access 200+ models with one key.");
|
|
}
|
|
"Ollama" => {
|
|
println!(" Setup:");
|
|
println!(" 1. Install: brew install ollama");
|
|
println!(" 2. Pull a model: ollama pull llama3");
|
|
println!(" 3. Set provider to 'ollama' in config.toml");
|
|
}
|
|
"iMessage" => {
|
|
println!(" Setup (macOS only):");
|
|
println!(" Uses AppleScript bridge to send/receive iMessages.");
|
|
println!(" Requires Full Disk Access in System Settings → Privacy.");
|
|
}
|
|
"GitHub" => {
|
|
println!(" Setup:");
|
|
println!(" 1. Create a personal access token at https://github.com/settings/tokens");
|
|
println!(" 2. Add to config: [integrations.github] token = \"ghp_...\"");
|
|
}
|
|
"Browser" => {
|
|
println!(" Built-in:");
|
|
println!(" ZeroClaw can control Chrome/Chromium for web tasks.");
|
|
println!(" Uses headless browser automation.");
|
|
}
|
|
"Cron" => {
|
|
println!(" Built-in:");
|
|
println!(" Schedule tasks in ~/.zeroclaw/workspace/cron/");
|
|
println!(" Run: zeroclaw cron list");
|
|
}
|
|
"Webhooks" => {
|
|
println!(" Built-in:");
|
|
println!(" HTTP endpoint for external triggers.");
|
|
println!(" Run: zeroclaw gateway");
|
|
}
|
|
_ => {
|
|
if status == IntegrationStatus::ComingSoon {
|
|
println!(" This integration is planned. Stay tuned!");
|
|
println!(" Track progress: https://github.com/theonlyhennygod/zeroclaw");
|
|
}
|
|
}
|
|
}
|
|
|
|
println!();
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn integration_category_all_includes_every_variant_once() {
|
|
let all = IntegrationCategory::all();
|
|
assert_eq!(all.len(), 9);
|
|
|
|
let labels: Vec<&str> = all.iter().map(|cat| cat.label()).collect();
|
|
assert!(labels.contains(&"Chat Providers"));
|
|
assert!(labels.contains(&"AI Models"));
|
|
assert!(labels.contains(&"Productivity"));
|
|
assert!(labels.contains(&"Music & Audio"));
|
|
assert!(labels.contains(&"Smart Home"));
|
|
assert!(labels.contains(&"Tools & Automation"));
|
|
assert!(labels.contains(&"Media & Creative"));
|
|
assert!(labels.contains(&"Social"));
|
|
assert!(labels.contains(&"Platforms"));
|
|
}
|
|
|
|
#[test]
|
|
fn handle_command_info_is_case_insensitive_for_known_integrations() {
|
|
let config = Config::default();
|
|
let first_name = registry::all_integrations()
|
|
.first()
|
|
.expect("registry should define at least one integration")
|
|
.name
|
|
.to_lowercase();
|
|
|
|
let result = handle_command(
|
|
crate::IntegrationCommands::Info { name: first_name },
|
|
&config,
|
|
);
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn handle_command_info_returns_error_for_unknown_integration() {
|
|
let config = Config::default();
|
|
let result = handle_command(
|
|
crate::IntegrationCommands::Info {
|
|
name: "definitely-not-a-real-integration".into(),
|
|
},
|
|
&config,
|
|
);
|
|
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(err.contains("Unknown integration"));
|
|
}
|
|
}
|