grok-cli-acp 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.env.example +42 -0
  2. package/.github/workflows/ci.yml +30 -0
  3. package/.github/workflows/rust.yml +22 -0
  4. package/.grok/.env.example +85 -0
  5. package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
  6. package/.grok/ENV_CONFIG_GUIDE.md +173 -0
  7. package/.grok/QUICK_REFERENCE.md +180 -0
  8. package/.grok/README.md +104 -0
  9. package/.grok/TESTING_GUIDE.md +393 -0
  10. package/CHANGELOG.md +465 -0
  11. package/CODE_REVIEW_SUMMARY.md +414 -0
  12. package/COMPLETE_FIX_SUMMARY.md +415 -0
  13. package/CONFIGURATION.md +489 -0
  14. package/CONTEXT_FILES_GUIDE.md +419 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/CURSOR_POSITION_FIX.md +206 -0
  17. package/Cargo.toml +88 -0
  18. package/ERROR_HANDLING_REPORT.md +361 -0
  19. package/FINAL_FIX_SUMMARY.md +462 -0
  20. package/FIXES.md +37 -0
  21. package/FIXES_SUMMARY.md +87 -0
  22. package/GROK_API_MIGRATION_SUMMARY.md +111 -0
  23. package/LICENSE +22 -0
  24. package/MIGRATION_TO_GROK_API.md +223 -0
  25. package/README.md +504 -0
  26. package/REVIEW_COMPLETE.md +416 -0
  27. package/REVIEW_QUICK_REFERENCE.md +173 -0
  28. package/SECURITY.md +463 -0
  29. package/SECURITY_AUDIT.md +661 -0
  30. package/SETUP.md +287 -0
  31. package/TESTING_TOOLS.md +88 -0
  32. package/TESTING_TOOL_EXECUTION.md +239 -0
  33. package/TOOL_EXECUTION_FIX.md +491 -0
  34. package/VERIFICATION_CHECKLIST.md +419 -0
  35. package/docs/API.md +74 -0
  36. package/docs/CHAT_LOGGING.md +39 -0
  37. package/docs/CURSOR_FIX_DEMO.md +306 -0
  38. package/docs/ERROR_HANDLING_GUIDE.md +547 -0
  39. package/docs/FILE_OPERATIONS.md +449 -0
  40. package/docs/INTERACTIVE.md +401 -0
  41. package/docs/PROJECT_CREATION_GUIDE.md +570 -0
  42. package/docs/QUICKSTART.md +378 -0
  43. package/docs/QUICK_REFERENCE.md +691 -0
  44. package/docs/RELEASE_NOTES_0.1.2.md +240 -0
  45. package/docs/TOOLS.md +459 -0
  46. package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
  47. package/docs/ZED_INTEGRATION.md +371 -0
  48. package/docs/extensions.md +464 -0
  49. package/docs/settings.md +293 -0
  50. package/examples/extensions/logging-hook/README.md +91 -0
  51. package/examples/extensions/logging-hook/extension.json +22 -0
  52. package/package.json +30 -0
  53. package/scripts/test_acp.py +252 -0
  54. package/scripts/test_acp.sh +143 -0
  55. package/scripts/test_acp_simple.sh +72 -0
  56. package/src/acp/mod.rs +741 -0
  57. package/src/acp/protocol.rs +323 -0
  58. package/src/acp/security.rs +298 -0
  59. package/src/acp/tools.rs +697 -0
  60. package/src/bin/banner_demo.rs +216 -0
  61. package/src/bin/docgen.rs +18 -0
  62. package/src/bin/installer.rs +217 -0
  63. package/src/cli/app.rs +310 -0
  64. package/src/cli/commands/acp.rs +721 -0
  65. package/src/cli/commands/chat.rs +485 -0
  66. package/src/cli/commands/code.rs +513 -0
  67. package/src/cli/commands/config.rs +394 -0
  68. package/src/cli/commands/health.rs +442 -0
  69. package/src/cli/commands/history.rs +421 -0
  70. package/src/cli/commands/mod.rs +14 -0
  71. package/src/cli/commands/settings.rs +1384 -0
  72. package/src/cli/mod.rs +166 -0
  73. package/src/config/mod.rs +2212 -0
  74. package/src/display/ascii_art.rs +139 -0
  75. package/src/display/banner.rs +289 -0
  76. package/src/display/components/input.rs +323 -0
  77. package/src/display/components/mod.rs +2 -0
  78. package/src/display/components/settings_list.rs +306 -0
  79. package/src/display/interactive.rs +1255 -0
  80. package/src/display/mod.rs +62 -0
  81. package/src/display/terminal.rs +42 -0
  82. package/src/display/tips.rs +316 -0
  83. package/src/grok_client_ext.rs +177 -0
  84. package/src/hooks/loader.rs +407 -0
  85. package/src/hooks/mod.rs +158 -0
  86. package/src/lib.rs +174 -0
  87. package/src/main.rs +65 -0
  88. package/src/mcp/client.rs +195 -0
  89. package/src/mcp/config.rs +20 -0
  90. package/src/mcp/mod.rs +6 -0
  91. package/src/mcp/protocol.rs +67 -0
  92. package/src/utils/auth.rs +41 -0
  93. package/src/utils/chat_logger.rs +568 -0
  94. package/src/utils/context.rs +390 -0
  95. package/src/utils/mod.rs +16 -0
  96. package/src/utils/network.rs +320 -0
  97. package/src/utils/rate_limiter.rs +166 -0
  98. package/src/utils/session.rs +73 -0
  99. package/src/utils/shell_permissions.rs +389 -0
  100. package/src/utils/telemetry.rs +41 -0
