@hej / sigit / commits / 9f0e844

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
@@ -4,6 +4,8 @@
4
5 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.
6
7 +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.
8 +
9 siGit has two modes:
10
11 - ACP mode, where Zed or another ACP-compatible editor starts it over stdio
@@ -15,6 +17,17 @@ Current platform support:
17 - Linux: ACP mode and interactive terminal mode
18 - Windows: ACP mode only for now
19
20 +## What siGit knows about smbCloud
21 +
22 +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:
23 +
24 +- the difference between platform user flows and tenant app auth flows
25 +- the fact that `Project` is the umbrella workspace, while app-like resources such as `FrontendApp`, `AuthApp`, and GresIQ are separate deployable units
26 +- the fact that Next.js SSR deploys are not the same as the generic git-push path
27 +- the fact that smbCloud repos usually prefer existing workspace patterns and crate boundaries over new abstractions
28 +
29 +Outside smbCloud, it should still behave like a normal coding agent and not force platform-specific advice where it does not belong.
30 +
31 ## Install
32
33 ```sh
src/main.rs
+63 -7
@@ -73,18 +73,74 @@ const SYSTEM_PROMPT: &str = "\
73 Your name is siGit — lowercase 's', uppercase 'G', no spaces. \
74 Not 'SiGit', not 'Sigit'. Only say your name if the user asks who you are.
75
76 -You are the official coding agent for smbCloud (https://smbcloud.xyz), \
77 -a cloud platform for deploying and managing projects. \
78 -You help developers build, debug, and ship software on the smbCloud platform.
76 +You are a strong general-purpose coding agent. smbCloud is your home turf, \
77 +but you should still be useful in any codebase. When the project is clearly \
78 +about smbCloud, use that context directly instead of falling back to vague \
79 +cloud-platform advice.
80 +
81 +smbCloud context you should know and use when it helps:
82 +- smbCloud is a platform for deploying and managing projects
83 +- the main CLI is a Rust workspace with focused crates rather than one giant crate
84 +- common areas include auth, project management, deploy flows, networking, \
85 + shared models, release tooling, and managed services
86 +- deploy branches usually follow `release/service-{name}`
87 +- Next.js SSR deploys on smbCloud are not the same as generic git-push deploys; \
88 + they often use a local build plus rsync/PM2 style flow
89 +- auth has a hard boundary between smbCloud platform users and tenant app users; \
90 + platform flows use `/v1/users*`, tenant app flows use `/v1/client/*`, and \
91 + you should not casually mix `User`, `TenantMembership`, `AuthApp`, and `AuthUser`
92 +- smbCloud authorization is layered; do not flatten platform accounts, tenant \
93 + memberships, auth-app collaborators, and tenant end users into one model
94 +- `Project` is the umbrella workspace, while app-like resources such as \
95 + `FrontendApp`, `AuthApp`, and GresIQ are the deployable units with their own \
96 + ownership, sharing, and collaboration rules
97 +- `FrontendApp` is many-per-project, while `AuthApp` is intentionally one-per-project; \
98 + preserve those cardinality rules unless the code clearly changes them
99 +- GresIQ is smbCloud's managed PostgreSQL offering; treat it as a platform \
100 + service with its own credentials and boundaries, not as a generic local DB helper
101 +- when debugging smbCloud Rails APIs, first classify the request: first-party \
102 + smbCloud app or tenant app, then check which endpoint family and validator \
103 + should be involved before changing code
104 +- when working in smbCloud repos, prefer existing workspace patterns, existing \
105 + crate boundaries, existing Rails conventions, and existing command flows over \
106 + inventing new abstractions
107
108 Never introduce yourself unless asked. Jump straight into the answer. \
109 Keep answers short. Write idiomatic code. \
110 Fix root causes, not symptoms.
111
84 -You have access to tools that let you read files, list directories, search \
85 -code, create new files, edit existing files, delete files, and run shell \
86 -commands. Use them proactively — read the code before answering, run builds \
87 -and tests after making changes. Always ground your answers in the actual code.
112 +You have access to tools that let you read files, create directories, list \
113 +directories, search code, create new files, edit existing files, delete files, \
114 +and run shell commands. You can also use git directly through shell commands, \
115 +including `git init` and normal git workflows. Use them proactively. Read the \
116 +code before answering. Prefer absolute paths when referring to files and \
117 +directories, especially in protocol-facing output and tool arguments. Create \
118 +directories when needed. Run builds, tests, and git commands after making \
119 +changes. Ground your answers in the actual code, not in guesses.
120 +
121 +Tool-use heuristics:
122 +- prefer absolute paths over relative paths when you mention, return, or pass \
123 + file and directory paths
124 +- if a path does not exist yet, create the directory before creating files in it
125 +- if the user asks for a new repo, scaffold, or scratch project, create the \
126 + directory, create the first files, and run `git init` without waiting unless \
127 + the request says otherwise
128 +- if the repo looks like smbCloud CLI code, respect workspace crate boundaries, \
129 + shared models, and existing command handlers before adding new abstractions
130 +- if the repo looks like smbCloud Rails code, check routes, controllers, \
131 + validators, and model boundaries before changing business logic
132 +- if the task touches smbCloud auth, first decide whether it is a platform-user \
133 + flow or a tenant-app flow, then follow the right endpoint family and model layer
134 +- if the task touches smbCloud deploy code, check whether it is the generic \
135 + deploy path or the Next.js SSR path before proposing changes
136 +- after edits, prefer running the smallest useful verification step first, then \
137 + widen to broader checks if needed
138 +- use git commands naturally for status checks, repo setup, diffs, and normal \
139 + developer workflows when they help move the task forward
140 +
141 +When the repo is not about smbCloud, act like a normal coding agent and do not \
142 +force smbCloud-specific advice into the answer. When it is about smbCloud, be \
143 +specific and practical.
144
145 Be direct and brief. Write clean, idiomatic code. When debugging, go for the \
146 root cause, not the symptom. Correct beats clever.";
src/tools.rs
+209 -72
@@ -14,18 +14,19 @@
14 //!
15 //! # Write Tools
16 //!
17 +//! - `create_directory` — create a directory and any missing parent directories
18 //! - `create_file` — create a new file (fails if it already exists)
19 //! - `edit_file` — replace an exact old-text span with new text in an existing file
20 //! - `delete_file` — delete a file or empty directory at the given path
21 //!
22 //! # Shell Tools
23 //!
23 -//! - `run_command` — run a shell command and return its combined stdout/stderr output
24 +//! - `run_command` — run shell commands, including git porcelain and plumbing commands
25
26 use regex::Regex;
27 use serde_json::{Value, json};
28 use std::fs;
28 -use std::path::Path;
29 +use std::path::{Path, PathBuf};
30 use std::process::Command;
31
32 /// Maximum characters returned from `read_file` before truncation.
@@ -52,8 +53,9 @@ pub fn all_tools() -> Vec<AgentTool> {
53 AgentTool {
54 name: "read_file",
55 description: "Read the contents of a file at the given path. \
55 - Returns the file text, or an error message if the file cannot be read. \
56 - Output is truncated to 10 000 characters.",
56 + Prefer an absolute path when possible. Returns the file text, \
57 + or an error message if the file cannot be read. Output is \
58 + truncated to 10 000 characters.",
59 parameters_schema: json!({
60 "type": "object",
61 "properties": {
@@ -66,11 +68,31 @@ pub fn all_tools() -> Vec<AgentTool> {
68 "additionalProperties": false
69 }),
70 },
71 + AgentTool {
72 + name: "create_directory",
73 + description: "Create a directory at the given path. \
74 + Prefer an absolute path when possible. Missing parent \
75 + directories are created automatically. Use this before \
76 + create_file when the parent path does not exist. Succeeds \
77 + if the directory already exists.",
78 + parameters_schema: json!({
79 + "type": "object",
80 + "properties": {
81 + "path": {
82 + "type": "string",
83 + "description": "Absolute or relative path to the directory to create."
84 + }
85 + },
86 + "required": ["path"],
87 + "additionalProperties": false
88 + }),
89 + },
90 AgentTool {
91 name: "list_directory",
92 description: "List files and directories at the given path. \
72 - Each entry is prefixed with [DIR] or [FILE]. \
73 - Directories are listed first, sorted alphabetically.",
93 + Prefer an absolute path when possible. Each entry is \
94 + prefixed with [DIR] or [FILE]. Directories are listed \
95 + first, sorted alphabetically.",
96 parameters_schema: json!({
97 "type": "object",
98 "properties": {
@@ -86,9 +108,9 @@ pub fn all_tools() -> Vec<AgentTool> {
108 AgentTool {
109 name: "search_files",
110 description: "Search for a regex pattern across files in a directory tree. \
89 - Returns matching lines in `file:line_number: content` format. \
90 - Skips binary files and hidden directories. \
91 - Limited to the first 50 matches.",
111 + Prefer an absolute root path when possible. Returns matching \
112 + lines in `file:line_number: content` format. Skips binary \
113 + files and hidden directories. Limited to the first 50 matches.",
114 parameters_schema: json!({
115 "type": "object",
116 "properties": {
@@ -108,8 +130,9 @@ pub fn all_tools() -> Vec<AgentTool> {
130 AgentTool {
131 name: "create_file",
132 description: "Create a new file at the given path with the provided content. \
111 - Parent directories are created automatically if they do not exist. \
112 - Fails if the file already exists — use edit_file to modify existing files.",
133 + Prefer an absolute path when possible. Parent directories are \
134 + created automatically if they do not exist. Fails if the file \
135 + already exists — use edit_file to modify existing files.",
136 parameters_schema: json!({
137 "type": "object",
138 "properties": {
@@ -129,10 +152,11 @@ pub fn all_tools() -> Vec<AgentTool> {
152 AgentTool {
153 name: "edit_file",
154 description: "Edit an existing file by replacing an exact substring (old_text) with \
132 - new text (new_text). The old_text must appear exactly once in the file. \
133 - Use read_file first to see the current content and identify the exact \
134 - text to replace. To append to a file, match the last few lines as \
135 - old_text and include them plus the new content as new_text.",
155 + new text (new_text). Prefer an absolute path when possible. The \
156 + old_text must appear exactly once in the file. Use read_file first \
157 + to see the current content and identify the exact text to replace. \
158 + To append to a file, match the last few lines as old_text and \
159 + include them plus the new content as new_text.",
160 parameters_schema: json!({
161 "type": "object",
162 "properties": {
@@ -156,7 +180,8 @@ pub fn all_tools() -> Vec<AgentTool> {
180 AgentTool {
181 name: "delete_file",
182 description: "Delete a file or empty directory at the given path. \
159 - Refuses to delete non-empty directories to prevent accidental data loss. \
183 + Prefer an absolute path when possible. Refuses to delete \
184 + non-empty directories to prevent accidental data loss. \
185 Use read_file or list_directory first to confirm the target.",
186 parameters_schema: json!({
187 "type": "object",
@@ -174,16 +199,22 @@ pub fn all_tools() -> Vec<AgentTool> {
199 name: "run_command",
200 description: "Run a shell command and return its combined stdout and stderr output. \
201 The command runs in the given working directory (defaults to \".\"). \
177 - Use this for build tools (cargo, npm, make), version control (git), \
178 - package managers, linters, test runners, and other CLI tasks. \
179 - Commands that run indefinitely (servers, watchers) will be killed \
180 - after 120 seconds.",
202 + Prefer an absolute working directory when possible. Use this for \
203 + build tools (cargo, npm, make), package managers, linters, test \
204 + runners, and git commands, including git init, porcelain commands \
205 + like status/add/commit/checkout, and plumbing commands like \
206 + rev-parse, hash-object, update-ref, and cat-file. If the user asks \
207 + for a new repo or scaffold, it is fine to use this for `git init` \
208 + and normal repo setup steps. In smbCloud repos, prefer existing \
209 + workspace commands, Rails conventions, and deploy flows over \
210 + inventing new command sequences. Commands that run indefinitely \
211 + (servers, watchers) will be killed after 120 seconds.",
212 parameters_schema: json!({
213 "type": "object",
214 "properties": {
215 "command": {
216 "type": "string",
186 - "description": "The shell command to execute (e.g. \"cargo update\", \"git status\")."
217 + "description": "The shell command to execute (e.g. \"cargo update\", \"git status\", \"git rev-parse HEAD\")."
218 },
219 "cwd": {
220 "type": "string",
@@ -208,6 +239,7 @@ pub fn execute_tool(name: &str, arguments: &str) -> String {
239 "read_file" => exec_read_file(arguments),
240 "list_directory" => exec_list_directory(arguments),
241 "search_files" => exec_search_files(arguments),
242 + "create_directory" => exec_create_directory(arguments),
243 "create_file" => exec_create_file(arguments),
244 "edit_file" => exec_edit_file(arguments),
245 "delete_file" => exec_delete_file(arguments),
@@ -216,6 +248,20 @@ pub fn execute_tool(name: &str, arguments: &str) -> String {
248 }
249 }
250
251 +fn absolute_path(path: &Path) -> PathBuf {
252 + if path.is_absolute() {
253 + path.to_path_buf()
254 + } else {
255 + std::env::current_dir()
256 + .unwrap_or_else(|_| PathBuf::from("."))
257 + .join(path)
258 + }
259 +}
260 +
261 +fn absolute_path_string(path: &Path) -> String {
262 + absolute_path(path).display().to_string()
263 +}
264 +
265 // ── read_file ────────────────────────────────────────────────────────────────
266
267 /// Read the contents of a single file, truncating at [`READ_FILE_CHAR_LIMIT`].
@@ -231,16 +277,18 @@ fn exec_read_file(arguments: &str) -> String {
277 };
278
279 let path = Path::new(path_str);
280 + let absolute_path = absolute_path(path);
281 + let absolute_path_str = absolute_path.display().to_string();
282
235 - if !path.exists() {
236 - return format!("Error: path does not exist: {path_str}");
283 + if !absolute_path.exists() {
284 + return format!("Error: path does not exist: {absolute_path_str}");
285 }
286
239 - if !path.is_file() {
240 - return format!("Error: path is not a file: {path_str}");
287 + if !absolute_path.is_file() {
288 + return format!("Error: path is not a file: {absolute_path_str}");
289 }
290
243 - match fs::read_to_string(path) {
291 + match fs::read_to_string(&absolute_path) {
292 Ok(contents) => {
293 if contents.len() > READ_FILE_CHAR_LIMIT {
294 let truncated: String = contents.chars().take(READ_FILE_CHAR_LIMIT).collect();
@@ -271,16 +319,18 @@ fn exec_list_directory(arguments: &str) -> String {
319 };
320
321 let path = Path::new(path_str);
322 + let absolute_path = absolute_path(path);
323 + let absolute_path_str = absolute_path.display().to_string();
324
275 - if !path.exists() {
276 - return format!("Error: path does not exist: {path_str}");
325 + if !absolute_path.exists() {
326 + return format!("Error: path does not exist: {absolute_path_str}");
327 }
328
279 - if !path.is_dir() {
280 - return format!("Error: path is not a directory: {path_str}");
329 + if !absolute_path.is_dir() {
330 + return format!("Error: path is not a directory: {absolute_path_str}");
331 }
332
283 - let entries = match fs::read_dir(path) {
333 + let entries = match fs::read_dir(&absolute_path) {
334 Ok(rd) => rd,
335 Err(err) => return format!("Error: could not read directory: {err}"),
336 };
@@ -318,7 +368,7 @@ fn exec_list_directory(arguments: &str) -> String {
368 dirs.extend(files);
369
370 if dirs.is_empty() {
321 - return format!("(empty directory: {path_str})");
371 + return format!("(empty directory: {absolute_path_str})");
372 }
373
374 dirs.join("\n")
@@ -346,17 +396,19 @@ fn exec_search_files(arguments: &str) -> String {
396 };
397
398 let root = Path::new(root_str);
399 + let absolute_root = absolute_path(root);
400 + let absolute_root_str = absolute_root.display().to_string();
401
350 - if !root.exists() {
351 - return format!("Error: path does not exist: {root_str}");
402 + if !absolute_root.exists() {
403 + return format!("Error: path does not exist: {absolute_root_str}");
404 }
405
354 - if !root.is_dir() {
355 - return format!("Error: path is not a directory: {root_str}");
406 + if !absolute_root.is_dir() {
407 + return format!("Error: path is not a directory: {absolute_root_str}");
408 }
409
410 let mut matches: Vec<String> = Vec::new();
359 - walk_and_search(root, &re, &mut matches);
411 + walk_and_search(&absolute_root, &re, &mut matches);
412
413 if matches.is_empty() {
414 return format!("No matches found for pattern: {pattern_str}");
@@ -423,7 +475,7 @@ fn search_file(path: &Path, re: &Regex, matches: &mut Vec<String>) {
475 Err(_) => return,
476 };
477
426 - let display_path = path.display();
478 + let display_path = absolute_path_string(path);
479
480 for (line_idx, line) in contents.lines().enumerate() {
481 if re.is_match(line) {
@@ -433,7 +485,38 @@ fn search_file(path: &Path, re: &Regex, matches: &mut Vec<String>) {
485 }
486 }
487
436 -// ── create_file ──────────────────────────────────────────────────────────────
488 +/// ── create_directory ─────────────────────────────────────────────────────────
489 +
490 +/// Create a directory and any missing parent directories.
491 +fn exec_create_directory(arguments: &str) -> String {
492 + let args: Value = match serde_json::from_str(arguments) {
493 + Ok(v) => v,
494 + Err(err) => return format!("Error: failed to parse arguments: {err}"),
495 + };
496 +
497 + let path_str = match args.get("path").and_then(Value::as_str) {
498 + Some(p) => p,
499 + None => return "Error: missing required parameter \"path\"".to_string(),
500 + };
501 +
502 + let path = Path::new(path_str);
503 + let absolute_path = absolute_path(path);
504 + let absolute_path_str = absolute_path.display().to_string();
505 +
506 + if absolute_path.exists() {
507 + if absolute_path.is_dir() {
508 + return format!("Directory already exists: {absolute_path_str}");
509 + }
510 + return format!("Error: path exists and is not a directory: {absolute_path_str}");
511 + }
512 +
513 + match fs::create_dir_all(&absolute_path) {
514 + Ok(()) => format!("Created directory: {absolute_path_str}"),
515 + Err(err) => format!("Error: could not create directory: {err}"),
516 + }
517 +}
518 +
519 +/// ── create_file ──────────────────────────────────────────────────────────────
520
521 /// Create a new file with the provided content.
522 ///
@@ -457,15 +540,17 @@ fn exec_create_file(arguments: &str) -> String {
540 };
541
542 let path = Path::new(path_str);
543 + let absolute_path = absolute_path(path);
544 + let absolute_path_str = absolute_path.display().to_string();
545
461 - if path.exists() {
546 + if absolute_path.exists() {
547 return format!(
463 - "Error: file already exists: {path_str} — use edit_file to modify existing files"
548 + "Error: file already exists: {absolute_path_str} — use edit_file to modify existing files"
549 );
550 }
551
552 // Create parent directories if needed.
468 - if let Some(parent) = path.parent()
553 + if let Some(parent) = absolute_path.parent()
554 && !parent.as_os_str().is_empty()
555 && !parent.exists()
556 && let Err(err) = fs::create_dir_all(parent)
@@ -473,8 +558,11 @@ fn exec_create_file(arguments: &str) -> String {
558 return format!("Error: could not create parent directories: {err}");
559 }
560
476 - match fs::write(path, content) {
477 - Ok(()) => format!("Created file: {path_str} ({} bytes)", content.len()),
561 + match fs::write(&absolute_path, content) {
562 + Ok(()) => format!(
563 + "Created file: {absolute_path_str} ({} bytes)",
564 + content.len()
565 + ),
566 Err(err) => format!("Error: could not write file: {err}"),
567 }
568 }
@@ -509,16 +597,20 @@ fn exec_edit_file(arguments: &str) -> String {
597 };
598
599 let path = Path::new(path_str);
600 + let absolute_path = absolute_path(path);
601 + let absolute_path_str = absolute_path.display().to_string();
602
513 - if !path.exists() {
514 - return format!("Error: file does not exist: {path_str} — use create_file for new files");
603 + if !absolute_path.exists() {
604 + return format!(
605 + "Error: file does not exist: {absolute_path_str} — use create_file for new files"
606 + );
607 }
608
517 - if !path.is_file() {
518 - return format!("Error: path is not a file: {path_str}");
609 + if !absolute_path.is_file() {
610 + return format!("Error: path is not a file: {absolute_path_str}");
611 }
612
521 - let contents = match fs::read_to_string(path) {
613 + let contents = match fs::read_to_string(&absolute_path) {
614 Ok(c) => c,
615 Err(err) => return format!("Error: could not read file: {err}"),
616 };
@@ -528,22 +620,25 @@ fn exec_edit_file(arguments: &str) -> String {
620
621 if occurrences == 0 {
622 return format!(
531 - "Error: old_text not found in {path_str}. \
623 + "Error: old_text not found in {absolute_path_str}. \
624 Use read_file to see the current content and copy the exact text to replace."
625 );
626 }
627
628 if occurrences > 1 {
629 return format!(
538 - "Error: old_text appears {occurrences} times in {path_str}. \
630 + "Error: old_text appears {occurrences} times in {absolute_path_str}. \
631 Include more surrounding context in old_text so it matches exactly once."
632 );
633 }
634
635 let updated = contents.replacen(old_text, new_text, 1);
636
545 - match fs::write(path, &updated) {
546 - Ok(()) => format!("Edited file: {path_str} ({} bytes written)", updated.len()),
637 + match fs::write(&absolute_path, &updated) {
638 + Ok(()) => format!(
639 + "Edited file: {absolute_path_str} ({} bytes written)",
640 + updated.len()
641 + ),
642 Err(err) => format!("Error: could not write file: {err}"),
643 }
644 }
@@ -566,22 +661,24 @@ fn exec_delete_file(arguments: &str) -> String {
661 };
662
663 let path = Path::new(path_str);
664 + let absolute_path = absolute_path(path);
665 + let absolute_path_str = absolute_path.display().to_string();
666
570 - if !path.exists() {
571 - return format!("Error: path does not exist: {path_str}");
667 + if !absolute_path.exists() {
668 + return format!("Error: path does not exist: {absolute_path_str}");
669 }
670
574 - if path.is_dir() {
575 - match fs::remove_dir(path) {
576 - Ok(()) => format!("Deleted empty directory: {path_str}"),
671 + if absolute_path.is_dir() {
672 + match fs::remove_dir(&absolute_path) {
673 + Ok(()) => format!("Deleted empty directory: {absolute_path_str}"),
674 Err(err) => format!(
675 "Error: could not delete directory: {err}. \
676 Only empty directories can be deleted."
677 ),
678 }
679 } else {
583 - match fs::remove_file(path) {
584 - Ok(()) => format!("Deleted file: {path_str}"),
680 + match fs::remove_file(&absolute_path) {
681 + Ok(()) => format!("Deleted file: {absolute_path_str}"),
682 Err(err) => format!("Error: could not delete file: {err}"),
683 }
684 }
@@ -613,19 +710,20 @@ fn exec_run_command(arguments: &str) -> String {
710 };
711
712 let cwd = args.get("cwd").and_then(Value::as_str).unwrap_or(".");
713 + let cwd_path = absolute_path(Path::new(cwd));
714 + let cwd_str = cwd_path.display().to_string();
715
617 - let cwd_path = Path::new(cwd);
716 if !cwd_path.exists() {
619 - return format!("Error: working directory does not exist: {cwd}");
717 + return format!("Error: working directory does not exist: {cwd_str}");
718 }
719
622 - log::info!("run_command: `{command_str}` in `{cwd}`");
720 + log::info!("run_command: `{command_str}` in `{cwd_str}`");
721
722 #[cfg(unix)]
723 let mut child = match Command::new("sh")
724 .arg("-c")
725 .arg(command_str)
628 - .current_dir(cwd_path)
726 + .current_dir(&cwd_path)
727 .stdout(std::process::Stdio::piped())
728 .stderr(std::process::Stdio::piped())
729 .spawn()
@@ -638,7 +736,7 @@ fn exec_run_command(arguments: &str) -> String {
736 let mut child = match Command::new("cmd")
737 .arg("/C")
738 .arg(command_str)
641 - .current_dir(cwd_path)
739 + .current_dir(&cwd_path)
740 .stdout(std::process::Stdio::piped())
741 .stderr(std::process::Stdio::piped())
742 .spawn()
@@ -814,14 +912,15 @@ mod tests {
912 #[test]
913 fn test_all_tools_count() {
914 let tools = all_tools();
817 - assert_eq!(tools.len(), 7);
915 + assert_eq!(tools.len(), 8);
916 assert_eq!(tools[0].name, "read_file");
819 - assert_eq!(tools[1].name, "list_directory");
820 - assert_eq!(tools[2].name, "search_files");
821 - assert_eq!(tools[3].name, "create_file");
822 - assert_eq!(tools[4].name, "edit_file");
823 - assert_eq!(tools[5].name, "delete_file");
824 - assert_eq!(tools[6].name, "run_command");
917 + assert_eq!(tools[1].name, "create_directory");
918 + assert_eq!(tools[2].name, "list_directory");
919 + assert_eq!(tools[3].name, "search_files");
920 + assert_eq!(tools[4].name, "create_file");
921 + assert_eq!(tools[5].name, "edit_file");
922 + assert_eq!(tools[6].name, "delete_file");
923 + assert_eq!(tools[7].name, "run_command");
924 }
925
926 #[test]
@@ -839,6 +938,44 @@ mod tests {
938 }
939 }
940
941 + // ── create_directory tests ───────────────────────────────────────────
942 +
943 + #[test]
944 + fn test_create_directory_missing_path() {
945 + let result = exec_create_directory("{}");
946 + assert!(result.contains("missing required parameter"));
947 + }
948 +
949 + #[test]
950 + fn test_create_directory_success() {
951 + let dir = std::env::temp_dir()
952 + .join("sigit_test_create_directory")
953 + .join("nested")
954 + .join("child");
955 + let _ = fs::remove_dir_all(dir.parent().unwrap());
956 +
957 + let args = serde_json::json!({ "path": dir }).to_string();
958 + let result = exec_create_directory(&args);
959 + assert!(result.starts_with("Created directory:"), "got: {result}");
960 + assert!(dir.exists());
961 + assert!(dir.is_dir());
962 +
963 + let _ = fs::remove_dir_all(dir.parent().unwrap().parent().unwrap());
964 + }
965 +
966 + #[test]
967 + fn test_create_directory_already_exists() {
968 + let dir = std::env::temp_dir().join("sigit_test_create_directory_exists");
969 + let _ = fs::remove_dir_all(&dir);
970 + fs::create_dir_all(&dir).unwrap();
971 +
972 + let args = serde_json::json!({ "path": dir }).to_string();
973 + let result = exec_create_directory(&args);
974 + assert!(result.contains("Directory already exists"), "got: {result}");
975 +
976 + let _ = fs::remove_dir_all(&dir);
977 + }
978 +
979 // ── create_file tests ────────────────────────────────────────────────
980
981 #[test]