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]