Skip to main content

exeora_cli/tools/
mod.rs

1mod files;
2pub(crate) mod path;
3mod processes;
4
5pub use processes::ProcessRegistry;
6
7use crate::{
8    error::{ErrorCode, ExeoraError},
9    protocol::ToolName,
10};
11use anyhow::{Result, anyhow};
12use jsonschema::Validator;
13use serde_json::Value;
14use std::{collections::HashMap, path::Path, sync::Arc};
15use tokio_util::sync::CancellationToken;
16
17pub struct ToolEngine {
18    validators: HashMap<ToolName, Validator>,
19    processes: Arc<ProcessRegistry>,
20}
21
22impl ToolEngine {
23    pub fn new() -> Result<Self> {
24        let contract: Value = serde_json::from_str(include_str!("../../protocol/contract.json"))?;
25        let tools = contract
26            .pointer("/schemas/tools")
27            .and_then(Value::as_object)
28            .ok_or_else(|| anyhow!("generated tool schemas are missing"))?;
29        let mut validators = HashMap::new();
30        for tool in ToolName::ALL {
31            let schema = tools
32                .get(tool.as_str())
33                .and_then(|value| value.get("input"))
34                .ok_or_else(|| anyhow!("schema missing for {tool}"))?;
35            validators.insert(tool, jsonschema::validator_for(schema)?);
36        }
37        Ok(Self {
38            validators,
39            processes: Arc::new(ProcessRegistry::new()),
40        })
41    }
42
43    pub async fn execute(
44        &self,
45        root: &Path,
46        tool: ToolName,
47        arguments: Value,
48        cancel: CancellationToken,
49    ) -> Result<Value, ExeoraError> {
50        self.execute_for_project(root, &root.to_string_lossy(), tool, arguments, cancel)
51            .await
52    }
53
54    pub async fn execute_for_project(
55        &self,
56        root: &Path,
57        project_scope: &str,
58        tool: ToolName,
59        arguments: Value,
60        cancel: CancellationToken,
61    ) -> Result<Value, ExeoraError> {
62        let validator = self
63            .validators
64            .get(&tool)
65            .ok_or_else(|| ExeoraError::new(ErrorCode::UnknownTool, "Unsupported tool."))?;
66        if let Err(error) = validator.validate(&arguments) {
67            return Err(ExeoraError::new(
68                ErrorCode::InvalidArguments,
69                error.to_string(),
70            ));
71        }
72        if cancel.is_cancelled() {
73            return Err(ExeoraError::new(
74                ErrorCode::Cancelled,
75                "The call was cancelled before it started.",
76            ));
77        }
78
79        match tool {
80            ToolName::ReadFile => files::read_file(root, arguments).await,
81            ToolName::ListFiles => files::list_files(root, arguments).await,
82            ToolName::Grep => files::grep(root, arguments).await,
83            ToolName::EditFile => files::edit_file(root, arguments).await,
84            ToolName::WriteFile => files::write_file(root, arguments).await,
85            ToolName::RunCommand => self.processes.run_command(root, arguments, cancel).await,
86            ToolName::StartCommand => {
87                self.processes
88                    .start_command(root, project_scope, arguments)
89                    .await
90            }
91            ToolName::GetCommandOutput => self.processes.get_output(root, arguments).await,
92            ToolName::SendCommandInput => self.processes.send_input(root, arguments).await,
93            ToolName::KillCommand => self.processes.kill_command(root, arguments).await,
94        }
95    }
96
97    pub async fn kill_all(&self) {
98        self.processes.kill_all().await;
99    }
100
101    pub async fn kill_root(&self, root: &Path) {
102        self.processes.kill_root(root).await;
103    }
104}