@hej / sigit / commits / c276617

Add delete_file tool to allow deleting files and empty directories

Includes implementation, schema, and tests for the new delete_file tool. Updates agent prompt and documentation to mention delete_file capability.

Seto Elkahfi committed Apr 24, 2026 at 01:28 UTC c276617a2db8b09658a95285ce6b0cd462ae5623
4 files changed +131 -7
Cargo.lock
+1 -1
@@ -5418,7 +5418,7 @@ version = "0.3.35"
5418 source = "registry+https://github.com/rust-lang/crates.io-index"
5419 checksum = "1c236db19d4cea01cf48b584838fcc898745514c8e01c1c0cad0cba976cabefb"
5420 dependencies = [
5421 - "clap 4.6.0",
5421 + "clap 4.6.1",
5422 "log",
5423 "reqwest 0.12.28",
5424 "serde",
src/chat.rs
+3 -1
@@ -17,7 +17,9 @@ use std::sync::mpsc as std_mpsc;
17 use anyhow::Result;
18 use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
19 use futures::StreamExt;
20 -use onde::inference::{ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk, ToolDefinition, ToolResult};
20 +use onde::inference::{
21 + ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk, ToolDefinition, ToolResult,
22 +};
23 use ratatui::{
24 Frame,
25 layout::{Constraint, Layout, Position},
src/main.rs
+4 -3
@@ -66,9 +66,10 @@ You help developers build, debug, and ship software on the smbCloud platform.
66 Keep answers short. Write idiomatic code. \
67 Fix root causes, not symptoms.
68
69 -You have access to tools that let you read files, list directories, and search \
70 -code. Use them proactively to understand the codebase before answering questions \
71 -or writing code. Always ground your answers in the actual code.
69 +You have access to tools that let you read files, list directories, search \
70 +code, create new files, edit existing files, and delete files. Use them \
71 +proactively to understand the codebase before answering questions or writing \
72 +code. Always ground your answers in the actual code.
73
74 Be direct and brief. Write clean, idiomatic code. When debugging, go for the \
75 root cause, not the symptom. Correct beats clever.";
src/tools.rs
+123 -2
@@ -1,4 +1,4 @@
1 -//! Tool definitions and execution for the siGit coding agent.
1 +//! Tool definitions and execution for the siGit Code.
2 //!
3 //! Each tool has:
4 //! - A schema (JSON Schema) that describes its parameters for the LLM
@@ -16,6 +16,7 @@
16 //!
17 //! - `create_file` — create a new file (fails if it already exists)
18 //! - `edit_file` — replace an exact old-text span with new text in an existing file
19 +//! - `delete_file` — delete a file or empty directory at the given path
20
21 use regex::Regex;
22 use serde_json::{Value, json};
@@ -147,6 +148,23 @@ pub fn all_tools() -> Vec<AgentTool> {
148 "additionalProperties": false
149 }),
150 },
151 + AgentTool {
152 + name: "delete_file",
153 + description: "Delete a file or empty directory at the given path. \
154 + Refuses to delete non-empty directories to prevent accidental data loss. \
155 + Use read_file or list_directory first to confirm the target.",
156 + parameters_schema: json!({
157 + "type": "object",
158 + "properties": {
159 + "path": {
160 + "type": "string",
161 + "description": "Absolute or relative path to the file or empty directory to delete."
162 + }
163 + },
164 + "required": ["path"],
165 + "additionalProperties": false
166 + }),
167 + },
168 ]
169 }
170
@@ -163,6 +181,7 @@ pub fn execute_tool(name: &str, arguments: &str) -> String {
181 "search_files" => exec_search_files(arguments),
182 "create_file" => exec_create_file(arguments),
183 "edit_file" => exec_edit_file(arguments),
184 + "delete_file" => exec_delete_file(arguments),
185 _ => format!("Unknown tool: {name}"),
186 }
187 }
@@ -499,6 +518,45 @@ fn exec_edit_file(arguments: &str) -> String {
518 }
519 }
520
521 +// ── delete_file ──────────────────────────────────────────────────────────────
522 +
523 +/// Delete a file or empty directory at the given path.
524 +///
525 +/// Refuses to remove non-empty directories to guard against accidental
526 +/// recursive deletes.
527 +fn exec_delete_file(arguments: &str) -> String {
528 + let args: Value = match serde_json::from_str(arguments) {
529 + Ok(v) => v,
530 + Err(err) => return format!("Error: failed to parse arguments: {err}"),
531 + };
532 +
533 + let path_str = match args.get("path").and_then(Value::as_str) {
534 + Some(p) => p,
535 + None => return "Error: missing required parameter \"path\"".to_string(),
536 + };
537 +
538 + let path = Path::new(path_str);
539 +
540 + if !path.exists() {
541 + return format!("Error: path does not exist: {path_str}");
542 + }
543 +
544 + if path.is_dir() {
545 + match fs::remove_dir(path) {
546 + Ok(()) => format!("Deleted empty directory: {path_str}"),
547 + Err(err) => format!(
548 + "Error: could not delete directory: {err}. \
549 + Only empty directories can be deleted."
550 + ),
551 + }
552 + } else {
553 + match fs::remove_file(path) {
554 + Ok(()) => format!("Deleted file: {path_str}"),
555 + Err(err) => format!("Error: could not delete file: {err}"),
556 + }
557 + }
558 +}
559 +
560 #[cfg(test)]
561 mod tests {
562 use super::*;
@@ -613,12 +671,13 @@ mod tests {
671 #[test]
672 fn test_all_tools_count() {
673 let tools = all_tools();
616 - assert_eq!(tools.len(), 5);
674 + assert_eq!(tools.len(), 6);
675 assert_eq!(tools[0].name, "read_file");
676 assert_eq!(tools[1].name, "list_directory");
677 assert_eq!(tools[2].name, "search_files");
678 assert_eq!(tools[3].name, "create_file");
679 assert_eq!(tools[4].name, "edit_file");
680 + assert_eq!(tools[5].name, "delete_file");
681 }
682
683 #[test]
@@ -778,4 +837,66 @@ mod tests {
837
838 let _ = fs::remove_dir_all(&dir);
839 }
840 +
841 + // ── delete_file tests ────────────────────────────────────────────────
842 +
843 + #[test]
844 + fn test_delete_file_missing_path() {
845 + let result = exec_delete_file("{}");
846 + assert!(
847 + result.contains("missing required parameter"),
848 + "got: {result}"
849 + );
850 + }
851 +
852 + #[test]
853 + fn test_delete_file_nonexistent() {
854 + let result = exec_delete_file(r#"{"path": "/tmp/sigit_test_no_such_file_xyz"}"#);
855 + assert!(result.contains("does not exist"), "got: {result}");
856 + }
857 +
858 + #[test]
859 + fn test_delete_file_success() {
860 + let dir = std::env::temp_dir().join("sigit_test_delete_file");
861 + let _ = fs::remove_dir_all(&dir);
862 + fs::create_dir_all(&dir).unwrap();
863 +
864 + let file_path = dir.join("to_delete.txt");
865 + fs::write(&file_path, "bye").unwrap();
866 + assert!(file_path.exists());
867 +
868 + let args = format!(r#"{{"path": "{}"}}"#, file_path.display());
869 + let result = exec_delete_file(&args);
870 + assert!(result.contains("Deleted file"), "got: {result}");
871 + assert!(!file_path.exists());
872 +
873 + let _ = fs::remove_dir_all(&dir);
874 + }
875 +
876 + #[test]
877 + fn test_delete_empty_directory() {
878 + let dir = std::env::temp_dir().join("sigit_test_delete_empty_dir");
879 + let _ = fs::remove_dir_all(&dir);
880 + fs::create_dir_all(&dir).unwrap();
881 +
882 + let args = format!(r#"{{"path": "{}"}}"#, dir.display());
883 + let result = exec_delete_file(&args);
884 + assert!(result.contains("Deleted empty directory"), "got: {result}");
885 + assert!(!dir.exists());
886 + }
887 +
888 + #[test]
889 + fn test_delete_nonempty_directory() {
890 + let dir = std::env::temp_dir().join("sigit_test_delete_nonempty_dir");
891 + let _ = fs::remove_dir_all(&dir);
892 + fs::create_dir_all(&dir).unwrap();
893 + fs::write(dir.join("child.txt"), "content").unwrap();
894 +
895 + let args = format!(r#"{{"path": "{}"}}"#, dir.display());
896 + let result = exec_delete_file(&args);
897 + assert!(result.contains("Error"), "got: {result}");
898 + assert!(dir.exists(), "directory should not have been deleted");
899 +
900 + let _ = fs::remove_dir_all(&dir);
901 + }
902 }