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).map_err(|e| SynapticError::Tool(format!("serialization: {}", e)))
37}
38
39/// Read file contents with optional line-based pagination
40#[tool]
41async fn read_file(
42    #[field] backend: Arc<dyn Backend>,
43    /// File path to read
44    path: String,
45    /// Starting line number (0-based, default 0)
46    #[default = 0]
47    offset: usize,
48    /// Maximum lines to read (default 2000)
49    #[default = 2000]
50    limit: usize,
51) -> Result<Value, SynapticError> {
52    let content = backend.read_file(&path, offset, limit).await?;
53    Ok(Value::String(content))
54}
55
56/// Create or overwrite a file with the given content
57#[tool]
58async fn write_file(
59    #[field] backend: Arc<dyn Backend>,
60    /// File path to write
61    path: String,
62    /// Content to write
63    content: String,
64) -> Result<Value, SynapticError> {
65    backend.write_file(&path, &content).await?;
66    Ok(Value::String(format!("wrote {}", path)))
67}
68
69/// Find and replace text in a file
70#[tool]
71async fn edit_file(
72    #[field] backend: Arc<dyn Backend>,
73    /// File path to edit
74    path: String,
75    /// Text to find
76    old_string: String,
77    /// Replacement text
78    new_string: String,
79    /// Replace all occurrences (default false)
80    #[default = false]
81    replace_all: bool,
82) -> Result<Value, SynapticError> {
83    backend
84        .edit_file(&path, &old_string, &new_string, replace_all)
85        .await?;
86    Ok(Value::String(format!("edited {}", path)))
87}
88
89/// Find files matching a glob pattern
90#[tool(name = "glob")]
91async fn glob_files(
92    #[field] backend: Arc<dyn Backend>,
93    /// Glob pattern (e.g. **/*.rs)
94    pattern: String,
95    /// Base directory (default .)
96    #[default = ".".to_string()]
97    path: String,
98) -> Result<Value, SynapticError> {
99    let matches = backend.glob(&pattern, &path).await?;
100    Ok(Value::String(matches.join("\n")))
101}
102
103/// Search file contents by regex pattern
104#[tool]
105async fn grep(
106    #[field] backend: Arc<dyn Backend>,
107    /// Regex pattern to search for
108    pattern: String,
109    /// Directory or file to search in
110    path: Option<String>,
111    /// Glob pattern to filter files
112    glob: Option<String>,
113    /// Output format: files_with_matches (default), content, count
114    output_mode: Option<String>,
115) -> Result<Value, SynapticError> {
116    let mode = match output_mode.as_deref() {
117        Some("content") => GrepOutputMode::Content,
118        Some("count") => GrepOutputMode::Count,
119        _ => GrepOutputMode::FilesWithMatches,
120    };
121    let result = backend
122        .grep(&pattern, path.as_deref(), glob.as_deref(), mode)
123        .await?;
124    Ok(Value::String(result))
125}
126
127/// Execute a shell command
128#[tool]
129async fn execute(
130    #[field] backend: Arc<dyn Backend>,
131    /// Shell command to execute
132    command: String,
133    /// Timeout in seconds
134    timeout: Option<u64>,
135) -> Result<Value, SynapticError> {
136    let duration = timeout.map(std::time::Duration::from_secs);
137    let result = backend.execute(&command, duration).await?;
138    Ok(json!({
139        "stdout": result.stdout,
140        "stderr": result.stderr,
141        "exit_code": result.exit_code,
142    }))
143}