Add smbCloud-specific context and tool heuristics
- Expand README with what siGit knows about smbCloud repos - Update SYSTEM_PROMPT to include detailed smbCloud platform knowledge, context-aware advice, and tool-use heuristics - Add create_directory tool and prefer absolute paths in tool descriptions - Improve tool descriptions and argument handling for clarity and platform awareness - Ensure all file and directory operations use absolute paths when possible
paydii committed
Apr 24, 2026 at 13:52 UTC
9f0e84431a580ed3937cb40efc5cf510d84b5c26
3 files changed
+285
-79
README.md
+13
index b791a33..c9bb918 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
A coding agent for [smbCloud](https://smbcloud.xyz/) that runs entirely on your machine. No API keys. No cloud round-trips. The model lives in your local HuggingFace cache.
+siGit is meant to be a general coding agent, but it is especially good in smbCloud codebases. It already knows the rough shape of the platform: Rust workspaces with focused crates, Rails services, deploy flows, auth boundaries, and platform-managed services like GresIQ. In smbCloud repos, that means it can usually give more grounded answers with less back-and-forth.
+
siGit has two modes:
- ACP mode, where Zed or another ACP-compatible editor starts it over stdio
@@ -15,6 +17,17 @@ Current platform support:
- Linux: ACP mode and interactive terminal mode
- Windows: ACP mode only for now
+## What siGit knows about smbCloud
+
+When siGit is working in an smbCloud repo, it should lean on platform context instead of treating everything like a generic cloud app. That includes things like:
+
+- the difference between platform user flows and tenant app auth flows
+- the fact that `Project` is the umbrella workspace, while app-like resources such as `FrontendApp`, `AuthApp`, and GresIQ are separate deployable units
+- the fact that Next.js SSR deploys are not the same as the generic git-push path
+- the fact that smbCloud repos usually prefer existing workspace patterns and crate boundaries over new abstractions
+
+Outside smbCloud, it should still behave like a normal coding agent and not force platform-specific advice where it does not belong.
+
## Install
```sh
src/main.rs
+63
-7
index 66c0d51..703515d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -73,18 +73,74 @@ const SYSTEM_PROMPT: &str = "\
Your name is siGit — lowercase 's', uppercase 'G', no spaces. \
Not 'SiGit', not 'Sigit'. Only say your name if the user asks who you are.
-You are the official coding agent for smbCloud (https://smbcloud.xyz), \
-a cloud platform for deploying and managing projects. \
-You help developers build, debug, and ship software on the smbCloud platform.
+You are a strong general-purpose coding agent. smbCloud is your home turf, \
+but you should still be useful in any codebase. When the project is clearly \
+about smbCloud, use that context directly instead of falling back to vague \
+cloud-platform advice.
+
+smbCloud context you should know and use when it helps:
+- smbCloud is a platform for deploying and managing projects
+- the main CLI is a Rust workspace with focused crates rather than one giant crate
+- common areas include auth, project management, deploy flows, networking, \
+ shared models, release tooling, and managed services
+- deploy branches usually follow `release/service-{name}`
+- Next.js SSR deploys on smbCloud are not the same as generic git-push deploys; \
+ they often use a local build plus rsync/PM2 style flow
+- auth has a hard boundary between smbCloud platform users and tenant app users; \
+ platform flows use `/v1/users*`, tenant app flows use `/v1/client/*`, and \
+ you should not casually mix `User`, `TenantMembership`, `AuthApp`, and `AuthUser`
+- smbCloud authorization is layered; do not flatten platform accounts, tenant \
+ memberships, auth-app collaborators, and tenant end users into one model
+- `Project` is the umbrella workspace, while app-like resources such as \
+ `FrontendApp`, `AuthApp`, and GresIQ are the deployable units with their own \
+ ownership, sharing, and collaboration rules
+- `FrontendApp` is many-per-project, while `AuthApp` is intentionally one-per-project; \
+ preserve those cardinality rules unless the code clearly changes them
+- GresIQ is smbCloud's managed PostgreSQL offering; treat it as a platform \
+ service with its own credentials and boundaries, not as a generic local DB helper
+- when debugging smbCloud Rails APIs, first classify the request: first-party \
+ smbCloud app or tenant app, then check which endpoint family and validator \
+ should be involved before changing code
+- when working in smbCloud repos, prefer existing workspace patterns, existing \
+ crate boundaries, existing Rails conventions, and existing command flows over \
+ inventing new abstractions
Never introduce yourself unless asked. Jump straight into the answer. \
Keep answers short. Write idiomatic code. \
Fix root causes, not symptoms.
-You have access to tools that let you read files, list directories, search \
-code, create new files, edit existing files, delete files, and run shell \
-commands. Use them proactively — read the code before answering, run builds \
-and tests after making changes. Always ground your answers in the actual code.
+You have access to tools that let you read files, create directories, list \
+directories, search code, create new files, edit existing files, delete files, \
+and run shell commands. You can also use git directly through shell commands, \
+including `git init` and normal git workflows. Use them proactively. Read the \
+code before answering. Prefer absolute paths when referring to files and \
+directories, especially in protocol-facing output and tool arguments. Create \
+directories when needed. Run builds, tests, and git commands after making \
+changes. Ground your answers in the actual code, not in guesses.
+
+Tool-use heuristics:
+- prefer absolute paths over relative paths when you mention, return, or pass \
+ file and directory paths
+- if a path does not exist yet, create the directory before creating files in it
+- if the user asks for a new repo, scaffold, or scratch project, create the \
+ directory, create the first files, and run `git init` without waiting unless \
+ the request says otherwise
+- if the repo looks like smbCloud CLI code, respect workspace crate boundaries, \
+ shared models, and existing command handlers before adding new abstractions
+- if the repo looks like smbCloud Rails code, check routes, controllers, \
+ validators, and model boundaries before changing business logic
+- if the task touches smbCloud auth, first decide whether it is a platform-user \
+ flow or a tenant-app flow, then follow the right endpoint family and model layer
+- if the task touches smbCloud deploy code, check whether it is the generic \
+ deploy path or the Next.js SSR path before proposing changes
+- after edits, prefer running the smallest useful verification step first, then \
+ widen to broader checks if needed
+- use git commands naturally for status checks, repo setup, diffs, and normal \
+ developer workflows when they help move the task forward
+
+When the repo is not about smbCloud, act like a normal coding agent and do not \
+force smbCloud-specific advice into the answer. When it is about smbCloud, be \
+specific and practical.
Be direct and brief. Write clean, idiomatic code. When debugging, go for the \
root cause, not the symptom. Correct beats clever.";
src/tools.rs
+209
-72
index a3d581e..789749d 100644
--- a/src/tools.rs
+++ b/src/tools.rs
@@ -14,18 +14,19 @@
//!
//! # Write Tools
//!
+//! - `create_directory` — create a directory and any missing parent directories
//! - `create_file` — create a new file (fails if it already exists)
//! - `edit_file` — replace an exact old-text span with new text in an existing file
//! - `delete_file` — delete a file or empty directory at the given path
//!
//! # Shell Tools
//!
-//! - `run_command` — run a shell command and return its combined stdout/stderr output
+//! - `run_command` — run shell commands, including git porcelain and plumbing commands
use regex::Regex;
use serde_json::{Value, json};
use std::fs;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::process::Command;
/// Maximum characters returned from `read_file` before truncation.
@@ -52,8 +53,9 @@ pub fn all_tools() -> Vec<AgentTool> {
AgentTool {
name: "read_file",
description: "Read the contents of a file at the given path. \
- Returns the file text, or an error message if the file cannot be read. \
- Output is truncated to 10 000 characters.",
+ Prefer an absolute path when possible. Returns the file text, \
+ or an error message if the file cannot be read. Output is \
+ truncated to 10 000 characters.",
parameters_schema: json!({
"type": "object",
"properties": {
@@ -66,11 +68,31 @@ pub fn all_tools() -> Vec<AgentTool> {
"additionalProperties": false
}),
},
+ AgentTool {
+ name: "create_directory",
+ description: "Create a directory at the given path. \
+ Prefer an absolute path when possible. Missing parent \
+ directories are created automatically. Use this before \
+ create_file when the parent path does not exist. Succeeds \
+ if the directory already exists.",
+ parameters_schema: json!({
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Absolute or relative path to the directory to create."
+ }
+ },
+ "required": ["path"],
+ "additionalProperties": false
+ }),
+ },
AgentTool {
name: "list_directory",
description: "List files and directories at the given path. \
- Each entry is prefixed with [DIR] or [FILE]. \
- Directories are listed first, sorted alphabetically.",
+ Prefer an absolute path when possible. Each entry is \
+ prefixed with [DIR] or [FILE]. Directories are listed \
+ first, sorted alphabetically.",
parameters_schema: json!({
"type": "object",
"properties": {
@@ -86,9 +108,9 @@ pub fn all_tools() -> Vec<AgentTool> {
AgentTool {
name: "search_files",
description: "Search for a regex pattern across files in a directory tree. \
- Returns matching lines in `file:line_number: content` format. \
- Skips binary files and hidden directories. \
- Limited to the first 50 matches.",
+ Prefer an absolute root path when possible. Returns matching \
+ lines in `file:line_number: content` format. Skips binary \
+ files and hidden directories. Limited to the first 50 matches.",
parameters_schema: json!({
"type": "object",
"properties": {
@@ -108,8 +130,9 @@ pub fn all_tools() -> Vec<AgentTool> {
AgentTool {
name: "create_file",
description: "Create a new file at the given path with the provided content. \
- Parent directories are created automatically if they do not exist. \
- Fails if the file already exists — use edit_file to modify existing files.",
+ Prefer an absolute path when possible. Parent directories are \
+ created automatically if they do not exist. Fails if the file \
+ already exists — use edit_file to modify existing files.",
parameters_schema: json!({
"type": "object",
"properties": {
@@ -129,10 +152,11 @@ pub fn all_tools() -> Vec<AgentTool> {
AgentTool {
name: "edit_file",
description: "Edit an existing file by replacing an exact substring (old_text) with \
- new text (new_text). The old_text must appear exactly once in the file. \
- Use read_file first to see the current content and identify the exact \
- text to replace. To append to a file, match the last few lines as \
- old_text and include them plus the new content as new_text.",
+ new text (new_text). Prefer an absolute path when possible. The \
+ old_text must appear exactly once in the file. Use read_file first \
+ to see the current content and identify the exact text to replace. \
+ To append to a file, match the last few lines as old_text and \
+ include them plus the new content as new_text.",
parameters_schema: json!({
"type": "object",
"properties": {
@@ -156,7 +180,8 @@ pub fn all_tools() -> Vec<AgentTool> {
AgentTool {
name: "delete_file",
description: "Delete a file or empty directory at the given path. \
- Refuses to delete non-empty directories to prevent accidental data loss. \
+ Prefer an absolute path when possible. Refuses to delete \
+ non-empty directories to prevent accidental data loss. \
Use read_file or list_directory first to confirm the target.",
parameters_schema: json!({
"type": "object",
@@ -174,16 +199,22 @@ pub fn all_tools() -> Vec<AgentTool> {
name: "run_command",
description: "Run a shell command and return its combined stdout and stderr output. \
The command runs in the given working directory (defaults to \".\"). \
- Use this for build tools (cargo, npm, make), version control (git), \
- package managers, linters, test runners, and other CLI tasks. \
- Commands that run indefinitely (servers, watchers) will be killed \
- after 120 seconds.",
+ Prefer an absolute working directory when possible. Use this for \
+ build tools (cargo, npm, make), package managers, linters, test \
+ runners, and git commands, including git init, porcelain commands \
+ like status/add/commit/checkout, and plumbing commands like \
+ rev-parse, hash-object, update-ref, and cat-file. If the user asks \
+ for a new repo or scaffold, it is fine to use this for `git init` \
+ and normal repo setup steps. In smbCloud repos, prefer existing \
+ workspace commands, Rails conventions, and deploy flows over \
+ inventing new command sequences. Commands that run indefinitely \
+ (servers, watchers) will be killed after 120 seconds.",
parameters_schema: json!({
"type": "object",
"properties": {
"command": {
"type": "string",
- "description": "The shell command to execute (e.g. \"cargo update\", \"git status\")."
+ "description": "The shell command to execute (e.g. \"cargo update\", \"git status\", \"git rev-parse HEAD\")."
},
"cwd": {
"type": "string",
@@ -208,6 +239,7 @@ pub fn execute_tool(name: &str, arguments: &str) -> String {
"read_file" => exec_read_file(arguments),
"list_directory" => exec_list_directory(arguments),
"search_files" => exec_search_files(arguments),
+ "create_directory" => exec_create_directory(arguments),
"create_file" => exec_create_file(arguments),
"edit_file" => exec_edit_file(arguments),
"delete_file" => exec_delete_file(arguments),
@@ -216,6 +248,20 @@ pub fn execute_tool(name: &str, arguments: &str) -> String {
}
}
+fn absolute_path(path: &Path) -> PathBuf {
+ if path.is_absolute() {
+ path.to_path_buf()
+ } else {
+ std::env::current_dir()
+ .unwrap_or_else(|_| PathBuf::from("."))
+ .join(path)
+ }
+}
+
+fn absolute_path_string(path: &Path) -> String {
+ absolute_path(path).display().to_string()
+}
+
// ── read_file ────────────────────────────────────────────────────────────────
/// Read the contents of a single file, truncating at [`READ_FILE_CHAR_LIMIT`].
@@ -231,16 +277,18 @@ fn exec_read_file(arguments: &str) -> String {
};
let path = Path::new(path_str);
+ let absolute_path = absolute_path(path);
+ let absolute_path_str = absolute_path.display().to_string();
- if !path.exists() {
- return format!("Error: path does not exist: {path_str}");
+ if !absolute_path.exists() {
+ return format!("Error: path does not exist: {absolute_path_str}");
}
- if !path.is_file() {
- return format!("Error: path is not a file: {path_str}");
+ if !absolute_path.is_file() {
+ return format!("Error: path is not a file: {absolute_path_str}");
}
- match fs::read_to_string(path) {
+ match fs::read_to_string(&absolute_path) {
Ok(contents) => {
if contents.len() > READ_FILE_CHAR_LIMIT {
let truncated: String = contents.chars().take(READ_FILE_CHAR_LIMIT).collect();
@@ -271,16 +319,18 @@ fn exec_list_directory(arguments: &str) -> String {
};
let path = Path::new(path_str);
+ let absolute_path = absolute_path(path);
+ let absolute_path_str = absolute_path.display().to_string();
- if !path.exists() {
- return format!("Error: path does not exist: {path_str}");
+ if !absolute_path.exists() {
+ return format!("Error: path does not exist: {absolute_path_str}");
}
- if !path.is_dir() {
- return format!("Error: path is not a directory: {path_str}");
+ if !absolute_path.is_dir() {
+ return format!("Error: path is not a directory: {absolute_path_str}");
}
- let entries = match fs::read_dir(path) {
+ let entries = match fs::read_dir(&absolute_path) {
Ok(rd) => rd,
Err(err) => return format!("Error: could not read directory: {err}"),
};
@@ -318,7 +368,7 @@ fn exec_list_directory(arguments: &str) -> String {
dirs.extend(files);
if dirs.is_empty() {
- return format!("(empty directory: {path_str})");
+ return format!("(empty directory: {absolute_path_str})");
}
dirs.join("\n")
@@ -346,17 +396,19 @@ fn exec_search_files(arguments: &str) -> String {
};
let root = Path::new(root_str);
+ let absolute_root = absolute_path(root);
+ let absolute_root_str = absolute_root.display().to_string();
- if !root.exists() {
- return format!("Error: path does not exist: {root_str}");
+ if !absolute_root.exists() {
+ return format!("Error: path does not exist: {absolute_root_str}");
}
- if !root.is_dir() {
- return format!("Error: path is not a directory: {root_str}");
+ if !absolute_root.is_dir() {
+ return format!("Error: path is not a directory: {absolute_root_str}");
}
let mut matches: Vec<String> = Vec::new();
- walk_and_search(root, &re, &mut matches);
+ walk_and_search(&absolute_root, &re, &mut matches);
if matches.is_empty() {
return format!("No matches found for pattern: {pattern_str}");
@@ -423,7 +475,7 @@ fn search_file(path: &Path, re: &Regex, matches: &mut Vec<String>) {
Err(_) => return,
};
- let display_path = path.display();
+ let display_path = absolute_path_string(path);
for (line_idx, line) in contents.lines().enumerate() {
if re.is_match(line) {
@@ -433,7 +485,38 @@ fn search_file(path: &Path, re: &Regex, matches: &mut Vec<String>) {
}
}
-// ── create_file ──────────────────────────────────────────────────────────────
+/// ── create_directory ─────────────────────────────────────────────────────────
+
+/// Create a directory and any missing parent directories.
+fn exec_create_directory(arguments: &str) -> String {
+ let args: Value = match serde_json::from_str(arguments) {
+ Ok(v) => v,
+ Err(err) => return format!("Error: failed to parse arguments: {err}"),
+ };
+
+ let path_str = match args.get("path").and_then(Value::as_str) {
+ Some(p) => p,
+ None => return "Error: missing required parameter \"path\"".to_string(),
+ };
+
+ let path = Path::new(path_str);
+ let absolute_path = absolute_path(path);
+ let absolute_path_str = absolute_path.display().to_string();
+
+ if absolute_path.exists() {
+ if absolute_path.is_dir() {
+ return format!("Directory already exists: {absolute_path_str}");
+ }
+ return format!("Error: path exists and is not a directory: {absolute_path_str}");
+ }
+
+ match fs::create_dir_all(&absolute_path) {
+ Ok(()) => format!("Created directory: {absolute_path_str}"),
+ Err(err) => format!("Error: could not create directory: {err}"),
+ }
+}
+
+/// ── create_file ──────────────────────────────────────────────────────────────
/// Create a new file with the provided content.
///
@@ -457,15 +540,17 @@ fn exec_create_file(arguments: &str) -> String {
};
let path = Path::new(path_str);
+ let absolute_path = absolute_path(path);
+ let absolute_path_str = absolute_path.display().to_string();
- if path.exists() {
+ if absolute_path.exists() {
return format!(
- "Error: file already exists: {path_str} — use edit_file to modify existing files"
+ "Error: file already exists: {absolute_path_str} — use edit_file to modify existing files"
);
}
// Create parent directories if needed.
- if let Some(parent) = path.parent()
+ if let Some(parent) = absolute_path.parent()
&& !parent.as_os_str().is_empty()
&& !parent.exists()
&& let Err(err) = fs::create_dir_all(parent)
@@ -473,8 +558,11 @@ fn exec_create_file(arguments: &str) -> String {
return format!("Error: could not create parent directories: {err}");
}
- match fs::write(path, content) {
- Ok(()) => format!("Created file: {path_str} ({} bytes)", content.len()),
+ match fs::write(&absolute_path, content) {
+ Ok(()) => format!(
+ "Created file: {absolute_path_str} ({} bytes)",
+ content.len()
+ ),
Err(err) => format!("Error: could not write file: {err}"),
}
}
@@ -509,16 +597,20 @@ fn exec_edit_file(arguments: &str) -> String {
};
let path = Path::new(path_str);
+ let absolute_path = absolute_path(path);
+ let absolute_path_str = absolute_path.display().to_string();
- if !path.exists() {
- return format!("Error: file does not exist: {path_str} — use create_file for new files");
+ if !absolute_path.exists() {
+ return format!(
+ "Error: file does not exist: {absolute_path_str} — use create_file for new files"
+ );
}
- if !path.is_file() {
- return format!("Error: path is not a file: {path_str}");
+ if !absolute_path.is_file() {
+ return format!("Error: path is not a file: {absolute_path_str}");
}
- let contents = match fs::read_to_string(path) {
+ let contents = match fs::read_to_string(&absolute_path) {
Ok(c) => c,
Err(err) => return format!("Error: could not read file: {err}"),
};
@@ -528,22 +620,25 @@ fn exec_edit_file(arguments: &str) -> String {
if occurrences == 0 {
return format!(
- "Error: old_text not found in {path_str}. \
+ "Error: old_text not found in {absolute_path_str}. \
Use read_file to see the current content and copy the exact text to replace."
);
}
if occurrences > 1 {
return format!(
- "Error: old_text appears {occurrences} times in {path_str}. \
+ "Error: old_text appears {occurrences} times in {absolute_path_str}. \
Include more surrounding context in old_text so it matches exactly once."
);
}
let updated = contents.replacen(old_text, new_text, 1);
- match fs::write(path, &updated) {
- Ok(()) => format!("Edited file: {path_str} ({} bytes written)", updated.len()),
+ match fs::write(&absolute_path, &updated) {
+ Ok(()) => format!(
+ "Edited file: {absolute_path_str} ({} bytes written)",
+ updated.len()
+ ),
Err(err) => format!("Error: could not write file: {err}"),
}
}
@@ -566,22 +661,24 @@ fn exec_delete_file(arguments: &str) -> String {
};
let path = Path::new(path_str);
+ let absolute_path = absolute_path(path);
+ let absolute_path_str = absolute_path.display().to_string();
- if !path.exists() {
- return format!("Error: path does not exist: {path_str}");
+ if !absolute_path.exists() {
+ return format!("Error: path does not exist: {absolute_path_str}");
}
- if path.is_dir() {
- match fs::remove_dir(path) {
- Ok(()) => format!("Deleted empty directory: {path_str}"),
+ if absolute_path.is_dir() {
+ match fs::remove_dir(&absolute_path) {
+ Ok(()) => format!("Deleted empty directory: {absolute_path_str}"),
Err(err) => format!(
"Error: could not delete directory: {err}. \
Only empty directories can be deleted."
),
}
} else {
- match fs::remove_file(path) {
- Ok(()) => format!("Deleted file: {path_str}"),
+ match fs::remove_file(&absolute_path) {
+ Ok(()) => format!("Deleted file: {absolute_path_str}"),
Err(err) => format!("Error: could not delete file: {err}"),
}
}
@@ -613,19 +710,20 @@ fn exec_run_command(arguments: &str) -> String {
};
let cwd = args.get("cwd").and_then(Value::as_str).unwrap_or(".");
+ let cwd_path = absolute_path(Path::new(cwd));
+ let cwd_str = cwd_path.display().to_string();
- let cwd_path = Path::new(cwd);
if !cwd_path.exists() {
- return format!("Error: working directory does not exist: {cwd}");
+ return format!("Error: working directory does not exist: {cwd_str}");
}
- log::info!("run_command: `{command_str}` in `{cwd}`");
+ log::info!("run_command: `{command_str}` in `{cwd_str}`");
#[cfg(unix)]
let mut child = match Command::new("sh")
.arg("-c")
.arg(command_str)
- .current_dir(cwd_path)
+ .current_dir(&cwd_path)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
@@ -638,7 +736,7 @@ fn exec_run_command(arguments: &str) -> String {
let mut child = match Command::new("cmd")
.arg("/C")
.arg(command_str)
- .current_dir(cwd_path)
+ .current_dir(&cwd_path)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
@@ -814,14 +912,15 @@ mod tests {
#[test]
fn test_all_tools_count() {
let tools = all_tools();
- assert_eq!(tools.len(), 7);
+ assert_eq!(tools.len(), 8);
assert_eq!(tools[0].name, "read_file");
- assert_eq!(tools[1].name, "list_directory");
- assert_eq!(tools[2].name, "search_files");
- assert_eq!(tools[3].name, "create_file");
- assert_eq!(tools[4].name, "edit_file");
- assert_eq!(tools[5].name, "delete_file");
- assert_eq!(tools[6].name, "run_command");
+ assert_eq!(tools[1].name, "create_directory");
+ assert_eq!(tools[2].name, "list_directory");
+ assert_eq!(tools[3].name, "search_files");
+ assert_eq!(tools[4].name, "create_file");
+ assert_eq!(tools[5].name, "edit_file");
+ assert_eq!(tools[6].name, "delete_file");
+ assert_eq!(tools[7].name, "run_command");
}
#[test]
@@ -839,6 +938,44 @@ mod tests {
}
}
+ // ── create_directory tests ───────────────────────────────────────────
+
+ #[test]
+ fn test_create_directory_missing_path() {
+ let result = exec_create_directory("{}");
+ assert!(result.contains("missing required parameter"));
+ }
+
+ #[test]
+ fn test_create_directory_success() {
+ let dir = std::env::temp_dir()
+ .join("sigit_test_create_directory")
+ .join("nested")
+ .join("child");
+ let _ = fs::remove_dir_all(dir.parent().unwrap());
+
+ let args = serde_json::json!({ "path": dir }).to_string();
+ let result = exec_create_directory(&args);
+ assert!(result.starts_with("Created directory:"), "got: {result}");
+ assert!(dir.exists());
+ assert!(dir.is_dir());
+
+ let _ = fs::remove_dir_all(dir.parent().unwrap().parent().unwrap());
+ }
+
+ #[test]
+ fn test_create_directory_already_exists() {
+ let dir = std::env::temp_dir().join("sigit_test_create_directory_exists");
+ let _ = fs::remove_dir_all(&dir);
+ fs::create_dir_all(&dir).unwrap();
+
+ let args = serde_json::json!({ "path": dir }).to_string();
+ let result = exec_create_directory(&args);
+ assert!(result.contains("Directory already exists"), "got: {result}");
+
+ let _ = fs::remove_dir_all(&dir);
+ }
+
// ── create_file tests ────────────────────────────────────────────────
#[test]