use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub mod analyzer;
pub mod browser;
pub mod cargo;
pub mod computer;
pub mod container;
pub mod file;
pub mod fim;
pub mod git;
#[cfg(feature = "hot-reload")]
pub mod hot_reload;
pub mod http;
pub mod knowledge;
pub mod lsp_tools;
pub mod package;
pub mod page_controller;
pub mod process;
pub mod pty_shell;
pub mod screen_capture;
pub mod search;
pub mod shell;
pub mod swarm_tool;
pub mod vision;
use browser::{BrowserEval, BrowserFetch, BrowserLinks, BrowserPdf, BrowserScreenshot};
use cargo::{CargoCheck, CargoClippy, CargoFmt, CargoTest};
use container::{
ComposeDown, ComposeUp, ContainerBuild, ContainerExec, ContainerImages, ContainerList,
ContainerLogs, ContainerPull, ContainerRemove, ContainerRun, ContainerStop,
};
use file::{DirectoryTree, FileDelete, FileEdit, FileRead, FileWrite};
use git::{GitCheckpoint, GitCommit, GitDiff, GitPush, GitStatus};
use http::HttpRequest;
use knowledge::{
KnowledgeAdd, KnowledgeClear, KnowledgeExport, KnowledgeQuery, KnowledgeRelate,
KnowledgeRemove, KnowledgeStats as KnowledgeStatsTool,
};
use package::{NpmInstall, NpmRun, NpmScripts, PipFreeze, PipInstall, PipList, YarnInstall};
use page_controller::PageControlTool;
use process::{PortCheck, ProcessList, ProcessLogs, ProcessRestart, ProcessStart, ProcessStop};
use pty_shell::PtyShellTool;
use screen_capture::ScreenCapture;
use search::{GlobFind, GrepSearch, SymbolSearch};
use shell::ShellExec;
use vision::{VisionAnalyze, VisionCompare};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationInfo {
pub offset: usize,
pub limit: usize,
pub total_chars: usize,
pub has_more: bool,
}
pub fn truncate_with_pagination(
output: &str,
offset: usize,
limit: usize,
) -> (String, PaginationInfo) {
let total_chars = output.chars().count();
let page: String = output.chars().skip(offset).take(limit).collect();
let consumed = offset + page.chars().count();
let info = PaginationInfo {
offset,
limit,
total_chars,
has_more: consumed < total_chars,
};
(page, info)
}
pub fn validate_tool_arguments_schema(tool_name: &str, schema: &Value, args: &Value) -> Result<()> {
if schema.get("type").and_then(|v| v.as_str()) == Some("object") && !args.is_object() {
anyhow::bail!(
"Schema validation failed for tool '{}': expected JSON object arguments",
tool_name
);
}
let Some(required) = schema.get("required").and_then(|v| v.as_array()) else {
return Ok(());
};
let Some(args_obj) = args.as_object() else {
return Ok(());
};
let missing: Vec<&str> = required
.iter()
.filter_map(|value| value.as_str())
.filter(|field| args_obj.get(*field).is_none_or(|value| value.is_null()))
.collect();
if missing.is_empty() {
Ok(())
} else {
anyhow::bail!(
"Schema validation failed for tool '{}': missing required field(s): {}",
tool_name,
missing.join(", ")
);
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn schema(&self) -> Value;
async fn execute(&self, args: Value) -> Result<Value>;
}
pub struct ToolRegistry {
tools: HashMap<String, Box<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
let mut registry = Self {
tools: HashMap::new(),
};
registry.register(FileRead::new());
registry.register(FileWrite::new());
registry.register(FileEdit::new());
registry.register(FileDelete::new());
registry.register(DirectoryTree::new());
registry.register(GitStatus);
registry.register(GitDiff);
registry.register(GitCommit);
registry.register(GitPush);
registry.register(GitCheckpoint);
registry.register(CargoTest);
registry.register(CargoCheck);
registry.register(CargoClippy);
registry.register(CargoFmt);
registry.register(ShellExec);
registry.register(PtyShellTool);
registry.register(GrepSearch);
registry.register(GlobFind);
registry.register(SymbolSearch);
registry.register(HttpRequest);
registry.register(ProcessStart);
registry.register(ProcessStop);
registry.register(ProcessList);
registry.register(ProcessLogs);
registry.register(ProcessRestart);
registry.register(PortCheck);
registry.register(NpmInstall);
registry.register(NpmRun);
registry.register(NpmScripts);
registry.register(PipInstall);
registry.register(PipList);
registry.register(PipFreeze);
registry.register(YarnInstall);
registry.register(ContainerRun);
registry.register(ContainerStop);
registry.register(ContainerList);
registry.register(ContainerLogs);
registry.register(ContainerExec);
registry.register(ContainerBuild);
registry.register(ContainerImages);
registry.register(ContainerPull);
registry.register(ContainerRemove);
registry.register(ComposeUp);
registry.register(ComposeDown);
registry.register(ScreenCapture);
registry.register(VisionAnalyze);
registry.register(VisionCompare);
registry.register(BrowserFetch);
registry.register(BrowserScreenshot);
registry.register(BrowserPdf);
registry.register(BrowserEval);
registry.register(BrowserLinks);
registry.register(PageControlTool::new());
registry.register(KnowledgeAdd);
registry.register(KnowledgeRelate);
registry.register(KnowledgeQuery);
registry.register(KnowledgeStatsTool);
registry.register(KnowledgeClear);
registry.register(KnowledgeRemove);
registry.register(KnowledgeExport);
registry.register(swarm_tool::SwarmDispatchTool::new());
registry.register(computer::ComputerMouseTool);
registry.register(computer::ComputerKeyboardTool);
registry.register(computer::ComputerScreenTool);
registry.register(computer::ComputerWindowTool);
let project_root =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let (lsp_goto, lsp_refs, lsp_syms, lsp_hover) = lsp_tools::create_lsp_tools(project_root);
registry.register(lsp_goto);
registry.register(lsp_refs);
registry.register(lsp_syms);
registry.register(lsp_hover);
registry
}
pub fn register<T: Tool + 'static>(&mut self, tool: T) {
self.tools.insert(tool.name().to_string(), Box::new(tool));
}
pub fn get(&self, name: &str) -> Option<&dyn Tool> {
self.tools.get(name).map(|t| t.as_ref())
}
pub fn list(&self) -> Vec<&dyn Tool> {
self.tools.values().map(|t| t.as_ref()).collect()
}
pub async fn execute(&self, name: &str, args: serde_json::Value) -> Result<serde_json::Value> {
let tool = self
.get(name)
.ok_or_else(|| anyhow::anyhow!("Unknown tool: {}", name))?;
tool.execute(args).await
}
pub fn definitions(&self) -> Vec<crate::api::types::ToolDefinition> {
self.tools
.values()
.map(|tool| crate::api::types::ToolDefinition {
def_type: "function".to_string(),
function: crate::api::types::FunctionDefinition {
name: tool.name().to_string(),
description: tool.description().to_string(),
parameters: tool.schema(),
},
})
.collect()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_registry_new() {
let registry = ToolRegistry::new();
assert!(registry.get("file_read").is_some());
assert!(registry.get("file_write").is_some());
assert!(registry.get("shell_exec").is_some());
assert!(registry.get("cargo_test").is_some());
}
#[test]
fn test_tool_registry_get_nonexistent() {
let registry = ToolRegistry::new();
assert!(registry.get("nonexistent_tool").is_none());
}
#[test]
fn test_tool_registry_list() {
let registry = ToolRegistry::new();
let tools = registry.list();
assert!(tools.len() > 5);
}
#[test]
fn test_tool_registry_default() {
let registry = ToolRegistry::default();
assert!(registry.get("file_read").is_some());
}
#[test]
fn test_tool_registry_definitions() {
let registry = ToolRegistry::new();
let definitions = registry.definitions();
assert!(!definitions.is_empty());
for def in &definitions {
assert_eq!(def.def_type, "function");
assert!(!def.function.name.is_empty());
assert!(!def.function.description.is_empty());
}
}
#[test]
fn test_file_read_tool_properties() {
let registry = ToolRegistry::new();
let tool = registry.get("file_read").unwrap();
assert_eq!(tool.name(), "file_read");
assert!(!tool.description().is_empty());
let schema = tool.schema();
assert!(schema.get("type").is_some());
}
#[test]
fn test_shell_exec_tool_properties() {
let registry = ToolRegistry::new();
let tool = registry.get("shell_exec").unwrap();
assert_eq!(tool.name(), "shell_exec");
assert!(tool.description().contains("Execute"));
}
#[test]
fn test_schema_validator_rejects_non_object_args() {
let registry = ToolRegistry::new();
let tool = registry.get("shell_exec").unwrap();
let err =
validate_tool_arguments_schema(tool.name(), &tool.schema(), &serde_json::json!("ls"))
.unwrap_err()
.to_string();
assert!(err.contains("expected JSON object arguments"));
}
#[test]
fn test_schema_validator_rejects_missing_required_fields() {
let registry = ToolRegistry::new();
let tool = registry.get("process_start").unwrap();
let err =
validate_tool_arguments_schema(tool.name(), &tool.schema(), &serde_json::json!({}))
.unwrap_err()
.to_string();
assert!(err.contains("missing required field(s): id, command"));
}
#[test]
fn test_schema_validator_accepts_required_fields_present() {
let registry = ToolRegistry::new();
let tool = registry.get("file_write").unwrap();
let args = serde_json::json!({
"path": "/tmp/example.txt",
"content": "hello"
});
validate_tool_arguments_schema(tool.name(), &tool.schema(), &args).unwrap();
}
#[test]
fn test_git_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("git_status").is_some());
assert!(registry.get("git_diff").is_some());
assert!(registry.get("git_commit").is_some());
assert!(registry.get("git_push").is_some());
assert!(registry.get("git_checkpoint").is_some());
}
#[test]
fn test_cargo_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("cargo_test").is_some());
assert!(registry.get("cargo_check").is_some());
assert!(registry.get("cargo_clippy").is_some());
assert!(registry.get("cargo_fmt").is_some());
}
#[test]
fn test_file_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("file_read").is_some());
assert!(registry.get("file_write").is_some());
assert!(registry.get("file_edit").is_some());
assert!(registry.get("file_delete").is_some());
assert!(registry.get("directory_tree").is_some());
}
#[test]
fn test_search_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("grep_search").is_some());
assert!(registry.get("glob_find").is_some());
assert!(registry.get("symbol_search").is_some());
}
#[test]
fn test_process_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("process_start").is_some());
assert!(registry.get("process_stop").is_some());
assert!(registry.get("process_list").is_some());
assert!(registry.get("process_logs").is_some());
assert!(registry.get("process_restart").is_some());
assert!(registry.get("port_check").is_some());
}
#[test]
fn test_package_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("npm_install").is_some());
assert!(registry.get("npm_run").is_some());
assert!(registry.get("npm_scripts").is_some());
assert!(registry.get("pip_install").is_some());
assert!(registry.get("pip_list").is_some());
assert!(registry.get("pip_freeze").is_some());
assert!(registry.get("yarn_install").is_some());
}
#[test]
fn test_container_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("container_run").is_some());
assert!(registry.get("container_stop").is_some());
assert!(registry.get("container_list").is_some());
assert!(registry.get("container_logs").is_some());
assert!(registry.get("container_exec").is_some());
assert!(registry.get("container_build").is_some());
assert!(registry.get("container_images").is_some());
assert!(registry.get("container_pull").is_some());
assert!(registry.get("container_remove").is_some());
assert!(registry.get("compose_up").is_some());
assert!(registry.get("compose_down").is_some());
}
#[test]
fn test_browser_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("browser_fetch").is_some());
assert!(registry.get("browser_screenshot").is_some());
assert!(registry.get("browser_pdf").is_some());
assert!(registry.get("browser_eval").is_some());
assert!(registry.get("browser_links").is_some());
}
#[test]
fn test_knowledge_tools_registered() {
let registry = ToolRegistry::new();
assert!(registry.get("knowledge_add").is_some());
assert!(registry.get("knowledge_relate").is_some());
assert!(registry.get("knowledge_query").is_some());
assert!(registry.get("knowledge_stats").is_some());
assert!(registry.get("knowledge_clear").is_some());
assert!(registry.get("knowledge_remove").is_some());
assert!(registry.get("knowledge_export").is_some());
}
#[test]
fn test_truncate_with_pagination_full() {
let (page, info) = truncate_with_pagination("hello", 0, 100);
assert_eq!(page, "hello");
assert_eq!(info.total_chars, 5);
assert!(!info.has_more);
assert_eq!(info.offset, 0);
}
#[test]
fn test_truncate_with_pagination_truncated() {
let (page, info) = truncate_with_pagination("hello world", 0, 5);
assert_eq!(page, "hello");
assert!(info.has_more);
assert_eq!(info.total_chars, 11);
}
#[test]
fn test_truncate_with_pagination_offset() {
let (page, info) = truncate_with_pagination("hello world", 6, 100);
assert_eq!(page, "world");
assert!(!info.has_more);
assert_eq!(info.offset, 6);
}
#[test]
fn test_truncate_with_pagination_unicode() {
let input = "héllo wörld";
let (page, info) = truncate_with_pagination(input, 0, 5);
assert_eq!(page, "héllo");
assert!(info.has_more);
}
#[test]
fn test_truncate_with_pagination_empty() {
let (page, info) = truncate_with_pagination("", 0, 100);
assert_eq!(page, "");
assert_eq!(info.total_chars, 0);
assert!(!info.has_more);
}
#[test]
fn test_truncate_with_pagination_offset_beyond() {
let (page, info) = truncate_with_pagination("hello", 100, 10);
assert_eq!(page, "");
assert!(!info.has_more);
}
}