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
|
@@ -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
|
+
}
|