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};
15use crate::utils::{extract_commands, prettify};
17
18#[derive(Parser, Debug)]
20#[clap(name = "deepseek", about = "DeepSeek CLI client")]
21struct Args {
22 #[clap(long)]
24 search: bool,
25
26 #[clap(long)]
28 no_thinking: bool,
29
30 #[clap(long, conflicts_with = "haiku")]
32 opus: bool,
33
34 #[clap(long, conflicts_with = "opus")]
36 haiku: bool,
37}
38
39pub async fn run() -> Result<()> {
41 let args = Args::parse();
42
43 let config_dir = dirs::config_dir()
45 .ok_or_else(|| anyhow!("Could not determine config directory"))?
46 .join("toast")
47 .join("deepseek");
48
49 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 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 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 let model = if args.opus {
102 "deepseek-coder"
103 } else if args.haiku {
104 "deepseek-lite"
105 } else {
106 "deepseek-chat" };
108
109 let mut deepseek = DeepSeek::new_with_model(session, model)?;
110
111 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 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 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 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 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(&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
176async 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 process_commands_internal(deepseek, chat_id, response, thinking_mode, search_mode, 0).await
186}
187
188fn 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 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 let (reads, execs) = extract_commands(response);
207
208 if reads.is_empty() && execs.is_empty() {
209 return Ok(());
210 }
211
212 sleep(Duration::from_millis(500)).await;
214
215 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_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 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_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 }) }
287
288fn 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
314fn 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}