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,1255 @@
1
+ //! Interactive mode for Grok CLI
2
+ //!
3
+ //! Provides a Gemini CLI-like interactive experience with persistent sessions,
4
+ //! input prompts, and real-time status display
5
+
6
+ use anyhow::Result;
7
+ use colored::*;
8
+ use std::env;
9
+ use std::io::{self, Write};
10
+ use std::path::PathBuf;
11
+ use tokio::time::{Duration, sleep};
12
+
13
+ use crate::acp::security::SecurityPolicy;
14
+ use crate::acp::tools;
15
+ use crate::GrokClient;
16
+ use crate::config::Config;
17
+ use crate::display::{
18
+ BannerConfig, clear_current_line, print_directory_recommendation, print_grok_logo,
19
+ print_welcome_banner,
20
+ };
21
+ use crate::utils::context::{
22
+ format_context_for_prompt, get_all_context_file_paths, load_and_merge_project_context,
23
+ };
24
+ use crate::utils::session::{list_sessions, load_session, save_session};
25
+ use crate::utils::shell_permissions::{ApprovalMode, ShellPermissions};
26
+ use serde::{Deserialize, Serialize};
27
+ use serde_json::json;
28
+
29
+ /// Interactive session state
30
+ #[derive(Debug, Clone, Serialize, Deserialize)]
31
+ pub struct InteractiveSession {
32
+ pub session_id: String,
33
+ pub model: String,
34
+ pub temperature: f32,
35
+ pub max_tokens: u32,
36
+ pub system_prompt: Option<String>,
37
+ pub conversation_history: Vec<ConversationItem>,
38
+ pub current_directory: PathBuf,
39
+ pub show_context_usage: bool,
40
+ pub total_tokens_used: u32,
41
+ }
42
+
43
+ /// Conversation item in the session
44
+ #[derive(Debug, Clone, Serialize, Deserialize)]
45
+ pub struct ConversationItem {
46
+ pub role: String,
47
+ pub content: String,
48
+ pub timestamp: chrono::DateTime<chrono::Utc>,
49
+ pub tokens_used: Option<u32>,
50
+ }
51
+
52
+ /// Interactive mode configuration
53
+ #[derive(Debug, Clone)]
54
+ pub struct InteractiveConfig {
55
+ pub show_banner: bool,
56
+ pub show_tips: bool,
57
+ pub show_status: bool,
58
+ pub auto_save_session: bool,
59
+ pub prompt_style: PromptStyle,
60
+ pub check_directory: bool,
61
+ }
62
+
63
+ /// Different prompt styles
64
+ #[derive(Debug, Clone, PartialEq)]
65
+ pub enum PromptStyle {
66
+ Simple,
67
+ Rich,
68
+ Minimal,
69
+ }
70
+
71
+ impl Default for InteractiveConfig {
72
+ fn default() -> Self {
73
+ Self {
74
+ show_banner: true,
75
+ show_tips: true,
76
+ show_status: true,
77
+ auto_save_session: false,
78
+ prompt_style: PromptStyle::Rich,
79
+ check_directory: true,
80
+ }
81
+ }
82
+ }
83
+
84
+ impl InteractiveSession {
85
+ /// Create a new interactive session
86
+ pub fn new(model: String, system_prompt: Option<String>) -> Self {
87
+ let session_id = generate_session_id();
88
+ let current_directory = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
89
+
90
+ Self {
91
+ session_id,
92
+ model,
93
+ temperature: 0.7,
94
+ max_tokens: 4096,
95
+ system_prompt,
96
+ conversation_history: Vec::new(),
97
+ current_directory,
98
+ show_context_usage: true,
99
+ total_tokens_used: 0,
100
+ }
101
+ }
102
+
103
+ /// Add a conversation item to the history
104
+ pub fn add_conversation_item(&mut self, role: &str, content: &str, tokens_used: Option<u32>) {
105
+ let item = ConversationItem {
106
+ role: role.to_string(),
107
+ content: content.to_string(),
108
+ timestamp: chrono::Utc::now(),
109
+ tokens_used,
110
+ };
111
+
112
+ if let Some(tokens) = tokens_used {
113
+ self.total_tokens_used += tokens;
114
+ }
115
+
116
+ self.conversation_history.push(item);
117
+ }
118
+
119
+ /// Get context usage information
120
+ pub fn get_context_info(&self) -> String {
121
+ let conversation_count = self.conversation_history.len();
122
+ let context_percentage = if self.total_tokens_used > 0 {
123
+ let estimated_max = 8192; // Rough estimate for context window
124
+ ((self.total_tokens_used as f32 / estimated_max as f32) * 100.0) as u8
125
+ } else {
126
+ 0
127
+ };
128
+
129
+ format!(
130
+ "{}% context left | {} messages",
131
+ 100 - context_percentage,
132
+ conversation_count
133
+ )
134
+ }
135
+ }
136
+
137
+ /// Generate a unique session ID
138
+ fn generate_session_id() -> String {
139
+ use std::time::{SystemTime, UNIX_EPOCH};
140
+ let timestamp = SystemTime::now()
141
+ .duration_since(UNIX_EPOCH)
142
+ .unwrap()
143
+ .as_nanos();
144
+ format!("grok-{}", timestamp)
145
+ }
146
+
147
+ /// Start the interactive mode
148
+ pub async fn start_interactive_mode(
149
+ api_key: &str,
150
+ model: &str,
151
+ config: &Config,
152
+ interactive_config: InteractiveConfig,
153
+ ) -> Result<()> {
154
+ // Load project context if available
155
+ let project_context = load_project_context_for_session(
156
+ &env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
157
+ );
158
+
159
+ let mut session = InteractiveSession::new(model.to_string(), project_context);
160
+ let client = GrokClient::new(api_key)?;
161
+
162
+ // Display startup elements
163
+ if interactive_config.show_banner {
164
+ display_startup_screen(&interactive_config, &session, config).await?;
165
+ }
166
+
167
+ // Check if running in home directory
168
+ if interactive_config.check_directory && is_home_directory(&session.current_directory) {
169
+ let banner_config = BannerConfig::default();
170
+ print_directory_recommendation(
171
+ &session.current_directory.display().to_string(),
172
+ &banner_config,
173
+ );
174
+ }
175
+
176
+ // Main interactive loop
177
+ loop {
178
+ match run_interactive_loop(&mut session, &client, &interactive_config, config).await {
179
+ Ok(should_continue) => {
180
+ if !should_continue {
181
+ break;
182
+ }
183
+ }
184
+ Err(e) => {
185
+ eprintln!("{} {}", "Error:".red(), e);
186
+ continue;
187
+ }
188
+ }
189
+ }
190
+
191
+ // Goodbye message
192
+ println!("{}", "\n👋 Thanks for using Grok CLI!".bright_cyan());
193
+
194
+ if interactive_config.auto_save_session && !session.conversation_history.is_empty() {
195
+ println!("{}", "Session saved for future reference.".dimmed());
196
+ }
197
+
198
+ Ok(())
199
+ }
200
+
201
+ /// Display the startup screen
202
+ async fn display_startup_screen(
203
+ config: &InteractiveConfig,
204
+ session: &InteractiveSession,
205
+ app_config: &Config,
206
+ ) -> Result<()> {
207
+ let (width, _) = crate::display::get_terminal_size();
208
+
209
+ // Clear screen and show logo with animation
210
+ crate::display::clear_screen();
211
+
212
+ if config.show_banner && !config.show_tips {
213
+ print_grok_logo(width);
214
+ sleep(Duration::from_millis(500)).await;
215
+ }
216
+
217
+ if config.show_tips {
218
+ let banner_config = BannerConfig {
219
+ show_banner: true,
220
+ show_tips: true,
221
+ show_updates: true,
222
+ width: Some(width),
223
+ };
224
+ print_welcome_banner(&banner_config);
225
+ }
226
+
227
+ // Show current session info
228
+ if config.show_status {
229
+ print_session_info(session, app_config);
230
+ }
231
+
232
+ Ok(())
233
+ }
234
+
235
+ /// Print current session information
236
+ fn print_session_info(session: &InteractiveSession, config: &Config) {
237
+ println!("{}", "Current session:".bright_white());
238
+ println!(" Model: {}", session.model.bright_cyan());
239
+ println!(
240
+ " Directory: {}",
241
+ session
242
+ .current_directory
243
+ .display()
244
+ .to_string()
245
+ .bright_yellow()
246
+ );
247
+
248
+ // Show config source
249
+ if let Some(source) = &config.config_source {
250
+ println!(" Configuration: {}", source.display().bright_magenta());
251
+ }
252
+
253
+ // Show context files info if loaded
254
+ let context_paths = get_all_context_file_paths(&session.current_directory);
255
+ if !context_paths.is_empty() {
256
+ if context_paths.len() == 1 {
257
+ println!(
258
+ " Context loaded: {}",
259
+ context_paths[0]
260
+ .file_name()
261
+ .and_then(|n| n.to_str())
262
+ .unwrap_or("unknown")
263
+ .bright_green()
264
+ );
265
+ } else {
266
+ println!(
267
+ " Context loaded: {} files",
268
+ context_paths.len().to_string().bright_green()
269
+ );
270
+ for path in &context_paths {
271
+ println!(
272
+ " - {}",
273
+ path.file_name()
274
+ .and_then(|n| n.to_str())
275
+ .unwrap_or("unknown")
276
+ .dimmed()
277
+ );
278
+ }
279
+ }
280
+ }
281
+
282
+ if let Some(system) = &session.system_prompt {
283
+ let preview = if system.len() > 60 {
284
+ format!("{}...", &system[..60])
285
+ } else {
286
+ system.clone()
287
+ };
288
+ println!(" System prompt: {}", preview.bright_green());
289
+ }
290
+ println!();
291
+ }
292
+
293
+ /// Load project context for a new session
294
+ fn load_project_context_for_session(project_root: &PathBuf) -> Option<String> {
295
+ match load_and_merge_project_context(project_root) {
296
+ Ok(Some(context)) => {
297
+ let formatted = format_context_for_prompt(&context);
298
+ let context_paths = get_all_context_file_paths(project_root);
299
+
300
+ if context_paths.is_empty() {
301
+ // Shouldn't happen but handle gracefully
302
+ return Some(formatted);
303
+ }
304
+
305
+ if context_paths.len() == 1 {
306
+ let context_file_name = context_paths[0]
307
+ .file_name()
308
+ .and_then(|n| n.to_os_string().into_string().ok())
309
+ .unwrap_or_else(|| "context file".to_string());
310
+
311
+ println!(
312
+ "{} {}",
313
+ "✓".bright_green(),
314
+ format!("Loaded project context from {}", context_file_name).dimmed()
315
+ );
316
+ } else {
317
+ println!(
318
+ "{} {}",
319
+ "✓".bright_green(),
320
+ format!("Loaded and merged {} context files", context_paths.len()).dimmed()
321
+ );
322
+ for path in &context_paths {
323
+ if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
324
+ println!(" {} {}", "•".dimmed(), name.dimmed());
325
+ }
326
+ }
327
+ }
328
+
329
+ Some(formatted)
330
+ }
331
+ Ok(None) => {
332
+ // No context file found - this is normal
333
+ None
334
+ }
335
+ Err(e) => {
336
+ eprintln!(
337
+ "{} Failed to load project context: {}",
338
+ "⚠".yellow(),
339
+ e.to_string().dimmed()
340
+ );
341
+ None
342
+ }
343
+ }
344
+ }
345
+
346
+ use crate::display::components::input::{Suggestion, read_input_with_suggestions};
347
+
348
+ /// Main interactive loop
349
+ async fn run_interactive_loop(
350
+ session: &mut InteractiveSession,
351
+ client: &GrokClient,
352
+ interactive_config: &InteractiveConfig,
353
+ app_config: &Config,
354
+ ) -> Result<bool> {
355
+ // Prepare prompt
356
+ let prompt = match interactive_config.prompt_style {
357
+ PromptStyle::Simple => format!("{} ", ">".bright_cyan()),
358
+ PromptStyle::Rich => {
359
+ let context_info = if session.show_context_usage {
360
+ format!(" | {}", session.get_context_info())
361
+ } else {
362
+ String::new()
363
+ };
364
+
365
+ format!(
366
+ "{} {} ",
367
+ format!("Grok ({})", session.model).bright_cyan(),
368
+ format!(
369
+ "[{}{}]",
370
+ session
371
+ .current_directory
372
+ .file_name()
373
+ .and_then(|n| n.to_str())
374
+ .unwrap_or("?"),
375
+ context_info
376
+ )
377
+ .dimmed()
378
+ )
379
+ }
380
+ PromptStyle::Minimal => "» ".to_string(),
381
+ };
382
+
383
+ // Prepare suggestions
384
+ let suggestions = vec![
385
+ Suggestion {
386
+ text: "/clear".to_string(),
387
+ description: "Clear screen".to_string(),
388
+ },
389
+ Suggestion {
390
+ text: "/help".to_string(),
391
+ description: "Show help message".to_string(),
392
+ },
393
+ Suggestion {
394
+ text: "/history".to_string(),
395
+ description: "Show history".to_string(),
396
+ },
397
+ Suggestion {
398
+ text: "/list".to_string(),
399
+ description: "List saved sessions".to_string(),
400
+ },
401
+ Suggestion {
402
+ text: "/load".to_string(),
403
+ description: "Load a session".to_string(),
404
+ },
405
+ Suggestion {
406
+ text: "/model".to_string(),
407
+ description: "Change model".to_string(),
408
+ },
409
+ Suggestion {
410
+ text: "/quit".to_string(),
411
+ description: "Exit interactive mode".to_string(),
412
+ },
413
+ Suggestion {
414
+ text: "/reset".to_string(),
415
+ description: "Reset session".to_string(),
416
+ },
417
+ Suggestion {
418
+ text: "/save".to_string(),
419
+ description: "Save current session".to_string(),
420
+ },
421
+ Suggestion {
422
+ text: "/settings".to_string(),
423
+ description: "Open settings".to_string(),
424
+ },
425
+ Suggestion {
426
+ text: "/status".to_string(),
427
+ description: "Show status".to_string(),
428
+ },
429
+ Suggestion {
430
+ text: "/system".to_string(),
431
+ description: "Set system prompt".to_string(),
432
+ },
433
+ Suggestion {
434
+ text: "/tools".to_string(),
435
+ description: "List coding tools".to_string(),
436
+ },
437
+ Suggestion {
438
+ text: "/version".to_string(),
439
+ description: "Show version info".to_string(),
440
+ },
441
+ Suggestion {
442
+ text: "/config".to_string(),
443
+ description: "Show configuration info".to_string(),
444
+ },
445
+ Suggestion {
446
+ text: "!ls".to_string(),
447
+ description: "List files (shell command)".to_string(),
448
+ },
449
+ Suggestion {
450
+ text: "!dir".to_string(),
451
+ description: "List files on Windows (shell command)".to_string(),
452
+ },
453
+ Suggestion {
454
+ text: "!git status".to_string(),
455
+ description: "Check git status (shell command)".to_string(),
456
+ },
457
+ Suggestion {
458
+ text: "!pwd".to_string(),
459
+ description: "Print working directory (shell command)".to_string(),
460
+ },
461
+ ];
462
+
463
+ // Read user input
464
+ // Note: We're running blocking TUI code in an async context, which is generally bad,
465
+ // but for a CLI it's acceptable as we're awaiting user input anyway.
466
+ let input =
467
+ tokio::task::spawn_blocking(move || read_input_with_suggestions(&prompt, &suggestions))
468
+ .await??;
469
+
470
+ let input = input.trim();
471
+
472
+ // Handle empty input
473
+ if input.is_empty() {
474
+ return Ok(true);
475
+ }
476
+
477
+ // Handle shell commands (starting with !)
478
+ if input.starts_with('!') {
479
+ return handle_shell_command(input).await;
480
+ }
481
+
482
+ // Handle special commands
483
+ if let Some(command_result) =
484
+ handle_special_commands(input, session, interactive_config, app_config).await?
485
+ {
486
+ return Ok(command_result);
487
+ }
488
+
489
+ // Send to Grok API
490
+ match send_to_grok(client, session, input).await {
491
+ Ok(_) => Ok(true),
492
+ Err(e) => {
493
+ eprintln!("{} Failed to get response: {}", "Error:".red(), e);
494
+ Ok(true)
495
+ }
496
+ }
497
+ }
498
+
499
+ /// Display the input prompt
500
+ fn display_prompt(session: &InteractiveSession, config: &InteractiveConfig) -> Result<()> {
501
+ match config.prompt_style {
502
+ PromptStyle::Simple => {
503
+ print!("{} ", ">".bright_cyan());
504
+ }
505
+ PromptStyle::Rich => {
506
+ let context_info = if session.show_context_usage {
507
+ format!(" | {}", session.get_context_info())
508
+ } else {
509
+ String::new()
510
+ };
511
+
512
+ print!(
513
+ "{} {} ",
514
+ format!("Grok ({})", session.model).bright_cyan(),
515
+ format!(
516
+ "[{}{}]",
517
+ session
518
+ .current_directory
519
+ .file_name()
520
+ .and_then(|n| n.to_str())
521
+ .unwrap_or("?"),
522
+ context_info
523
+ )
524
+ .dimmed()
525
+ );
526
+ }
527
+ PromptStyle::Minimal => {
528
+ print!("» ");
529
+ }
530
+ }
531
+
532
+ io::stdout().flush()?;
533
+ Ok(())
534
+ }
535
+
536
+ /// Read user input from stdin
537
+ fn read_user_input() -> Result<String> {
538
+ let mut input = String::new();
539
+ std::io::stdin().read_line(&mut input)?;
540
+ Ok(input)
541
+ }
542
+
543
+ /// Handle shell commands (those starting with !)
544
+ async fn handle_shell_command(input: &str) -> Result<bool> {
545
+ let command = input.trim_start_matches('!').trim();
546
+
547
+ if command.is_empty() {
548
+ println!("{}", "Error: No command specified".red());
549
+ return Ok(true);
550
+ }
551
+
552
+ // Create permissions manager (TODO: pass from session state)
553
+ let mut permissions = ShellPermissions::new(ApprovalMode::Default);
554
+
555
+ // Check if command should be executed
556
+ match permissions.should_execute(command) {
557
+ Ok(true) => {
558
+ // Permission granted, execute command
559
+ println!();
560
+ println!("{} {}", "Executing:".bright_cyan(), command.bright_yellow());
561
+ println!();
562
+
563
+ // Determine shell based on OS
564
+ #[cfg(target_os = "windows")]
565
+ let shell = "cmd";
566
+ #[cfg(target_os = "windows")]
567
+ let shell_arg = "/C";
568
+
569
+ #[cfg(not(target_os = "windows"))]
570
+ let shell = "sh";
571
+ #[cfg(not(target_os = "windows"))]
572
+ let shell_arg = "-c";
573
+
574
+ // Execute the command
575
+ match std::process::Command::new(shell)
576
+ .arg(shell_arg)
577
+ .arg(command)
578
+ .output()
579
+ {
580
+ Ok(output) => {
581
+ // Print stdout
582
+ if !output.stdout.is_empty() {
583
+ let stdout = String::from_utf8_lossy(&output.stdout);
584
+ print!("{}", stdout);
585
+ }
586
+
587
+ // Print stderr in red
588
+ if !output.stderr.is_empty() {
589
+ let stderr = String::from_utf8_lossy(&output.stderr);
590
+ eprint!("{}", stderr.red());
591
+ }
592
+
593
+ // Show exit code if non-zero
594
+ if !output.status.success() {
595
+ println!();
596
+ println!(
597
+ "{} Command exited with code: {}",
598
+ "⚠".yellow(),
599
+ output.status.code().unwrap_or(-1)
600
+ );
601
+ }
602
+ }
603
+ Err(e) => {
604
+ eprintln!("{} Failed to execute command: {}", "Error:".red(), e);
605
+ }
606
+ }
607
+
608
+ println!();
609
+ }
610
+ Ok(false) => {
611
+ // Permission denied
612
+ println!();
613
+ println!("{}", "Command execution cancelled".yellow());
614
+ println!();
615
+ }
616
+ Err(e) => {
617
+ eprintln!("{} Permission check failed: {}", "Error:".red(), e);
618
+ }
619
+ }
620
+
621
+ Ok(true)
622
+ }
623
+
624
+ /// Handle special commands (those starting with /)
625
+ async fn handle_special_commands(
626
+ input: &str,
627
+ session: &mut InteractiveSession,
628
+ interactive_config: &InteractiveConfig,
629
+ app_config: &Config,
630
+ ) -> Result<Option<bool>> {
631
+ if !input.starts_with('/') {
632
+ return Ok(None);
633
+ }
634
+
635
+ let command = input.trim_start_matches('/').trim();
636
+ let parts: Vec<&str> = command.split_whitespace().collect();
637
+
638
+ if parts.is_empty() {
639
+ return Ok(Some(true));
640
+ }
641
+
642
+ match parts[0] {
643
+ "help" | "h" => {
644
+ print_interactive_help();
645
+ Ok(Some(true))
646
+ }
647
+ "quit" | "exit" | "q" => Ok(Some(false)),
648
+ "clear" | "cls" => {
649
+ crate::display::clear_screen();
650
+ if interactive_config.show_banner {
651
+ let (width, _) = crate::display::get_terminal_size();
652
+ print_grok_logo(width);
653
+ }
654
+ Ok(Some(true))
655
+ }
656
+ "model" | "models" => {
657
+ if parts.len() > 1 {
658
+ session.model = parts[1].to_string();
659
+ println!(
660
+ "{} Model changed to: {}",
661
+ "✓".bright_green(),
662
+ session.model.bright_cyan()
663
+ );
664
+ } else {
665
+ println!(
666
+ "{} Current model: {}",
667
+ "ℹ".bright_blue(),
668
+ session.model.bright_cyan()
669
+ );
670
+ }
671
+ Ok(Some(true))
672
+ }
673
+ "system" => {
674
+ if parts.len() > 1 {
675
+ let system_prompt = parts[1..].join(" ");
676
+ session.system_prompt = Some(system_prompt.clone());
677
+ println!(
678
+ "{} System prompt set: {}",
679
+ "✓".bright_green(),
680
+ system_prompt.bright_yellow()
681
+ );
682
+ } else {
683
+ match &session.system_prompt {
684
+ Some(prompt) => println!(
685
+ "{} Current system prompt: {}",
686
+ "ℹ".bright_blue(),
687
+ prompt.bright_yellow()
688
+ ),
689
+ None => println!("{} No system prompt set", "ℹ".bright_blue()),
690
+ }
691
+ }
692
+ Ok(Some(true))
693
+ }
694
+ "settings" => {
695
+ crate::cli::commands::settings::handle_settings_action(
696
+ crate::SettingsAction::Show,
697
+ app_config,
698
+ )
699
+ .await?;
700
+ Ok(Some(true))
701
+ }
702
+ "tools" => {
703
+ print_available_tools();
704
+ Ok(Some(true))
705
+ }
706
+ "history" => {
707
+ print_conversation_history(session);
708
+ Ok(Some(true))
709
+ }
710
+ "status" => {
711
+ print_session_status(session);
712
+ Ok(Some(true))
713
+ }
714
+ "version" => {
715
+ println!(
716
+ "{} Grok CLI v{}",
717
+ "ℹ".bright_blue(),
718
+ env!("CARGO_PKG_VERSION")
719
+ );
720
+ Ok(Some(true))
721
+ }
722
+ "reset" => {
723
+ session.conversation_history.clear();
724
+ session.total_tokens_used = 0;
725
+ println!("{} Conversation history cleared", "✓".bright_green());
726
+ Ok(Some(true))
727
+ }
728
+ "save" => {
729
+ if parts.len() < 2 {
730
+ println!("{} Usage: /save <name>", "⚠".bright_yellow());
731
+ } else {
732
+ let name = parts[1];
733
+ match save_session(session, name) {
734
+ Ok(path) => {
735
+ println!("{} Session saved to {}", "✓".bright_green(), path.display())
736
+ }
737
+ Err(e) => println!("{} Failed to save session: {}", "✗".bright_red(), e),
738
+ }
739
+ }
740
+ Ok(Some(true))
741
+ }
742
+ "load" => {
743
+ if parts.len() < 2 {
744
+ println!("{} Usage: /load <name>", "⚠".bright_yellow());
745
+ } else {
746
+ let name = parts[1];
747
+ match load_session(name) {
748
+ Ok(loaded_session) => {
749
+ *session = loaded_session;
750
+ println!("{} Session '{}' loaded", "✓".bright_green(), name);
751
+ // Note: Can't show config here as we don't have access to it in this scope
752
+ println!(" Model: {}", session.model.bright_cyan());
753
+ println!(
754
+ " Directory: {}",
755
+ session
756
+ .current_directory
757
+ .display()
758
+ .to_string()
759
+ .bright_yellow()
760
+ );
761
+ }
762
+ Err(e) => println!("{} Failed to load session: {}", "✗".bright_red(), e),
763
+ }
764
+ }
765
+ Ok(Some(true))
766
+ }
767
+ "list" | "sessions" => {
768
+ match list_sessions() {
769
+ Ok(sessions) => {
770
+ if sessions.is_empty() {
771
+ println!("{} No saved sessions found", "ℹ".bright_blue());
772
+ } else {
773
+ println!("{}", "Saved Sessions:".bright_cyan().bold());
774
+ for s in sessions {
775
+ println!(" • {}", s);
776
+ }
777
+ }
778
+ }
779
+ Err(e) => println!("{} Failed to list sessions: {}", "✗".bright_red(), e),
780
+ }
781
+ Ok(Some(true))
782
+ }
783
+ _ => {
784
+ println!("{} Unknown command: /{}", "⚠".bright_yellow(), parts[0]);
785
+ println!("Type /help for available commands");
786
+ Ok(Some(true))
787
+ }
788
+ }
789
+ }
790
+
791
+ /// Print interactive mode help
792
+ fn print_interactive_help() {
793
+ println!("{}", "Interactive mode commands:".bright_cyan().bold());
794
+ println!();
795
+
796
+ let commands = vec![
797
+ ("/help, /h", "Show this help message"),
798
+ ("/quit, /exit, /q", "Exit interactive mode"),
799
+ ("/clear, /cls", "Clear screen and show logo"),
800
+ ("/model [name]", "Show or change the current model"),
801
+ ("/system [prompt]", "Show or set system prompt"),
802
+ ("/tools", "List available coding tools"),
803
+ (
804
+ "!<command>",
805
+ "Execute shell command locally (e.g., !dir, !ls -la)",
806
+ ),
807
+ ("/settings", "Open settings menu"),
808
+ ("/history", "Show conversation history"),
809
+ ("/status", "Show session status"),
810
+ ("/version", "Show version info"),
811
+ ("/config", "Show configuration info"),
812
+ ("/reset", "Clear conversation history"),
813
+ ("/save [name]", "Save current session"),
814
+ ("/load [name]", "Load a saved session"),
815
+ ("/list", "List saved sessions"),
816
+ ];
817
+
818
+ for (command, description) in commands {
819
+ println!(" {:<20} {}", command.bright_white(), description);
820
+ }
821
+ println!();
822
+ println!("{}", "Just type your message to chat with Grok!".dimmed());
823
+ println!();
824
+ }
825
+
826
+ /// Print available coding tools
827
+ fn print_available_tools() {
828
+ use colored::*;
829
+
830
+ println!("{}", "Available Coding Tools:".bright_cyan().bold());
831
+ println!();
832
+ println!("{}", "These tools are available when using the ACP server or when Grok needs to perform file operations:".dimmed());
833
+ println!();
834
+
835
+ let tools = vec![
836
+ (
837
+ "read_file",
838
+ "Read the content of a file",
839
+ "read_file(path: string)",
840
+ ),
841
+ (
842
+ "write_file",
843
+ "Write content to a file",
844
+ "write_file(path: string, content: string)",
845
+ ),
846
+ (
847
+ "replace",
848
+ "Replace text in a file",
849
+ "replace(path: string, old_string: string, new_string: string)",
850
+ ),
851
+ (
852
+ "list_directory",
853
+ "List files and directories",
854
+ "list_directory(path: string)",
855
+ ),
856
+ (
857
+ "glob_search",
858
+ "Find files matching a pattern",
859
+ "glob_search(pattern: string)",
860
+ ),
861
+ (
862
+ "search_file_content",
863
+ "Search for text in files",
864
+ "search_file_content(path: string, pattern: string)",
865
+ ),
866
+ (
867
+ "run_shell_command",
868
+ "Execute a shell command",
869
+ "run_shell_command(command: string)",
870
+ ),
871
+ ("web_search", "Search the web", "web_search(query: string)"),
872
+ (
873
+ "web_fetch",
874
+ "Fetch content from a URL",
875
+ "web_fetch(url: string)",
876
+ ),
877
+ (
878
+ "save_memory",
879
+ "Save a fact to memory",
880
+ "save_memory(fact: string)",
881
+ ),
882
+ ];
883
+
884
+ println!("{}", "File Operations:".bright_yellow().bold());
885
+ for (name, desc, sig) in &tools[0..3] {
886
+ println!(" {} {}", name.bright_white().bold(), "-".dimmed());
887
+ println!(" {}", desc.dimmed());
888
+ println!(" {}", sig.bright_blue());
889
+ println!();
890
+ }
891
+
892
+ println!("{}", "File Search & Discovery:".bright_yellow().bold());
893
+ for (name, desc, sig) in &tools[3..6] {
894
+ println!(" {} {}", name.bright_white().bold(), "-".dimmed());
895
+ println!(" {}", desc.dimmed());
896
+ println!(" {}", sig.bright_blue());
897
+ println!();
898
+ }
899
+
900
+ println!("{}", "Execution & Web:".bright_yellow().bold());
901
+ for (name, desc, sig) in &tools[6..9] {
902
+ println!(" {} {}", name.bright_white().bold(), "-".dimmed());
903
+ println!(" {}", desc.dimmed());
904
+ println!(" {}", sig.bright_blue());
905
+ println!();
906
+ }
907
+
908
+ println!("{}", "Memory:".bright_yellow().bold());
909
+ for (name, desc, sig) in &tools[9..10] {
910
+ println!(" {} {}", name.bright_white().bold(), "-".dimmed());
911
+ println!(" {}", desc.dimmed());
912
+ println!(" {}", sig.bright_blue());
913
+ println!();
914
+ }
915
+
916
+ println!("{}", "Note:".bright_cyan());
917
+ println!(
918
+ " {}",
919
+ "• Tools are automatically used by Grok when needed".dimmed()
920
+ );
921
+ println!(
922
+ " {}",
923
+ "• For ACP server mode, use: grok acp stdio".dimmed()
924
+ );
925
+ println!(
926
+ " {}",
927
+ "• All file operations respect security permissions".dimmed()
928
+ );
929
+ println!();
930
+ }
931
+
932
+ /// Print conversation history
933
+ fn print_conversation_history(session: &InteractiveSession) {
934
+ if session.conversation_history.is_empty() {
935
+ println!("{} No conversation history yet", "ℹ".bright_blue());
936
+ return;
937
+ }
938
+
939
+ println!("{}", "Conversation History:".bright_cyan().bold());
940
+ println!();
941
+
942
+ for (i, item) in session.conversation_history.iter().enumerate() {
943
+ let role_color = if item.role == "user" {
944
+ Color::BrightGreen
945
+ } else {
946
+ Color::BrightBlue
947
+ };
948
+
949
+ let role_symbol = if item.role == "user" { "👤" } else { "🤖" };
950
+
951
+ println!(
952
+ "{} {} {}",
953
+ format!("{}.", i + 1).dimmed(),
954
+ role_symbol,
955
+ item.role.color(role_color).bold()
956
+ );
957
+
958
+ // Show first 100 chars of content
959
+ let content_preview = if item.content.len() > 100 {
960
+ format!("{}...", &item.content[..97])
961
+ } else {
962
+ item.content.clone()
963
+ };
964
+
965
+ println!(" {}", content_preview);
966
+
967
+ if let Some(tokens) = item.tokens_used {
968
+ println!(" {} tokens used", tokens.to_string().dimmed());
969
+ }
970
+ println!();
971
+ }
972
+ }
973
+
974
+ /// Print session status
975
+ fn print_session_status(session: &InteractiveSession) {
976
+ println!("{}", "Session Status:".bright_cyan().bold());
977
+ println!(" Session ID: {}", session.session_id.bright_white());
978
+ println!(" Model: {}", session.model.bright_cyan());
979
+ println!(
980
+ " Temperature: {}",
981
+ session.temperature.to_string().bright_yellow()
982
+ );
983
+ println!(
984
+ " Max tokens: {}",
985
+ session.max_tokens.to_string().bright_yellow()
986
+ );
987
+ println!(
988
+ " Messages: {}",
989
+ session
990
+ .conversation_history
991
+ .len()
992
+ .to_string()
993
+ .bright_green()
994
+ );
995
+ println!(
996
+ " Total tokens used: {}",
997
+ session.total_tokens_used.to_string().bright_red()
998
+ );
999
+ println!(
1000
+ " Directory: {}",
1001
+ session
1002
+ .current_directory
1003
+ .display()
1004
+ .to_string()
1005
+ .bright_magenta()
1006
+ );
1007
+
1008
+ if let Some(system) = &session.system_prompt {
1009
+ println!(" System prompt: {}", system.bright_green());
1010
+ }
1011
+ println!();
1012
+ }
1013
+
1014
+ /// Send message to Grok and handle response
1015
+ async fn send_to_grok(
1016
+ client: &GrokClient,
1017
+ session: &mut InteractiveSession,
1018
+ input: &str,
1019
+ ) -> Result<()> {
1020
+ // Add user message to history
1021
+ session.add_conversation_item("user", input, None);
1022
+
1023
+ // Show thinking indicator
1024
+ print!("{} ", "Thinking...".bright_yellow());
1025
+ io::stdout().flush()?;
1026
+
1027
+ // Prepare messages for API
1028
+ let mut messages = vec![];
1029
+
1030
+ if let Some(system) = &session.system_prompt {
1031
+ messages.push(json!({
1032
+ "role": "system",
1033
+ "content": system
1034
+ }));
1035
+ }
1036
+
1037
+ // Add conversation history (keep last 10 messages to avoid context overflow)
1038
+ let recent_history = session
1039
+ .conversation_history
1040
+ .iter()
1041
+ .rev()
1042
+ .take(10)
1043
+ .rev()
1044
+ .collect::<Vec<_>>();
1045
+
1046
+ for item in recent_history {
1047
+ messages.push(json!({
1048
+ "role": item.role,
1049
+ "content": item.content
1050
+ }));
1051
+ }
1052
+
1053
+ // Get tool definitions for function calling
1054
+ let tools = tools::get_tool_definitions();
1055
+
1056
+ // Set up security policy with current directory as trusted
1057
+ let mut security = SecurityPolicy::new();
1058
+ security.add_trusted_directory(&session.current_directory);
1059
+
1060
+ // Send request using the existing client method with tools
1061
+ match client
1062
+ .chat_completion_with_history(
1063
+ &messages,
1064
+ session.temperature,
1065
+ session.max_tokens,
1066
+ &session.model,
1067
+ Some(tools),
1068
+ )
1069
+ .await
1070
+ {
1071
+ Ok(response_msg) => {
1072
+ clear_current_line();
1073
+
1074
+ // Handle tool calls if present
1075
+ if let Some(tool_calls) = &response_msg.tool_calls {
1076
+ if !tool_calls.is_empty() {
1077
+ println!("{}", "Grok is executing operations...".blue().bold());
1078
+ println!();
1079
+
1080
+ for tool_call in tool_calls {
1081
+ if let Err(e) = execute_tool_call_interactive(tool_call, &security) {
1082
+ eprintln!(" {} Tool execution failed: {}", "✗".red(), e);
1083
+ }
1084
+ }
1085
+
1086
+ println!();
1087
+ println!("{}", "All operations completed!".green().bold());
1088
+ println!();
1089
+
1090
+ // Add assistant's response to history
1091
+ let content = response_msg
1092
+ .content
1093
+ .unwrap_or_else(|| "Operations completed.".to_string());
1094
+ session.add_conversation_item("assistant", &content, None);
1095
+ return Ok(());
1096
+ }
1097
+ }
1098
+
1099
+ let content = response_msg.content.unwrap_or_default();
1100
+
1101
+ // Print Grok's response with nice formatting
1102
+ println!("{} {}", "🤖".bright_blue(), "Grok:".bright_blue().bold());
1103
+ println!();
1104
+ println!("{}", content);
1105
+ println!();
1106
+
1107
+ // Add to conversation history
1108
+ session.add_conversation_item("assistant", &content, None);
1109
+ }
1110
+ Err(e) => {
1111
+ clear_current_line();
1112
+ return Err(e);
1113
+ }
1114
+ }
1115
+
1116
+ Ok(())
1117
+ }
1118
+
1119
+ /// Execute a tool call in interactive mode
1120
+ fn execute_tool_call_interactive(
1121
+ tool_call: &crate::ToolCall,
1122
+ security: &SecurityPolicy,
1123
+ ) -> Result<()> {
1124
+ use anyhow::anyhow;
1125
+
1126
+ let name = &tool_call.function.name;
1127
+ let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
1128
+
1129
+ match name.as_str() {
1130
+ "write_file" => {
1131
+ let path = args["path"]
1132
+ .as_str()
1133
+ .ok_or_else(|| anyhow!("Missing path"))?;
1134
+ let content = args["content"]
1135
+ .as_str()
1136
+ .ok_or_else(|| anyhow!("Missing content"))?;
1137
+ let result = tools::write_file(path, content, security)?;
1138
+ println!(" {} {}", "✓".green(), result);
1139
+ }
1140
+ "read_file" => {
1141
+ let path = args["path"]
1142
+ .as_str()
1143
+ .ok_or_else(|| anyhow!("Missing path"))?;
1144
+ let content = tools::read_file(path, security)?;
1145
+ println!(
1146
+ " {} Read {} bytes from {}",
1147
+ "✓".green(),
1148
+ content.len(),
1149
+ path
1150
+ );
1151
+ }
1152
+ "replace" => {
1153
+ let path = args["path"]
1154
+ .as_str()
1155
+ .ok_or_else(|| anyhow!("Missing path"))?;
1156
+ let old = args["old_string"]
1157
+ .as_str()
1158
+ .ok_or_else(|| anyhow!("Missing old_string"))?;
1159
+ let new = args["new_string"]
1160
+ .as_str()
1161
+ .ok_or_else(|| anyhow!("Missing new_string"))?;
1162
+ let expected = args
1163
+ .get("expected_replacements")
1164
+ .and_then(|v| v.as_u64())
1165
+ .map(|v| v as u32);
1166
+ let result = tools::replace(path, old, new, expected, security)?;
1167
+ println!(" {} {}", "✓".green(), result);
1168
+ }
1169
+ "list_directory" => {
1170
+ let path = args["path"]
1171
+ .as_str()
1172
+ .ok_or_else(|| anyhow!("Missing path"))?;
1173
+ let result = tools::list_directory(path, security)?;
1174
+ println!(" {} Directory contents of {}:", "✓".green(), path);
1175
+ for line in result.lines() {
1176
+ println!(" {}", line);
1177
+ }
1178
+ }
1179
+ "glob_search" => {
1180
+ let pattern = args["pattern"]
1181
+ .as_str()
1182
+ .ok_or_else(|| anyhow!("Missing pattern"))?;
1183
+ let result = tools::glob_search(pattern, security)?;
1184
+ println!(" {} Files matching '{}':", "✓".green(), pattern);
1185
+ for line in result.lines() {
1186
+ println!(" {}", line);
1187
+ }
1188
+ }
1189
+ "save_memory" => {
1190
+ let fact = args["fact"]
1191
+ .as_str()
1192
+ .ok_or_else(|| anyhow!("Missing fact"))?;
1193
+ let result = tools::save_memory(fact)?;
1194
+ println!(" {} {}", "✓".green(), result);
1195
+ }
1196
+ "run_shell_command" => {
1197
+ let command = args["command"]
1198
+ .as_str()
1199
+ .ok_or_else(|| anyhow!("Missing command"))?;
1200
+ println!(" {} Executing: {}", "⚙".cyan(), command);
1201
+ let result = tools::run_shell_command(command, security)?;
1202
+ println!(" {} Command output:", "✓".green());
1203
+ for line in result.lines() {
1204
+ println!(" {}", line);
1205
+ }
1206
+ }
1207
+ _ => {
1208
+ println!(" {} Unsupported tool: {}", "⚠".yellow(), name);
1209
+ }
1210
+ }
1211
+
1212
+ Ok(())
1213
+ }
1214
+
1215
+ /// Check if current directory is the home directory
1216
+ fn is_home_directory(current_dir: &PathBuf) -> bool {
1217
+ if let Some(home) = dirs::home_dir() {
1218
+ current_dir == &home
1219
+ } else {
1220
+ false
1221
+ }
1222
+ }
1223
+
1224
+ #[cfg(test)]
1225
+ mod tests {
1226
+ use super::*;
1227
+
1228
+ #[test]
1229
+ fn test_session_creation() {
1230
+ let session = InteractiveSession::new("grok-3".to_string(), None);
1231
+ assert_eq!(session.model, "grok-3");
1232
+ assert!(session.conversation_history.is_empty());
1233
+ assert_eq!(session.total_tokens_used, 0);
1234
+ }
1235
+
1236
+ #[test]
1237
+ fn test_add_conversation_item() {
1238
+ let mut session = InteractiveSession::new("grok-3".to_string(), None);
1239
+ session.add_conversation_item("user", "Hello", Some(10));
1240
+
1241
+ assert_eq!(session.conversation_history.len(), 1);
1242
+ assert_eq!(session.total_tokens_used, 10);
1243
+ assert_eq!(session.conversation_history[0].content, "Hello");
1244
+ }
1245
+
1246
+ #[test]
1247
+ fn test_generate_session_id() {
1248
+ let id1 = generate_session_id();
1249
+ let id2 = generate_session_id();
1250
+
1251
+ assert!(id1.starts_with("grok-"));
1252
+ assert!(id2.starts_with("grok-"));
1253
+ assert_ne!(id1, id2); // Should be different due to timestamp
1254
+ }
1255
+ }