toast_api/
deepseek_cli.rs

1use anyhow::{anyhow, Context, Result};
2use clap::Parser;
3use ctrlc;
4use std::fs;
5use std::io::{self, stdout, Write};
6use std::process;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::time::sleep;
11use std::future::Future;
12use std::pin::Pin;
13
14use crate::deepseek::{DeepSeek, Session, SearchMode, ThinkingMode};
15// No need to import from config as we're using string literals directly
16use crate::utils::{extract_commands, prettify};
17
18/// CLI arguments for the DeepSeek interface
19#[derive(Parser, Debug)]
20#[clap(name = "deepseek", about = "DeepSeek CLI client")]
21struct Args {
22    /// Enable web search
23    #[clap(long)]
24    search: bool,
25    
26    /// Disable thinking mode
27    #[clap(long)]
28    no_thinking: bool,
29
30    /// Use opus model
31    #[clap(long, conflicts_with = "haiku")]
32    opus: bool,
33
34    /// Use haiku model
35    #[clap(long, conflicts_with = "opus")]
36    haiku: bool,
37}
38
39/// Run the DeepSeek CLI application
40pub async fn run() -> Result<()> {
41    let args = Args::parse();
42    
43    // Load session values from config files
44    let config_dir = dirs::config_dir()
45        .ok_or_else(|| anyhow!("Could not determine config directory"))?
46        .join("toast")
47        .join("deepseek");
48    
49    // Create config directory if it doesn't exist
50    if !config_dir.exists() {
51        fs::create_dir_all(&config_dir)?;
52    }
53    
54    let auth_token_path = config_dir.join("auth_token");
55    let cookies_path = config_dir.join("cookies.json");
56    
57    // Check auth token
58    let auth_token = if auth_token_path.exists() {
59        fs::read_to_string(&auth_token_path)
60            .context(format!("Failed to read auth token from {:?}", auth_token_path))?
61            .trim()
62            .to_string()
63    } else {
64        return Err(anyhow!(
65            "Auth token file not found at {:?}\n\n{}",
66            auth_token_path,
67            get_config_help("deepseek_auth_token")
68        ));
69    };
70    
71    // Check cookies
72    let cookies = if cookies_path.exists() {
73        serde_json::from_str(&fs::read_to_string(&cookies_path)
74            .context(format!("Failed to read cookies from {:?}", cookies_path))?)?
75    } else {
76        return Err(anyhow!(
77            "Cookies file not found at {:?}\n\n{}",
78            cookies_path,
79            get_config_help("deepseek_cookies")
80        ));
81    };
82    
83    let session = Session {
84        auth_token,
85        cookies,
86    };
87    
88    let thinking_mode = if args.no_thinking {
89        ThinkingMode::Disabled
90    } else {
91        ThinkingMode::Detailed
92    };
93    
94    let search_mode = if args.search {
95        SearchMode::Enabled
96    } else {
97        SearchMode::Disabled
98    };
99    
100    // Determine model based on flags
101    let model = if args.opus {
102        "deepseek-coder"
103    } else if args.haiku {
104        "deepseek-lite"
105    } else {
106        "deepseek-chat" // Default model
107    };
108    
109    let mut deepseek = DeepSeek::new_with_model(session, model)?;
110    
111    // Set up Ctrl-C handler
112    let running = Arc::new(AtomicBool::new(true));
113    {
114        let running = running.clone();
115        ctrlc::set_handler(move || {
116            running.store(false, Ordering::SeqCst);
117            println!("\nGoodbye!");
118            process::exit(0);
119        })?;
120    }
121    
122    let stdin = io::stdin();
123    let mut stdout = io::stdout();
124    
125    // Create a new chat session
126    println!("Starting new DeepSeek chat session...");
127    let chat_id = match deepseek.create_chat_session().await {
128        Ok(id) => {
129            println!("Session started!\n");
130            id
131        }
132        Err(e) => {
133            return Err(anyhow!("Failed to create chat session: {}", e));
134        }
135    };
136    
137    // Main chat loop
138    while running.load(Ordering::SeqCst) {
139        print!("You: ");
140        stdout.flush()?;
141        
142        let mut buf = String::new();
143        stdin.read_line(&mut buf)?;
144        let input = buf.trim_end();
145        
146        // Check for empty input or exit commands
147        if input.is_empty() {
148            continue;
149        }
150        
151        if input.eq_ignore_ascii_case("/exit") || input.eq_ignore_ascii_case("exit") || input == "x" {
152            break;
153        }
154        
155        // Send message to API
156        print!("DeepSeek: ");
157        stdout.flush()?;
158        
159        match deepseek.chat_completion(&chat_id, input, None, thinking_mode.clone(), search_mode.clone()).await {
160            Ok(response) => {
161                println!("{}", prettify(&response));
162                
163                // Process commands in the response
164                process_commands(&mut deepseek, &chat_id, &response, thinking_mode.clone(), search_mode.clone()).await?;
165            }
166            Err(e) => {
167                eprintln!("\nError: {}", e);
168            }
169        }
170        println!();
171    }
172    
173    Ok(())
174}
175
176// Process commands in DeepSeek's response
177async fn process_commands(
178    deepseek: &mut DeepSeek,
179    chat_id: &str,
180    response: &str,
181    thinking_mode: ThinkingMode,
182    search_mode: SearchMode,
183) -> Result<()> {
184    // Handle commands with max depth to avoid infinite recursion
185    process_commands_internal(deepseek, chat_id, response, thinking_mode, search_mode, 0).await
186}
187
188// Internal implementation with max depth control using Pin<Box<dyn Future>>
189fn process_commands_internal<'a>(
190    deepseek: &'a mut DeepSeek,
191    chat_id: &'a str,
192    response: &'a str,
193    thinking_mode: ThinkingMode,
194    search_mode: SearchMode,
195    depth: u8,
196) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
197    Box::pin(async move {
198    // Limit recursion depth
199    const MAX_DEPTH: u8 = 5;
200    if depth >= MAX_DEPTH {
201        println!("Maximum command processing depth reached ({}). Stopping recursion.", MAX_DEPTH);
202        return Ok(());
203    }
204    
205    // Extract read_file and exec commands
206    let (reads, execs) = extract_commands(response);
207    
208    if reads.is_empty() && execs.is_empty() {
209        return Ok(());
210    }
211    
212    // Short pause before processing commands
213    sleep(Duration::from_millis(500)).await;
214    
215    // Process file reads
216    if !reads.is_empty() {
217        let mut file_contents = Vec::new();
218        
219        for path in &reads {
220            match fs::read_to_string(path) {
221                Ok(content) => {
222                    file_contents.push(format!("=== File: {} ===\n{}", path, content));
223                }
224                Err(e) => {
225                    file_contents.push(format!("Error reading file {}: {}", path, e));
226                }
227            }
228        }
229        
230        let file_message = format!("Here are the contents of the files you requested:\n\n{}", 
231                                  file_contents.join("\n\n"));
232        
233        print!("Sending file contents... ");
234        stdout().flush()?;
235        
236        match deepseek.chat_completion(chat_id, &file_message, None, thinking_mode, search_mode).await {
237            Ok(response) => {
238                println!("Done!");
239                println!("DeepSeek: {}", prettify(&response));
240                
241                // Process next level of commands
242                process_commands_internal(deepseek, chat_id, &response, thinking_mode, search_mode, depth + 1).await?;
243            }
244            Err(e) => {
245                println!("Error: {}", e);
246            }
247        }
248    }
249    
250    // Process exec commands
251    if !execs.is_empty() {
252        for cmd in &execs {
253            println!("\nExecuting: {}", cmd);
254            
255            match execute_command(cmd) {
256                Ok(output) => {
257                    println!("{}", output);
258                    
259                    print!("Sending command results... ");
260                    stdout().flush()?;
261                    
262                    let cmd_message = format!("Command executed: {}\n\nOutput:\n{}", cmd, output);
263                    
264                    match deepseek.chat_completion(chat_id, &cmd_message, None, thinking_mode, search_mode).await {
265                        Ok(response) => {
266                            println!("Done!");
267                            println!("DeepSeek: {}", prettify(&response));
268                            
269                            // Process next level of commands
270                            process_commands_internal(deepseek, chat_id, &response, thinking_mode, search_mode, depth + 1).await?;
271                        }
272                        Err(e) => {
273                            println!("Error: {}", e);
274                        }
275                    }
276                }
277                Err(e) => {
278                    println!("Error executing command: {}", e);
279                }
280            }
281        }
282    }
283    
284    Ok(())
285    }) // Close the Box::pin(async move {}) block
286}
287
288// Execute a shell command and return the output
289fn execute_command(command: &str) -> Result<String> {
290    let output = process::Command::new("sh")
291        .arg("-c")
292        .arg(command)
293        .output()?;
294    
295    let mut result = String::new();
296    
297    if !output.stdout.is_empty() {
298        result.push_str("=== STDOUT ===\n");
299        result.push_str(&String::from_utf8_lossy(&output.stdout));
300        result.push('\n');
301    }
302    
303    if !output.stderr.is_empty() {
304        result.push_str("=== STDERR ===\n");
305        result.push_str(&String::from_utf8_lossy(&output.stderr));
306        result.push('\n');
307    }
308    
309    result.push_str(&format!("Exit code: {}", output.status.code().unwrap_or(-1)));
310    
311    Ok(result)
312}
313
314// Configuration help text
315fn get_config_help(file_name: &str) -> String {
316    match file_name {
317        "deepseek_auth_token" => "To get your DeepSeek auth token:
3181. Go to chat.deepseek.com in your browser
3192. Log in to your account
3203. Open Developer Tools (F12 or right-click and select 'Inspect')
3214. Go to the Network tab
3225. Refresh the page or make a request
3236. Look for requests to the DeepSeek API
3247. In the 'Headers' tab, find 'Request Headers'
3258. Look for the 'Authorization' header with format 'Bearer {token}'
3269. Copy the token part (without 'Bearer ') and save it to this folder with filename: auth_token".to_string(),
327        
328        "deepseek_cookies" => "The DeepSeek API requires Cloudflare cookies to bypass protection:
3291. Run the Python script from the deepseek4free/dsk folder:
330   python bypass.py
3312. Copy the generated cookies.json file to this folder".to_string(),
332        
333        _ => format!("Configuration file {} is missing.", file_name),
334    }
335}