Skip to main content

synaptic_deep/tools/
mod.rs

1use serde_json::{json, Value};
2use std::sync::Arc;
3use synaptic_core::{SynapticError, Tool};
4use synaptic_macros::{tool, traceable};
5
6use crate::backend::{Backend, GrepOutputMode};
7
8/// Create the built-in filesystem tools backed by the given backend.
9///
10/// Returns 6 tools (ls, read_file, write_file, edit_file, glob, grep) plus
11/// an `execute` tool if the backend supports execution.
12#[traceable(skip = "backend")]
13pub fn create_filesystem_tools(backend: Arc<dyn Backend>) -> Vec<Arc<dyn Tool>> {
14    let mut tools: Vec<Arc<dyn Tool>> = vec![
15        ls(backend.clone()),
16        read_file(backend.clone()),
17        write_file(backend.clone()),
18        edit_file(backend.clone()),
19        glob_files(backend.clone()),
20        grep(backend.clone()),
21    ];
22    if backend.supports_execution() {
23        tools.push(execute(backend));
24    }
25    tools
26}
27
28/// List directory contents
29#[tool]
30async fn ls(
31    #[field] backend: Arc<dyn Backend>,
32    /// Directory path to list
33    path: String,
34) -> Result<Value, SynapticError> {
35    let entries = backend.ls(&path).await?;
36    serde_json::to_value(entries)
37        .map_err(|e| SynapticError::Tool(format!("serialization: {}", e)))
38}
39
40/// Read file contents with optional line-based pagination
41#[tool]
42async fn read_file(
43    #[field] backend: Arc<dyn Backend>,
44    /// File path to read
45    path: String,
46    /// Starting line number (0-based, default 0)
47    #[default = 0]
48    offset: usize,
49    /// Maximum lines to read (default 2000)
50    #[default = 2000]
51    limit: usize,
52) -> Result<Value, SynapticError> {
53    let content = backend.read_file(&path, offset, limit).await?;
54    Ok(Value::String(content))
55}
56
57/// Create or overwrite a file with the given content
58#[tool]
59async fn write_file(
60    #[field] backend: Arc<dyn Backend>,
61    /// File path to write
62    path: String,
63    /// Content to write
64    content: String,
65) -> Result<Value, SynapticError> {
66    backend.write_file(&path, &content).await?;
67    Ok(Value::String(format!("wrote {}", path)))
68}
69
70/// Find and replace text in a file
71#[tool]
72async fn edit_file(
73    #[field] backend: Arc<dyn Backend>,
74    /// File path to edit
75    path: String,
76    /// Text to find
77    old_string: String,
78    /// Replacement text
79    new_string: String,
80    /// Replace all occurrences (default false)
81    #[default = false]
82    replace_all: bool,
83) -> Result<Value, SynapticError> {
84    backend
85        .edit_file(&path, &old_string, &new_string, replace_all)
86        .await?;
87    Ok(Value::String(format!("edited {}", path)))
88}
89
90/// Find files matching a glob pattern
91#[tool(name = "glob")]
92async fn glob_files(
93    #[field] backend: Arc<dyn Backend>,
94    /// Glob pattern (e.g. **/*.rs)
95    pattern: String,
96    /// Base directory (default .)
97    #[default = ".".to_string()]
98    path: String,
99) -> Result<Value, SynapticError> {
100    let matches = backend.glob(&pattern, &path).await?;
101    Ok(Value::String(matches.join("\n")))
102}
103
104/// Search file contents by regex pattern
105#[tool]
106async fn grep(
107    #[field] backend: Arc<dyn Backend>,
108    /// Regex pattern to search for
109    pattern: String,
110    /// Directory or file to search in
111    path: Option<String>,
112    /// Glob pattern to filter files
113    glob: Option<String>,
114    /// Output format: files_with_matches (default), content, count
115    output_mode: Option<String>,
116) -> Result<Value, SynapticError> {
117    let mode = match output_mode.as_deref() {
118        Some("content") => GrepOutputMode::Content,
119        Some("count") => GrepOutputMode::Count,
120        _ => GrepOutputMode::FilesWithMatches,
121    };
122    let result = backend
123        .grep(&pattern, path.as_deref(), glob.as_deref(), mode)
124        .await?;
125    Ok(Value::String(result))
126}
127
128/// Execute a shell command
129#[tool]
130async fn execute(
131    #[field] backend: Arc<dyn Backend>,
132    /// Shell command to execute
133    command: String,
134    /// Timeout in seconds
135    timeout: Option<u64>,
136) -> Result<Value, SynapticError> {
137    let duration = timeout.map(std::time::Duration::from_secs);
138    let result = backend.execute(&command, duration).await?;
139    Ok(json!({
140        "stdout": result.stdout,
141        "stderr": result.stderr,
142        "exit_code": result.exit_code,
143    }))
144}