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.
- package/.env.example +42 -0
- package/.github/workflows/ci.yml +30 -0
- package/.github/workflows/rust.yml +22 -0
- package/.grok/.env.example +85 -0
- package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
- package/.grok/ENV_CONFIG_GUIDE.md +173 -0
- package/.grok/QUICK_REFERENCE.md +180 -0
- package/.grok/README.md +104 -0
- package/.grok/TESTING_GUIDE.md +393 -0
- package/CHANGELOG.md +465 -0
- package/CODE_REVIEW_SUMMARY.md +414 -0
- package/COMPLETE_FIX_SUMMARY.md +415 -0
- package/CONFIGURATION.md +489 -0
- package/CONTEXT_FILES_GUIDE.md +419 -0
- package/CONTRIBUTING.md +55 -0
- package/CURSOR_POSITION_FIX.md +206 -0
- package/Cargo.toml +88 -0
- package/ERROR_HANDLING_REPORT.md +361 -0
- package/FINAL_FIX_SUMMARY.md +462 -0
- package/FIXES.md +37 -0
- package/FIXES_SUMMARY.md +87 -0
- package/GROK_API_MIGRATION_SUMMARY.md +111 -0
- package/LICENSE +22 -0
- package/MIGRATION_TO_GROK_API.md +223 -0
- package/README.md +504 -0
- package/REVIEW_COMPLETE.md +416 -0
- package/REVIEW_QUICK_REFERENCE.md +173 -0
- package/SECURITY.md +463 -0
- package/SECURITY_AUDIT.md +661 -0
- package/SETUP.md +287 -0
- package/TESTING_TOOLS.md +88 -0
- package/TESTING_TOOL_EXECUTION.md +239 -0
- package/TOOL_EXECUTION_FIX.md +491 -0
- package/VERIFICATION_CHECKLIST.md +419 -0
- package/docs/API.md +74 -0
- package/docs/CHAT_LOGGING.md +39 -0
- package/docs/CURSOR_FIX_DEMO.md +306 -0
- package/docs/ERROR_HANDLING_GUIDE.md +547 -0
- package/docs/FILE_OPERATIONS.md +449 -0
- package/docs/INTERACTIVE.md +401 -0
- package/docs/PROJECT_CREATION_GUIDE.md +570 -0
- package/docs/QUICKSTART.md +378 -0
- package/docs/QUICK_REFERENCE.md +691 -0
- package/docs/RELEASE_NOTES_0.1.2.md +240 -0
- package/docs/TOOLS.md +459 -0
- package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
- package/docs/ZED_INTEGRATION.md +371 -0
- package/docs/extensions.md +464 -0
- package/docs/settings.md +293 -0
- package/examples/extensions/logging-hook/README.md +91 -0
- package/examples/extensions/logging-hook/extension.json +22 -0
- package/package.json +30 -0
- package/scripts/test_acp.py +252 -0
- package/scripts/test_acp.sh +143 -0
- package/scripts/test_acp_simple.sh +72 -0
- package/src/acp/mod.rs +741 -0
- package/src/acp/protocol.rs +323 -0
- package/src/acp/security.rs +298 -0
- package/src/acp/tools.rs +697 -0
- package/src/bin/banner_demo.rs +216 -0
- package/src/bin/docgen.rs +18 -0
- package/src/bin/installer.rs +217 -0
- package/src/cli/app.rs +310 -0
- package/src/cli/commands/acp.rs +721 -0
- package/src/cli/commands/chat.rs +485 -0
- package/src/cli/commands/code.rs +513 -0
- package/src/cli/commands/config.rs +394 -0
- package/src/cli/commands/health.rs +442 -0
- package/src/cli/commands/history.rs +421 -0
- package/src/cli/commands/mod.rs +14 -0
- package/src/cli/commands/settings.rs +1384 -0
- package/src/cli/mod.rs +166 -0
- package/src/config/mod.rs +2212 -0
- package/src/display/ascii_art.rs +139 -0
- package/src/display/banner.rs +289 -0
- package/src/display/components/input.rs +323 -0
- package/src/display/components/mod.rs +2 -0
- package/src/display/components/settings_list.rs +306 -0
- package/src/display/interactive.rs +1255 -0
- package/src/display/mod.rs +62 -0
- package/src/display/terminal.rs +42 -0
- package/src/display/tips.rs +316 -0
- package/src/grok_client_ext.rs +177 -0
- package/src/hooks/loader.rs +407 -0
- package/src/hooks/mod.rs +158 -0
- package/src/lib.rs +174 -0
- package/src/main.rs +65 -0
- package/src/mcp/client.rs +195 -0
- package/src/mcp/config.rs +20 -0
- package/src/mcp/mod.rs +6 -0
- package/src/mcp/protocol.rs +67 -0
- package/src/utils/auth.rs +41 -0
- package/src/utils/chat_logger.rs +568 -0
- package/src/utils/context.rs +390 -0
- package/src/utils/mod.rs +16 -0
- package/src/utils/network.rs +320 -0
- package/src/utils/rate_limiter.rs +166 -0
- package/src/utils/session.rs +73 -0
- package/src/utils/shell_permissions.rs +389 -0
- package/src/utils/telemetry.rs +41 -0
package/src/acp/tools.rs
ADDED
|
@@ -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
|
+
}
|