@@ -0,0 +1,697 @@
1
+ use crate::acp::security::SecurityPolicy;
2
+ use anyhow::{Result, anyhow};
3
+ use glob::glob;
4
+ use regex::Regex;
5
+ use serde_json::{Value, json};
6
+ use std::fs::{self, File};
7
+ use std::io::{BufRead, BufReader, Write};
8
+ use std::path::Path;
9
+ use std::process::Command;
10
+
11
+ /// Read file content
12
+ pub fn read_file(path: &str, security: &SecurityPolicy) -> Result<String> {
13
+ // Resolve path to absolute canonical form
14
+ let resolved_path = security
15
+ .resolve_path(path)
16
+ .map_err(|e| anyhow!("Failed to resolve path '{}': {}", path, e))?;
17
+
18
+ // Check trust on resolved path
19
+ if !security.is_path_trusted(&resolved_path) {
20
+ return Err(anyhow!("Access denied: Path is not in a trusted directory"));
21
+ }
22
+
23
+ if !resolved_path.exists() {
24
+ return Err(anyhow!("File not found: {}", resolved_path.display()));
25
+ }
26
+
27
+ fs::read_to_string(&resolved_path).map_err(|e| anyhow!("Failed to read file: {}", e))
28
+ }
29
+
30
+ /// Write content to file
31
+ pub fn write_file(path: &str, content: &str, security: &SecurityPolicy) -> Result<String> {
32
+ // Convert to absolute path first (without canonicalizing yet)
33
+ let path_ref = Path::new(path);
34
+ let absolute_path = if path_ref.is_absolute() {
35
+ path_ref.to_path_buf()
36
+ } else {
37
+ security.working_directory().join(path_ref)
38
+ };
39
+
40
+ // Create parent directories first if they don't exist
41
+ if let Some(parent) = absolute_path.parent() {
42
+ fs::create_dir_all(parent).map_err(|e| anyhow!("Failed to create directory: {}", e))?;
43
+ }
44
+
45
+ // Now resolve to canonical form (after directories exist)
46
+ let resolved_path = security
47
+ .resolve_path(path)
48
+ .map_err(|e| anyhow!("Failed to resolve path '{}': {}", path, e))?;
49
+
50
+ // Check trust on resolved path
51
+ if !security.is_path_trusted(&resolved_path) {
52
+ return Err(anyhow!("Access denied: Path is not in a trusted directory"));
53
+ }
54
+
55
+ fs::write(&resolved_path, content).map_err(|e| anyhow!("Failed to write file: {}", e))?;
56
+ Ok(format!("Successfully wrote to {}", resolved_path.display()))
57
+ }
58
+
59
+ /// Replace text in a file
60
+ pub fn replace(
61
+ path: &str,
62
+ old_string: &str,
63
+ new_string: &str,
64
+ expected_replacements: Option<u32>,
65
+ security: &SecurityPolicy,
66
+ ) -> Result<String> {
67
+ // Resolve path to absolute canonical form
68
+ let resolved_path = security
69
+ .resolve_path(path)
70
+ .map_err(|e| anyhow!("Failed to resolve path '{}': {}", path, e))?;
71
+
72
+ // Check trust on resolved path
73
+ if !security.is_path_trusted(&resolved_path) {
74
+ return Err(anyhow!("Access denied: Path is not in a trusted directory"));
75
+ }
76
+
77
+ if !resolved_path.exists() {
78
+ return Err(anyhow!("File not found: {}", resolved_path.display()));
79
+ }
80
+
81
+ let content =
82
+ fs::read_to_string(&resolved_path).map_err(|e| anyhow!("Failed to read file: {}", e))?;
83
+
84
+ let occurrences = content.matches(old_string).count();
85
+ if occurrences == 0 {
86
+ return Err(anyhow!(
87
+ "Failed to replace: '{}' not found in file. Use read_file to verify content.",
88
+ old_string
89
+ ));
90
+ }
91
+
92
+ if let Some(expected) = expected_replacements
93
+ && occurrences != expected as usize
94
+ {
95
+ return Err(anyhow!(
96
+ "Failed to replace: Expected {} occurrences, but found {}.",
97
+ expected,
98
+ occurrences
99
+ ));
100
+ }
101
+
102
+ let new_content = content.replace(old_string, new_string);
103
+ fs::write(&resolved_path, new_content).map_err(|e| anyhow!("Failed to write file: {}", e))?;
104
+
105
+ Ok(format!(
106
+ "Successfully replaced {} occurrence(s) in {}",
107
+ occurrences,
108
+ resolved_path.display()
109
+ ))
110
+ }
111
+
112
+ /// Save a fact to long-term memory
113
+ pub fn save_memory(fact: &str) -> Result<String> {
114
+ let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
115
+ let grok_dir = home_dir.join(".grok");
116
+
117
+ if !grok_dir.exists() {
118
+ fs::create_dir_all(&grok_dir)
119
+ .map_err(|e| anyhow!("Failed to create .grok directory: {}", e))?;
120
+ }
121
+
122
+ let memory_file = grok_dir.join("memory.md");
123
+ let mut file = fs::OpenOptions::new()
124
+ .create(true)
125
+ .append(true)
126
+ .open(&memory_file)
127
+ .map_err(|e| anyhow!("Failed to open memory file: {}", e))?;
128
+
129
+ writeln!(file, "- {}", fact).map_err(|e| anyhow!("Failed to write to memory file: {}", e))?;
130
+
131
+ Ok("Fact saved to memory.".to_string())
132
+ }
133
+
134
+ /// List directory contents
135
+ /// List files in a directory
136
+ pub fn list_directory(path: &str, security: &SecurityPolicy) -> Result<String> {
137
+ // Resolve path to absolute canonical form
138
+ let resolved_path = security
139
+ .resolve_path(path)
140
+ .map_err(|e| anyhow!("Failed to resolve path '{}': {}", path, e))?;
141
+
142
+ // Check trust on resolved path
143
+ if !security.is_path_trusted(&resolved_path) {
144
+ return Err(anyhow!("Access denied: Path is not in a trusted directory"));
145
+ }
146
+
147
+ if !resolved_path.exists() {
148
+ return Err(anyhow!("Directory not found: {}", resolved_path.display()));
149
+ }
150
+
151
+ if !resolved_path.is_dir() {
152
+ return Err(anyhow!(
153
+ "Path is not a directory: {}",
154
+ resolved_path.display()
155
+ ));
156
+ }
157
+
158
+ let mut entries = Vec::new();
159
+ for entry in
160
+ fs::read_dir(&resolved_path).map_err(|e| anyhow!("Failed to read directory: {}", e))?
161
+ {
162
+ let entry = entry?;
163
+ let path = entry.path();
164
+ let name = path.file_name().unwrap_or_default().to_string_lossy();
165
+ let is_dir = path.is_dir();
166
+ entries.push(format!("{}{}", name, if is_dir { "/" } else { "" }));
167
+ }
168
+
169
+ Ok(entries.join("\n"))
170
+ }
171
+
172
+ /// Find files using glob pattern
173
+ pub fn glob_search(pattern: &str, security: &SecurityPolicy) -> Result<String> {
174
+ // Glob patterns might traverse anywhere, so we need to filter results
175
+ // based on security policy.
176
+ let mut matches = Vec::new();
177
+ for entry in glob(pattern).map_err(|e| anyhow!("Failed to read glob pattern: {}", e))? {
178
+ match entry {
179
+ Ok(path) => {
180
+ if security.is_path_trusted(&path) {
181
+ matches.push(path.display().to_string());
182
+ }
183
+ }
184
+ Err(e) => return Err(anyhow!("Error matching glob: {}", e)),
185
+ }
186
+ }
187
+
188
+ if matches.is_empty() {
189
+ Ok("No files found matching pattern".to_string())
190
+ } else {
191
+ Ok(matches.join("\n"))
192
+ }
193
+ }
194
+
195
+ /// Search file content using regex (grep-like)
196
+ pub fn search_file_content(path: &str, pattern: &str, security: &SecurityPolicy) -> Result<String> {
197
+ // Resolve path to absolute canonical form
198
+ let resolved_path = security
199
+ .resolve_path(path)
200
+ .map_err(|e| anyhow!("Failed to resolve path '{}': {}", path, e))?;
201
+
202
+ // Check trust on resolved path
203
+ if !security.is_path_trusted(&resolved_path) {
204
+ return Err(anyhow!("Access denied: Path is not in a trusted directory"));
205
+ }
206
+
207
+ let re = Regex::new(pattern).map_err(|e| anyhow!("Invalid regex pattern: {}", e))?;
208
+
209
+ if resolved_path.is_dir() {
210
+ // Simple recursive search if directory
211
+ let mut results = Vec::new();
212
+ for entry in walkdir::WalkDir::new(&resolved_path) {
213
+ let entry = entry.map_err(|e| anyhow!("Error walking directory: {}", e))?;
214
+ if entry.file_type().is_file() {
215
+ let entry_path = entry.path();
216
+ if !security.is_path_trusted(entry_path) {
217
+ continue;
218
+ }
219
+
220
+ let file =
221
+ File::open(entry_path).map_err(|e| anyhow!("Failed to open file: {}", e))?;
222
+ let reader = BufReader::new(file);
223
+
224
+ for (i, line) in reader.lines().enumerate() {
225
+ match line {
226
+ Ok(line) => {
227
+ if re.is_match(&line) {
228
+ results.push(format!(
229
+ "{}:{}: {}",
230
+ entry_path.display(),
231
+ i + 1,
232
+ line
233
+ ));
234
+ }
235
+ }
236
+ Err(_) => continue, // Skip binary or invalid UTF-8 files
237
+ }
238
+ }
239
+ }
240
+ }
241
+ if results.is_empty() {
242
+ Ok("No matches found".to_string())
243
+ } else {
244
+ Ok(results.join("\n"))
245
+ }
246
+ } else {
247
+ // Single file search
248
+ let file = File::open(&resolved_path).map_err(|e| anyhow!("Failed to open file: {}", e))?;
249
+ let reader = BufReader::new(file);
250
+ let mut results = Vec::new();
251
+
252
+ for (i, line) in reader.lines().enumerate() {
253
+ match line {
254
+ Ok(line) => {
255
+ if re.is_match(&line) {
256
+ results.push(format!("{}:{}: {}", resolved_path.display(), i + 1, line));
257
+ }
258
+ }
259
+ Err(_) => continue,
260
+ }
261
+ }
262
+ if results.is_empty() {
263
+ Ok("No matches found".to_string())
264
+ } else {
265
+ Ok(results.join("\n"))
266
+ }
267
+ }
268
+ }
269
+
270
+ /// Run shell command
271
+ pub fn run_shell_command(command: &str, security: &SecurityPolicy) -> Result<String> {
272
+ security.validate_shell_command(command)?;
273
+
274
+ // Check if we are in a trusted directory (implicit in policy check usually,
275
+ // but here we might want to check CWD if not done yet)
276
+ // For now, simple execution
277
+
278
+ if cfg!(target_os = "windows") {
279
+ // Convert bash-style && to PowerShell-style ; for command chaining
280
+ let powershell_command = command.replace(" && ", "; ");
281
+
282
+ let output = Command::new("powershell")
283
+ .args(["-Command", &powershell_command])
284
+ .output()
285
+ .map_err(|e| anyhow!("Failed to execute command: {}", e))?;
286
+
287
+ let stdout = String::from_utf8_lossy(&output.stdout);
288
+ let stderr = String::from_utf8_lossy(&output.stderr);
289
+
290
+ if !output.status.success() {
291
+ Ok(format!(
292
+ "Command failed with code {}:\nStdout: {}\nStderr: {}",
293
+ output.status, stdout, stderr
294
+ ))
295
+ } else {
296
+ Ok(format!("Stdout: {}\nStderr: {}", stdout, stderr))
297
+ }
298
+ } else {
299
+ let output = Command::new("sh")
300
+ .arg("-c")
301
+ .arg(command)
302
+ .output()
303
+ .map_err(|e| anyhow!("Failed to execute command: {}", e))?;
304
+
305
+ let stdout = String::from_utf8_lossy(&output.stdout);
306
+ let stderr = String::from_utf8_lossy(&output.stderr);
307
+
308
+ if !output.status.success() {
309
+ Ok(format!(
310
+ "Command failed with code {}:\nStdout: {}\nStderr: {}",
311
+ output.status, stdout, stderr
312
+ ))
313
+ } else {
314
+ Ok(format!("Stdout: {}\nStderr: {}", stdout, stderr))
315
+ }
316
+ }
317
+ }
318
+
319
+ /// Perform a web search using Google Custom Search API
320
+ pub async fn web_search(query: &str) -> Result<String> {
321
+ let api_key = std::env::var("GOOGLE_API_KEY")
322
+ .map_err(|_| anyhow!("GOOGLE_API_KEY environment variable not set"))?;
323
+ let cx = std::env::var("GOOGLE_CX")
324
+ .map_err(|_| anyhow!("GOOGLE_CX environment variable not set"))?;
325
+
326
+ let url = format!(
327
+ "https://www.googleapis.com/customsearch/v1?key={}&cx={}&q={}",
328
+ api_key,
329
+ cx,
330
+ urlencoding::encode(query)
331
+ );
332
+
333
+ let client = reqwest::Client::new();
334
+ let response = client.get(&url).send().await?;
335
+
336
+ if !response.status().is_success() {
337
+ return Err(anyhow!("Search request failed: {}", response.status()));
338
+ }
339
+
340
+ let json: Value = response.json().await?;
341
+
342
+ let mut results = Vec::new();
343
+ if let Some(items) = json.get("items").and_then(|i| i.as_array()) {
344
+ for item in items {
345
+ let title = item
346
+ .get("title")
347
+ .and_then(|t| t.as_str())
348
+ .unwrap_or("No title");
349
+ let link = item
350
+ .get("link")
351
+ .and_then(|l| l.as_str())
352
+ .unwrap_or("No link");
353
+ let snippet = item.get("snippet").and_then(|s| s.as_str()).unwrap_or("");
354
+ results.push(format!(
355
+ "Title: {}\nLink: {}\nSnippet: {}\n",
356
+ title, link, snippet
357
+ ));
358
+ }
359
+ }
360
+
361
+ if results.is_empty() {
362
+ Ok("No results found".to_string())
363
+ } else {
364
+ Ok(results.join("\n---\n"))
365
+ }
366
+ }
367
+
368
+ /// Fetch content from a URL
369
+ pub async fn web_fetch(url: &str) -> Result<String> {
370
+ let client = reqwest::Client::new();
371
+ let response = client
372
+ .get(url)
373
+ .header("User-Agent", "grok-cli/0.1.0")
374
+ .send()
375
+ .await?;
376
+
377
+ if !response.status().is_success() {
378
+ return Err(anyhow!("Failed to fetch URL: {}", response.status()));
379
+ }
380
+
381
+ let text = response.text().await?;
382
+ // Basic cleanup: take first 10000 chars to avoid overloading context
383
+ let truncated = if text.len() > 10000 {
384
+ format!("{}... (truncated)", &text[..10000])
385
+ } else {
386
+ text
387
+ };
388
+
389
+ Ok(truncated)
390
+ }
391
+
392
+ /// Get tool definitions for the LLM
393
+ pub fn get_tool_definitions() -> Vec<Value> {
394
+ vec![
395
+ json!({
396
+ "type": "function",
397
+ "function": {
398
+ "name": "read_file",
399
+ "description": "Read the content of a file",
400
+ "parameters": {
401
+ "type": "object",
402
+ "properties": {
403
+ "path": {
404
+ "type": "string",
405
+ "description": "The path to the file to read"
406
+ }
407
+ },
408
+ "required": ["path"]
409
+ }
410
+ }
411
+ }),
412
+ json!({
413
+ "type": "function",
414
+ "function": {
415
+ "name": "write_file",
416
+ "description": "Write content to a file",
417
+ "parameters": {
418
+ "type": "object",
419
+ "properties": {
420
+ "path": {
421
+ "type": "string",
422
+ "description": "The path to the file to write"
423
+ },
424
+ "content": {
425
+ "type": "string",
426
+ "description": "The content to write"
427
+ }
428
+ },
429
+ "required": ["path", "content"]
430
+ }
431
+ }
432
+ }),
433
+ json!({
434
+ "type": "function",
435
+ "function": {
436
+ "name": "replace",
437
+ "description": "Replace text in a file",
438
+ "parameters": {
439
+ "type": "object",
440
+ "properties": {
441
+ "path": {
442
+ "type": "string",
443
+ "description": "The path to the file to modify"
444
+ },
445
+ "old_string": {
446
+ "type": "string",
447
+ "description": "The string to be replaced"
448
+ },
449
+ "new_string": {
450
+ "type": "string",
451
+ "description": "The new string to replace with"
452
+ },
453
+ "expected_replacements": {
454
+ "type": "integer",
455
+ "description": "Expected number of replacements (optional)"
456
+ }
457
+ },
458
+ "required": ["path", "old_string", "new_string"]
459
+ }
460
+ }
461
+ }),
462
+ json!({
463
+ "type": "function",
464
+ "function": {
465
+ "name": "save_memory",
466
+ "description": "Save a fact to long-term memory",
467
+ "parameters": {
468
+ "type": "object",
469
+ "properties": {
470
+ "fact": {
471
+ "type": "string",
472
+ "description": "The fact to remember"
473
+ }
474
+ },
475
+ "required": ["fact"]
476
+ }
477
+ }
478
+ }),
479
+ json!({
480
+ "type": "function",
481
+ "function": {
482
+ "name": "list_directory",
483
+ "description": "List files and directories in a path",
484
+ "parameters": {
485
+ "type": "object",
486
+ "properties": {
487
+ "path": {
488
+ "type": "string",
489
+ "description": "The directory path to list"
490
+ }
491
+ },
492
+ "required": ["path"]
493
+ }
494
+ }
495
+ }),
496
+ json!({
497
+ "type": "function",
498
+ "function": {
499
+ "name": "glob_search",
500
+ "description": "Find files matching a glob pattern",
501
+ "parameters": {
502
+ "type": "object",
503
+ "properties": {
504
+ "pattern": {
505
+ "type": "string",
506
+ "description": "The glob pattern to match (e.g. **/*.rs)"
507
+ }
508
+ },
509
+ "required": ["pattern"]
510
+ }
511
+ }
512
+ }),
513
+ json!({
514
+ "type": "function",
515
+ "function": {
516
+ "name": "search_file_content",
517
+ "description": "Search for text patterns in files using regex",
518
+ "parameters": {
519
+ "type": "object",
520
+ "properties": {
521
+ "path": {
522
+ "type": "string",
523
+ "description": "The file or directory to search in"
524
+ },
525
+ "pattern": {
526
+ "type": "string",
527
+ "description": "The regex pattern to search for"
528
+ }
529
+ },
530
+ "required": ["path", "pattern"]
531
+ }
532
+ }
533
+ }),
534
+ json!({
535
+ "type": "function",
536
+ "function": {
537
+ "name": "run_shell_command",
538
+ "description": "Execute a shell command",
539
+ "parameters": {
540
+ "type": "object",
541
+ "properties": {
542
+ "command": {
543
+ "type": "string",
544
+ "description": "The command to execute"
545
+ }
546
+ },
547
+ "required": ["command"]
548
+ }
549
+ }
550
+ }),
551
+ json!({
552
+ "type": "function",
553
+ "function": {
554
+ "name": "web_search",
555
+ "description": "Search the web using Google Custom Search",
556
+ "parameters": {
557
+ "type": "object",
558
+ "properties": {
559
+ "query": {
560
+ "type": "string",
561
+ "description": "The search query"
562
+ }
563
+ },
564
+ "required": ["query"]
565
+ }
566
+ }
567
+ }),
568
+ json!({
569
+ "type": "function",
570
+ "function": {
571
+ "name": "web_fetch",
572
+ "description": "Fetch content from a URL",
573
+ "parameters": {
574
+ "type": "object",
575
+ "properties": {
576
+ "url": {
577
+ "type": "string",
578
+ "description": "The URL to fetch"
579
+ }
580
+ },
581
+ "required": ["url"]
582
+ }
583
+ }
584
+ }),
585
+ ]
586
+ }
587
+
588
+ #[cfg(test)]
589
+ mod tests {
590
+ use super::*;
591
+ use crate::acp::security::SecurityPolicy;
592
+ use std::fs;
593
+ use tempfile::TempDir;
594
+
595
+ #[test]
596
+ fn test_file_operations() {
597
+ let temp_dir = TempDir::new().unwrap();
598
+ let file_path = temp_dir.path().join("test.txt");
599
+ let path_str = file_path.to_str().unwrap();
600
+
601
+ let mut security = SecurityPolicy::new();
602
+ security.add_trusted_directory(temp_dir.path());
603
+
604
+ // Test write_file
605
+ let write_result = write_file(path_str, "Hello, world!", &security);
606
+ assert!(write_result.is_ok());
607
+
608
+ // Test read_file
609
+ let read_result = read_file(path_str, &security);
610
+ assert!(read_result.is_ok());
611
+ assert_eq!(read_result.unwrap(), "Hello, world!");
612
+
613
+ // Test list_directory
614
+ let list_result = list_directory(temp_dir.path().to_str().unwrap(), &security);
615
+ assert!(list_result.is_ok());
616
+ assert!(list_result.unwrap().contains("test.txt"));
617
+ }
618
+
619
+ #[test]
620
+ fn test_glob_search() {
621
+ let temp_dir = TempDir::new().unwrap();
622
+ let file1 = temp_dir.path().join("file1.txt");
623
+ let file2 = temp_dir.path().join("file2.rs");
624
+ fs::write(&file1, "content1").unwrap();
625
+ fs::write(&file2, "content2").unwrap();
626
+
627
+ let mut security = SecurityPolicy::new();
628
+ security.add_trusted_directory(temp_dir.path());
629
+
630
+ let pattern = temp_dir.path().join("*.txt");
631
+ let result = glob_search(pattern.to_str().unwrap(), &security);
632
+ assert!(result.is_ok());
633
+ assert!(result.unwrap().contains("file1.txt"));
634
+ }
635
+
636
+ #[test]
637
+ fn test_search_content() {
638
+ let temp_dir = TempDir::new().unwrap();
639
+ let file1 = temp_dir.path().join("test_grep.txt");
640
+ fs::write(&file1, "hello world\nfoo bar\nhello rust").unwrap();
641
+
642
+ let mut security = SecurityPolicy::new();
643
+ security.add_trusted_directory(temp_dir.path());
644
+
645
+ let result = search_file_content(file1.to_str().unwrap(), "hello", &security);
646
+ assert!(result.is_ok());
647
+ let output = result.unwrap();
648
+ assert!(output.contains("1: hello world"));
649
+ assert!(output.contains("3: hello rust"));
650
+ }
651
+
652
+ #[test]
653
+ fn test_replace() {
654
+ let temp_dir = TempDir::new().unwrap();
655
+ let file_path = temp_dir.path().join("replace.txt");
656
+ let path_str = file_path.to_str().unwrap();
657
+
658
+ let mut security = SecurityPolicy::new();
659
+ security.add_trusted_directory(temp_dir.path());
660
+
661
+ fs::write(&file_path, "Hello world, hello universe").unwrap();
662
+
663
+ // Test successful replace
664
+ let result = replace(path_str, "hello", "hi", None, &security);
665
+ assert!(result.is_ok());
666
+ let content = fs::read_to_string(&file_path).unwrap();
667
+ assert_eq!(content, "Hello world, hi universe");
668
+
669
+ // Test replace with expected count
670
+ let result = replace(path_str, "universe", "cosmos", Some(1), &security);
671
+ assert!(result.is_ok());
672
+ let content = fs::read_to_string(&file_path).unwrap();
673
+ assert_eq!(content, "Hello world, hi cosmos");
674
+
675
+ // Test replace not found
676
+ let result = replace(path_str, "missing", "nothing", None, &security);
677
+ assert!(result.is_err());
678
+ assert!(result.unwrap_err().to_string().contains("not found"));
679
+
680
+ // Test replace count mismatch
681
+ let result = replace(path_str, "hi", "hey", Some(5), &security);
682
+ assert!(result.is_err());
683
+ assert!(
684
+ result
685
+ .unwrap_err()
686
+ .to_string()
687
+ .contains("Expected 5 occurrences")
688
+ );
689
+ }
690
+
691
+ #[test]
692
+ fn test_get_tool_definitions_updated() {
693
+ let tools = get_tool_definitions();
694
+ assert!(tools.iter().any(|t| t["function"]["name"] == "replace"));
695
+ assert!(tools.iter().any(|t| t["function"]["name"] == "save_memory"));
696
+ }
697
+ }