1use crate::utils::extract_org_id_from_cookie;
2use anyhow::{anyhow, Context, Result};
3use ctrlc;
4use std::fs;
5use std::future::Future;
6use std::io::{self, Write};
7use std::path::Path;
8use std::pin::Pin;
9use std::process::{self, Command, Stdio};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::Arc;
12
13use crate::api::{Attachment, Claude, Session as ClaudeSession};
14use crate::config::{HAIKU_MODEL, MAX_INTERNAL_ITERS, OPUS_MODEL, SONNET_MODEL, SYSTEM_PROMPT};
15use crate::deepseek::{DeepSeek, Session as DeepSeekSession};
16use crate::utils::{extract_commands, prettify};
17use log::debug;
18
19#[derive(Debug)]
21pub struct UnifiedArgs {
22 pub use_deepseek: bool,
23 pub use_opus: bool,
24 pub use_haiku: bool,
25}
26
27fn execute_command(command: &str) -> Result<String> {
29 let result = Command::new("sh")
30 .arg("-c")
31 .arg(command)
32 .stdout(Stdio::piped())
33 .stderr(Stdio::piped())
34 .spawn()?;
35
36 let output = result.wait_with_output()?;
37 let mut msg = String::new();
38
39 if !output.stdout.is_empty() {
40 msg.push_str("=== STDOUT ===\n");
41 msg.push_str(&String::from_utf8_lossy(&output.stdout));
42 msg.push('\n');
43 }
44
45 if !output.stderr.is_empty() {
46 msg.push_str("=== STDERR ===\n");
47 msg.push_str(&String::from_utf8_lossy(&output.stderr));
48 msg.push('\n');
49 }
50
51 msg.push_str(&format!(
52 "Exit code: {}",
53 output.status.code().unwrap_or(-1)
54 ));
55
56 Ok(msg)
57}
58
59fn collect_claude_attachments(paths: &[&str]) -> Result<Vec<Attachment>> {
61 const LIMIT: usize = 5;
62 const SIZE_LIMIT: u64 = 10 * 1024 * 1024;
63
64 if paths.len() > LIMIT {
65 return Err(anyhow!("cannot attach more than {LIMIT} files"));
66 }
67
68 let mut atts = Vec::new();
69
70 for p in paths {
71 if let Ok(meta) = fs::metadata(p) {
72 if meta.len() > SIZE_LIMIT {
73 eprintln!("Warning: file {p} is larger than 10 MB, skipping");
74 continue;
75 }
76
77 if let Ok(content) = fs::read_to_string(p) {
78 atts.push(Attachment {
79 file_name: Path::new(p)
80 .file_name()
81 .unwrap_or_default()
82 .to_string_lossy()
83 .into(),
84 size: meta.len(),
85 content,
86 });
87 } else {
88 eprintln!("Warning: couldn't read file {p}");
89 }
90 } else {
91 eprintln!("Warning: couldn't access file {p}");
92 }
93 }
94
95 Ok(atts)
96}
97
98pub async fn run(args: UnifiedArgs) -> Result<()> {
100 let running = Arc::new(AtomicBool::new(true));
102 {
103 let running = running.clone();
104 ctrlc::set_handler(move || {
105 running.store(false, Ordering::SeqCst);
106 println!("\nGoodbye!");
107 process::exit(0);
108 })?;
109 }
110
111 if args.use_deepseek {
112 run_deepseek(args, running).await
113 } else {
114 run_claude(args, running).await
115 }
116}
117
118async fn run_deepseek(args: UnifiedArgs, running: Arc<AtomicBool>) -> Result<()> {
120 let config_dir = dirs::config_dir()
122 .ok_or_else(|| anyhow!("Could not determine config directory"))?
123 .join("toast")
124 .join("deepseek");
125
126 if !config_dir.exists() {
128 fs::create_dir_all(&config_dir)?;
129 }
130
131 let auth_token_path = config_dir.join("auth_token");
132 let cookies_path = config_dir.join("cookies.json");
133
134 let auth_token = if auth_token_path.exists() {
136 fs::read_to_string(&auth_token_path)
137 .context(format!(
138 "Failed to read auth token from {auth_token_path:?}"
139 ))?
140 .trim()
141 .to_string()
142 } else {
143 return Err(anyhow!(
144 "Auth token file not found at {:?}\n\nTo get your DeepSeek auth token:\n1. Go to chat.deepseek.com in your browser\n2. Open Developer Tools (F12)\n3. Go to Network tab\n4. Look for Authorization header in any request\n5. Save the token part (without 'Bearer ') to this file",
145 auth_token_path
146 ));
147 };
148
149 let cookies = if cookies_path.exists() {
151 serde_json::from_str(
152 &fs::read_to_string(&cookies_path)
153 .context(format!("Failed to read cookies from {cookies_path:?}"))?,
154 )?
155 } else {
156 return Err(anyhow!(
157 "Cookies file not found at {:?}\n\nDeepSeek requires Cloudflare cookies.\nUse the deepseek4free library to generate them.",
158 cookies_path
159 ));
160 };
161
162 let session = DeepSeekSession {
163 auth_token,
164 cookies,
165 };
166
167 let model = if args.use_opus {
169 "deepseek-r1" } else if args.use_haiku {
171 "deepseek-lite"
172 } else {
173 "deepseek-r1" };
175
176 let mut deepseek = DeepSeek::new(session)?;
177
178 let stdin = io::stdin();
179 let mut stdout = io::stdout();
180
181 let mut system_prompt_sent = false;
183
184 println!("Starting new DeepSeek chat session...");
186 let chat_id = match deepseek.create_chat_session().await {
187 Ok(id) => {
188 println!("Session started with DeepSeek!\n");
189 id
190 }
191 Err(e) => {
192 return Err(anyhow!("Failed to create DeepSeek chat session: {}", e));
193 }
194 };
195
196 let thinking_mode = if model == "deepseek-r1" {
198 crate::deepseek::ThinkingMode::Detailed
199 } else {
200 crate::deepseek::ThinkingMode::Simple
201 };
202 let search_mode = crate::deepseek::SearchMode::Disabled;
203
204 while running.load(Ordering::SeqCst) {
206 print!("You: ");
207 stdout.flush()?;
208
209 let mut buf = String::new();
210 match stdin.read_line(&mut buf) {
211 Ok(0) => {
212 println!("\nGoodbye!");
214 break;
215 }
216 Ok(_) => {
217 let input = buf.trim_end();
218
219 if input.is_empty() {
221 continue;
222 }
223
224 if input.eq_ignore_ascii_case("/exit")
225 || input.eq_ignore_ascii_case("exit")
226 || input == "x"
227 {
228 break;
229 }
230
231 print!("DeepSeek: ");
233 stdout.flush()?;
234
235 debug!("Sending to DeepSeek API...");
236
237 let system_prompt = if !system_prompt_sent {
239 system_prompt_sent = true;
240 Some(SYSTEM_PROMPT)
241 } else {
242 None
243 };
244
245 match deepseek
246 .chat_completion(
247 &chat_id,
248 input,
249 None,
250 thinking_mode,
251 search_mode,
252 system_prompt,
253 )
254 .await
255 {
256 Ok(response) => {
257 debug!("Got response, length: {}", response.len());
258 println!("{}", prettify(&response));
259
260 process_deepseek_commands(
262 &mut deepseek,
263 &chat_id,
264 &response,
265 thinking_mode,
266 search_mode,
267 )
268 .await?;
269 }
270 Err(e) => {
271 debug!("DeepSeek API error: {e}");
272 eprintln!("\nError: {e}");
273 }
274 }
275 println!();
276 }
277 Err(e) => {
278 eprintln!("Failed to read input: {e}");
279 break;
280 }
281 }
282 }
283
284 Ok(())
285}
286
287async fn process_deepseek_commands(
289 deepseek: &mut DeepSeek,
290 chat_id: &str,
291 response: &str,
292 thinking_mode: crate::deepseek::ThinkingMode,
293 search_mode: crate::deepseek::SearchMode,
294) -> Result<()> {
295 process_deepseek_commands_internal(deepseek, chat_id, response, thinking_mode, search_mode, 0)
296 .await
297}
298
299fn process_deepseek_commands_internal<'a>(
300 deepseek: &'a mut DeepSeek,
301 chat_id: &'a str,
302 response: &'a str,
303 thinking_mode: crate::deepseek::ThinkingMode,
304 search_mode: crate::deepseek::SearchMode,
305 depth: usize,
306) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
307 Box::pin(async move {
308 const MAX_DEPTH: usize = 20;
310 if depth >= MAX_DEPTH {
311 println!(
312 "Maximum command processing depth reached ({MAX_DEPTH}). Returning to user."
313 );
314 return Ok(());
315 }
316
317 let (reads, execs) = extract_commands(response);
319
320 if reads.is_empty() && execs.is_empty() {
321 return Ok(());
322 }
323
324 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
326
327 if !reads.is_empty() {
329 let mut file_contents = Vec::new();
330
331 for path in &reads {
332 match fs::read_to_string(path) {
333 Ok(content) => {
334 file_contents.push(format!("=== File: {path} ===\n{content}"));
335 }
336 Err(e) => {
337 file_contents.push(format!("Error reading file {path}: {e}"));
338 }
339 }
340 }
341
342 let file_message = format!(
343 "Here are the contents of the files you requested:\n\n{}",
344 file_contents.join("\n\n")
345 );
346
347 io::stdout().flush()?;
349
350 match deepseek
351 .chat_completion(
352 chat_id,
353 &file_message,
354 None,
355 thinking_mode,
356 search_mode,
357 None,
358 )
359 .await
360 {
361 Ok(response) => {
362 println!("Done!");
363 println!("DeepSeek: {}", prettify(&response));
364
365 process_deepseek_commands_internal(
367 deepseek,
368 chat_id,
369 &response,
370 thinking_mode,
371 search_mode,
372 depth + 1,
373 )
374 .await?;
375 }
376 Err(e) => {
377 println!("Error: {e}");
378 }
379 }
380 }
381
382 if !execs.is_empty() {
384 for cmd in &execs {
385 println!("\nExecuting: {cmd}");
386
387 match execute_command(cmd) {
388 Ok(output) => {
389 println!("{output}");
390
391 print!("Sending command results... ");
392 io::stdout().flush()?;
393
394 let cmd_message =
395 format!("Command executed: {cmd}\n\nOutput:\n{output}");
396
397 match deepseek
398 .chat_completion(
399 chat_id,
400 &cmd_message,
401 None,
402 thinking_mode,
403 search_mode,
404 None,
405 )
406 .await
407 {
408 Ok(response) => {
409 println!("Done!");
410 println!("DeepSeek: {}", prettify(&response));
411
412 process_deepseek_commands_internal(
414 deepseek,
415 chat_id,
416 &response,
417 thinking_mode,
418 search_mode,
419 depth + 1,
420 )
421 .await?;
422 }
423 Err(e) => {
424 println!("Error: {e}");
425 }
426 }
427 }
428 Err(e) => {
429 println!("Error executing command: {e}");
430 }
431 }
432 }
433 }
434
435 Ok(())
436 })
437}
438
439async fn run_claude(args: UnifiedArgs, running: Arc<AtomicBool>) -> Result<()> {
441 let config_dir = dirs::config_dir()
443 .ok_or_else(|| anyhow!("Could not determine config directory"))?
444 .join("toast");
445
446 let cookie_path = config_dir.join("cookie");
447 let org_id_path = config_dir.join("org_id");
448
449 if !config_dir.exists() {
451 fs::create_dir_all(&config_dir).context(format!(
452 "Failed to create config directory at {config_dir:?}"
453 ))?;
454 return Err(anyhow!(
455 "Configuration directory created at {:?}\n\nPlease create a cookie file with your Claude cookie",
456 config_dir,
457 ));
458 }
459
460 let cookie = if cookie_path.exists() {
462 fs::read_to_string(&cookie_path)
463 .context(format!("Failed to read cookie from {cookie_path:?}"))?
464 .trim()
465 .to_string()
466 } else {
467 return Err(anyhow!("Cookie file not found at {:?}", cookie_path,));
468 };
469
470 let org_id = if org_id_path.exists() {
472 fs::read_to_string(&org_id_path)
473 .context(format!(
474 "Failed to read organization ID from {org_id_path:?}"
475 ))?
476 .trim()
477 .to_string()
478 } else {
479 if let Some(extracted_org_id) = extract_org_id_from_cookie(&cookie) {
481 fs::write(&org_id_path, &extracted_org_id).context(format!(
483 "Failed to write organization ID to {org_id_path:?}"
484 ))?;
485 println!(
486 "Extracted organization ID from cookie and saved to {org_id_path:?}"
487 );
488 extracted_org_id
489 } else {
490 return Err(anyhow!(
491 "Organization ID file not found at {:?} and couldn't extract it from cookie.",
492 org_id_path,
493 ));
494 }
495 };
496
497 let user_agent =
498 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0"
499 .to_string();
500
501 let session = ClaudeSession {
502 cookie,
503 user_agent,
504 organization_id: org_id,
505 };
506
507 let model: &str = if args.use_opus {
509 OPUS_MODEL
510 } else if args.use_haiku {
511 HAIKU_MODEL
512 } else {
513 SONNET_MODEL
514 };
515
516 let claude = Claude::new(session.clone(), model)?;
517 println!("Starting new Claude chat session using model: {model}");
518
519 let stdin = io::stdin();
520 let mut stdout = io::stdout();
521 let mut chat_id = String::new();
522 let mut system_prompt_sent = false;
523
524 while running.load(Ordering::SeqCst) {
525 print!("You: ");
526 stdout.flush()?;
527 let mut buf = String::new();
528 stdin.read_line(&mut buf)?;
529 let input = buf.trim_end();
530 if input.is_empty() {
531 continue;
532 }
533 if input.eq_ignore_ascii_case("/exit") || input.eq_ignore_ascii_case("exit") || input == "x"
534 {
535 if !chat_id.is_empty() {
536 claude.delete_chat(&chat_id).await.ok();
537 }
538 break;
539 }
540
541 if chat_id.is_empty() {
543 chat_id = claude.create_chat().await.context("creating chat")?;
544 }
545
546 if let Some(caps) = crate::utils::EXEC_RE.captures(input) {
548 let cmd = caps[1].to_string();
549 if !system_prompt_sent {
550 claude
551 .send_message(&chat_id, SYSTEM_PROMPT, &[])
552 .await
553 .context("sending system prompt")?;
554 system_prompt_sent = true;
555 }
556
557 match execute_command(&cmd) {
558 Ok(output) => {
559 let msg = format!("Command executed: {cmd}\n\n{output}");
560 let ans = claude.send_message(&chat_id, &msg, &[]).await?;
561 println!("Claude:\n{}", prettify(&ans));
562 process_claude_commands(&claude, &chat_id, &ans).await?;
563 }
564 Err(e) => {
565 eprintln!("Warning: command execution failed: {e}");
566 let msg = format!("Command execution failed: {e}");
567 let ans = claude.send_message(&chat_id, &msg, &[]).await?;
568 println!("Claude:\n{}", prettify(&ans));
569 }
570 }
571 continue;
572 }
573
574 if let Some(caps) = crate::utils::READ_RE.captures(input) {
576 let paths: Vec<String> = caps[1].split_whitespace().map(String::from).collect();
577 let path_refs: Vec<&str> = paths.iter().map(String::as_str).collect();
578 if !system_prompt_sent {
579 claude
580 .send_message(&chat_id, SYSTEM_PROMPT, &[])
581 .await
582 .context("sending system prompt")?;
583 system_prompt_sent = true;
584 }
585
586 let rest = input.strip_prefix(&caps[0]).unwrap_or("").trim();
587 let attachments = collect_claude_attachments(&path_refs).unwrap_or_default();
588 let ans = claude
589 .send_message(&chat_id, rest, &attachments)
590 .await
591 .context("sending user message")?;
592
593 println!("Claude:\n{}", prettify(&ans));
594 process_claude_commands(&claude, &chat_id, &ans).await?;
595 } else {
596 if !system_prompt_sent {
598 claude
599 .send_message(&chat_id, SYSTEM_PROMPT, &[])
600 .await
601 .context("sending system prompt")?;
602 system_prompt_sent = true;
603 }
604
605 let ans = claude
606 .send_message(&chat_id, input, &[])
607 .await
608 .context("sending user message")?;
609
610 println!("Claude:\n{}", prettify(&ans));
611 process_claude_commands(&claude, &chat_id, &ans).await?;
612 }
613 }
614
615 Ok(())
616}
617
618async fn process_claude_commands(claude: &Claude, chat_id: &str, response: &str) -> Result<()> {
620 process_claude_commands_internal(claude, chat_id, response, 0).await
621}
622
623fn process_claude_commands_internal<'a>(
624 claude: &'a Claude,
625 chat_id: &'a str,
626 response: &'a str,
627 depth: usize,
628) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
629 Box::pin(async move {
630 if depth >= MAX_INTERNAL_ITERS {
632 println!("Max internal iterations reached, returning to user.");
633 return Ok(());
634 }
635
636 let (reads, execs) = extract_commands(response);
637 if reads.is_empty() && execs.is_empty() {
638 return Ok(());
639 }
640
641 if !reads.is_empty() {
642 let atts =
643 collect_claude_attachments(&reads.iter().map(String::as_str).collect::<Vec<_>>())
644 .unwrap_or_default();
645
646 match claude
647 .send_message(chat_id, "read_file response:", &atts)
648 .await
649 {
650 Ok(resp) => {
651 println!("Claude:\n{}", prettify(&resp));
652 return process_claude_commands_internal(claude, chat_id, &resp, depth + 1)
653 .await;
654 }
655 Err(e) => {
656 return Err(e);
657 }
658 }
659 }
660
661 if !execs.is_empty() {
662 let mut outputs = String::new();
663
664 for cmd in &execs {
665 match execute_command(cmd) {
666 Ok(output) => outputs.push_str(&output),
667 Err(e) => outputs.push_str(&format!("Command execution failed: {e}")),
668 }
669 outputs.push_str("\n\n---\n\n");
670 }
671
672 match claude.send_message(chat_id, &outputs, &[]).await {
673 Ok(resp) => {
674 println!("Claude:\n{}", prettify(&resp));
675 return process_claude_commands_internal(claude, chat_id, &resp, depth + 1)
676 .await;
677 }
678 Err(e) => {
679 return Err(e);
680 }
681 }
682 }
683
684 Ok(())
685 })
686}