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,485 @@
1
+ //! Chat command handler for grok-cli
2
+ //!
3
+ //! Handles interactive and non-interactive chat sessions with Grok AI
4
+
5
+ use anyhow::{Result, anyhow};
6
+ use colored::*;
7
+ use serde_json::{Value, json};
8
+ use std::env;
9
+ use std::fs;
10
+ use std::io::{self, Write};
11
+ use std::process::Command;
12
+
13
+ use crate::acp::security::SecurityPolicy;
14
+ use crate::acp::tools;
15
+ use crate::{ToolCall, GrokClient};
16
+ use crate::cli::{create_spinner, format_grok_response, print_error, print_info, print_success};
17
+ use crate::config::RateLimitConfig;
18
+
19
+ pub struct ChatOptions<'a> {
20
+ pub message: Vec<String>,
21
+ pub interactive: bool,
22
+ pub system: Option<String>,
23
+ pub temperature: f32,
24
+ pub max_tokens: u32,
25
+ pub api_key: &'a str,
26
+ pub model: &'a str,
27
+ pub timeout_secs: u64,
28
+ pub max_retries: u32,
29
+ pub rate_limit_config: RateLimitConfig,
30
+ }
31
+
32
+ pub async fn handle_chat(options: ChatOptions<'_>) -> Result<()> {
33
+ let client =
34
+ GrokClient::with_settings(options.api_key, options.timeout_secs, options.max_retries)?
35
+ .with_rate_limits(options.rate_limit_config);
36
+
37
+ if options.interactive {
38
+ handle_interactive_chat(
39
+ client,
40
+ options.system,
41
+ options.temperature,
42
+ options.max_tokens,
43
+ options.model,
44
+ )
45
+ .await
46
+ } else {
47
+ let combined_message = options.message.join(" ");
48
+ handle_single_chat(
49
+ client,
50
+ &combined_message,
51
+ options.system,
52
+ options.temperature,
53
+ options.max_tokens,
54
+ options.model,
55
+ )
56
+ .await
57
+ }
58
+ }
59
+
60
+ async fn handle_single_chat(
61
+ client: GrokClient,
62
+ message: &str,
63
+ system: Option<String>,
64
+ temperature: f32,
65
+ max_tokens: u32,
66
+ model: &str,
67
+ ) -> Result<()> {
68
+ print_info(&format!("Sending message to Grok (model: {})...", model));
69
+
70
+ let spinner = create_spinner("Thinking...");
71
+
72
+ // Prepare messages
73
+ let mut messages = Vec::new();
74
+ if let Some(sys) = system {
75
+ messages.push(json!({
76
+ "role": "system",
77
+ "content": sys
78
+ }));
79
+ }
80
+ messages.push(json!({
81
+ "role": "user",
82
+ "content": message
83
+ }));
84
+
85
+ // Add tool definitions
86
+ let tools = tools::get_tool_definitions();
87
+
88
+ let result = client
89
+ .chat_completion_with_history(&messages, temperature, max_tokens, model, Some(tools))
90
+ .await;
91
+
92
+ spinner.finish_and_clear();
93
+
94
+ match result {
95
+ Ok(response) => {
96
+ // Handle tool calls if present
97
+ if let Some(tool_calls) = &response.tool_calls {
98
+ if !tool_calls.is_empty() {
99
+ print_info("Executing requested operations...");
100
+ let mut security = SecurityPolicy::new();
101
+ security.add_trusted_directory(&env::current_dir()?);
102
+
103
+ for tool_call in tool_calls {
104
+ execute_tool_call(tool_call, &security)?;
105
+ }
106
+ print_success("All operations completed!");
107
+ return Ok(());
108
+ }
109
+ }
110
+
111
+ // Regular text response
112
+ print_success("Response received!");
113
+ println!();
114
+ if let Some(content) = response.content {
115
+ println!("{}", format_grok_response(&content, true));
116
+ }
117
+ }
118
+ Err(e) => {
119
+ print_error(&format!("Failed to get response: {}", e));
120
+ return Err(e);
121
+ }
122
+ }
123
+
124
+ Ok(())
125
+ }
126
+
127
+ fn execute_tool_call(tool_call: &ToolCall, security: &SecurityPolicy) -> Result<()> {
128
+ let name = &tool_call.function.name;
129
+ let args: Value = serde_json::from_str(&tool_call.function.arguments)?;
130
+
131
+ match name.as_str() {
132
+ "write_file" => {
133
+ let path = args["path"]
134
+ .as_str()
135
+ .ok_or_else(|| anyhow!("Missing path"))?;
136
+ let content = args["content"]
137
+ .as_str()
138
+ .ok_or_else(|| anyhow!("Missing content"))?;
139
+ let result = tools::write_file(path, content, security)?;
140
+ println!(" {} {}", "✓".green(), result);
141
+ }
142
+ "read_file" => {
143
+ let path = args["path"]
144
+ .as_str()
145
+ .ok_or_else(|| anyhow!("Missing path"))?;
146
+ let content = tools::read_file(path, security)?;
147
+ println!(
148
+ " {} Read {} bytes from {}",
149
+ "✓".green(),
150
+ content.len(),
151
+ path
152
+ );
153
+ }
154
+ "replace" => {
155
+ let path = args["path"]
156
+ .as_str()
157
+ .ok_or_else(|| anyhow!("Missing path"))?;
158
+ let old = args["old_string"]
159
+ .as_str()
160
+ .ok_or_else(|| anyhow!("Missing old_string"))?;
161
+ let new = args["new_string"]
162
+ .as_str()
163
+ .ok_or_else(|| anyhow!("Missing new_string"))?;
164
+ let expected = args
165
+ .get("expected_replacements")
166
+ .and_then(|v| v.as_u64())
167
+ .map(|v| v as u32);
168
+ let result = tools::replace(path, old, new, expected, security)?;
169
+ println!(" {} {}", "✓".green(), result);
170
+ }
171
+ "list_directory" => {
172
+ let path = args["path"]
173
+ .as_str()
174
+ .ok_or_else(|| anyhow!("Missing path"))?;
175
+ let result = tools::list_directory(path, security)?;
176
+ println!(" {} Directory contents of {}:", "✓".green(), path);
177
+ for line in result.lines() {
178
+ println!(" {}", line);
179
+ }
180
+ }
181
+ "glob_search" => {
182
+ let pattern = args["pattern"]
183
+ .as_str()
184
+ .ok_or_else(|| anyhow!("Missing pattern"))?;
185
+ let result = tools::glob_search(pattern, security)?;
186
+ println!(" {} Files matching '{}':", "✓".green(), pattern);
187
+ for line in result.lines() {
188
+ println!(" {}", line);
189
+ }
190
+ }
191
+ "save_memory" => {
192
+ let fact = args["fact"]
193
+ .as_str()
194
+ .ok_or_else(|| anyhow!("Missing fact"))?;
195
+ let result = tools::save_memory(fact)?;
196
+ println!(" {} {}", "✓".green(), result);
197
+ }
198
+ "run_shell_command" => {
199
+ let command = args["command"]
200
+ .as_str()
201
+ .ok_or_else(|| anyhow!("Missing command"))?;
202
+ println!(" {} Executing: {}", "⚙".cyan(), command);
203
+ let result = tools::run_shell_command(command, security)?;
204
+ println!(" {} Command output:", "✓".green());
205
+ for line in result.lines() {
206
+ println!(" {}", line);
207
+ }
208
+ }
209
+ _ => {
210
+ println!(" {} Unsupported tool: {}", "⚠".yellow(), name);
211
+ }
212
+ }
213
+
214
+ Ok(())
215
+ }
216
+
217
+ async fn handle_interactive_chat(
218
+ client: GrokClient,
219
+ system: Option<String>,
220
+ temperature: f32,
221
+ max_tokens: u32,
222
+ model: &str,
223
+ ) -> Result<()> {
224
+ println!("{}", "🤖 Interactive Grok Chat Session".cyan().bold());
225
+ println!("{}", format!("Model: {}", model).dimmed());
226
+
227
+ if let Some(ref sys) = system {
228
+ println!("{}", format!("System: {}", sys).dimmed());
229
+ }
230
+
231
+ println!(
232
+ "{}",
233
+ "Type 'exit', 'quit', or press Ctrl+C to end the session".dimmed()
234
+ );
235
+ println!("{}", "Type 'help' for available commands".dimmed());
236
+ println!();
237
+
238
+ let mut conversation_history = Vec::new();
239
+
240
+ // Add system message if provided
241
+ if let Some(sys) = system {
242
+ conversation_history.push(json!({
243
+ "role": "system",
244
+ "content": sys
245
+ }));
246
+ }
247
+
248
+ // Set up security policy with current directory as trusted
249
+ let mut security = SecurityPolicy::new();
250
+ security.add_trusted_directory(&env::current_dir()?);
251
+
252
+ // Get tool definitions for function calling
253
+ let tools = tools::get_tool_definitions();
254
+
255
+ loop {
256
+ // Prompt for input
257
+ let cwd = env::current_dir().unwrap_or_default();
258
+ print!(
259
+ "{} {} ",
260
+ format!("[{}]", cwd.display()).dimmed(),
261
+ "You:".green().bold()
262
+ );
263
+ io::stdout().flush()?;
264
+
265
+ let mut input = String::new();
266
+ match io::stdin().read_line(&mut input) {
267
+ Ok(0) => {
268
+ // EOF reached (Ctrl+D)
269
+ println!("\n{}", "Goodbye!".cyan());
270
+ break;
271
+ }
272
+ Ok(_) => {
273
+ let input = input.trim();
274
+ let lower_input = input.to_lowercase();
275
+
276
+ // Handle special commands
277
+ if lower_input == "exit" || lower_input == "quit" || lower_input == "q" {
278
+ println!("{}", "Goodbye!".cyan());
279
+ break;
280
+ } else if lower_input == "help" || lower_input == "h" {
281
+ print_help();
282
+ continue;
283
+ } else if lower_input == "clear" || lower_input == "cls" {
284
+ // Clear conversation history but keep system message
285
+ if conversation_history
286
+ .first()
287
+ .and_then(|msg| msg.get("role"))
288
+ .and_then(|role| role.as_str())
289
+ == Some("system")
290
+ {
291
+ let system_msg = conversation_history[0].clone();
292
+ conversation_history.clear();
293
+ conversation_history.push(system_msg);
294
+ } else {
295
+ conversation_history.clear();
296
+ }
297
+ print_success("Conversation history cleared!");
298
+ continue;
299
+ } else if lower_input == "history" {
300
+ print_conversation_history(&conversation_history);
301
+ continue;
302
+ } else if lower_input == "ls" || lower_input == "dir" {
303
+ match fs::read_dir(".") {
304
+ Ok(entries) => {
305
+ println!("{}", "Current Directory Entries:".cyan().bold());
306
+ for entry in entries.flatten() {
307
+ let path = entry.path();
308
+ let name = path.file_name().unwrap_or_default().to_string_lossy();
309
+ if path.is_dir() {
310
+ println!(" {} {}", name.blue().bold(), "(DIR)".dimmed());
311
+ } else {
312
+ println!(" {}", name);
313
+ }
314
+ }
315
+ }
316
+ Err(e) => print_error(&format!("Failed to list directory: {}", e)),
317
+ }
318
+ continue;
319
+ } else if lower_input.starts_with("cd ") {
320
+ let path = input[3..].trim();
321
+ if let Err(e) = env::set_current_dir(path) {
322
+ print_error(&format!("Failed to change directory to '{}': {}", path, e));
323
+ } else {
324
+ print_success(&format!(
325
+ "Changed directory to {}",
326
+ env::current_dir().unwrap_or_default().display()
327
+ ));
328
+ }
329
+ continue;
330
+ } else if let Some(stripped) = input.strip_prefix('!') {
331
+ let command_line = stripped.trim();
332
+ if !command_line.is_empty() {
333
+ let result = if cfg!(target_os = "windows") {
334
+ Command::new("cmd").arg("/C").arg(command_line).status()
335
+ } else {
336
+ Command::new("sh").arg("-c").arg(command_line).status()
337
+ };
338
+
339
+ match result {
340
+ Ok(status) => {
341
+ if !status.success() {
342
+ print_error(&format!("Command exited with status: {}", status));
343
+ }
344
+ }
345
+ Err(e) => print_error(&format!("Failed to execute command: {}", e)),
346
+ }
347
+ }
348
+ continue;
349
+ } else if input.is_empty() {
350
+ continue;
351
+ }
352
+
353
+ // Add user message to history
354
+ conversation_history.push(json!({
355
+ "role": "user",
356
+ "content": input
357
+ }));
358
+
359
+ // Show spinner while waiting for response
360
+ let spinner = create_spinner("Grok is thinking...");
361
+
362
+ // Get response with timeout and retries (including tool definitions)
363
+ let response_msg = client
364
+ .chat_completion_with_history(
365
+ &conversation_history,
366
+ temperature,
367
+ max_tokens,
368
+ model,
369
+ Some(tools.clone()),
370
+ )
371
+ .await?;
372
+
373
+ spinner.finish_and_clear();
374
+
375
+ // Handle tool calls if present
376
+ if let Some(tool_calls) = &response_msg.tool_calls {
377
+ if !tool_calls.is_empty() {
378
+ println!("{}", "Grok is executing operations...".blue().dimmed());
379
+
380
+ for tool_call in tool_calls {
381
+ if let Err(e) = execute_tool_call(tool_call, &security) {
382
+ print_error(&format!("Tool execution failed: {}", e));
383
+ }
384
+ }
385
+
386
+ // Add assistant's tool call response to history
387
+ conversation_history.push(json!({
388
+ "role": "assistant",
389
+ "content": response_msg.content.clone(),
390
+ "tool_calls": tool_calls
391
+ }));
392
+
393
+ continue;
394
+ }
395
+ }
396
+
397
+ let response = response_msg.content.unwrap_or_default();
398
+
399
+ // Add assistant response to history
400
+ conversation_history.push(json!({
401
+ "role": "assistant",
402
+ "content": response.clone()
403
+ }));
404
+
405
+ // Display response
406
+ println!("{} {}", "Grok:".blue().bold(), response);
407
+ }
408
+ Err(e) => {
409
+ print_error(&format!("Failed to read input: {}", e));
410
+ break;
411
+ }
412
+ }
413
+ }
414
+
415
+ Ok(())
416
+ }
417
+
418
+ fn print_help() {
419
+ println!();
420
+ println!("{}", "Available commands:".cyan().bold());
421
+ println!(" {} - Exit the chat session", "exit, quit, q".yellow());
422
+ println!(" {} - Show this help message", "help, h".yellow());
423
+ println!(" {} - Clear conversation history", "clear, cls".yellow());
424
+ println!(" {} - Show conversation history", "history".yellow());
425
+ println!(" {} - List files in current directory", "ls, dir".yellow());
426
+ println!(" {} - Change current directory", "cd <path>".yellow());
427
+ println!(" {} - Execute shell command", "!<command>".yellow());
428
+ println!();
429
+ println!("{}", "Tool Support:".cyan().bold());
430
+ println!(" Grok can now automatically create files and directories!");
431
+ println!(" Just ask naturally, e.g.:");
432
+ println!(" {} 'Create a new Rust project structure'", "•".green());
433
+ println!(
434
+ " {} 'Write a hello world program to main.rs'",
435
+ "•".green()
436
+ );
437
+ println!();
438
+ }
439
+
440
+ fn print_conversation_history(history: &[Value]) {
441
+ if history.is_empty() {
442
+ print_info("No conversation history yet.");
443
+ return;
444
+ }
445
+
446
+ println!();
447
+ println!("{}", "Conversation History:".cyan().bold());
448
+ println!("{}", "─".repeat(50));
449
+
450
+ for (i, message) in history.iter().enumerate() {
451
+ if let (Some(role), Some(content)) = (
452
+ message.get("role").and_then(|r| r.as_str()),
453
+ message.get("content").and_then(|c| c.as_str()),
454
+ ) {
455
+ let role_display = match role {
456
+ "system" => "System".magenta().bold(),
457
+ "user" => "You".green().bold(),
458
+ "assistant" => "Grok".blue().bold(),
459
+ _ => role.white().bold(),
460
+ };
461
+
462
+ println!("{}: {}", role_display, content);
463
+
464
+ if i < history.len() - 1 {
465
+ println!();
466
+ }
467
+ }
468
+ }
469
+
470
+ println!("{}", "─".repeat(50));
471
+ println!();
472
+ }
473
+
474
+ #[cfg(test)]
475
+ mod tests {
476
+ use super::*;
477
+ use tokio;
478
+
479
+ #[tokio::test]
480
+ async fn test_chat_command_structure() {
481
+ // Test that the chat command structure is properly defined
482
+ // This is a placeholder test - in a real implementation you'd mock the API
483
+ // The test passes as long as the module compiles correctly
484
+ }
485
+ }