toast_api/
agent_cli.rs

1//! Agent-based CLI implementation
2
3use crate::agent::{AgentConfig, AgentSession};
4use crate::api::{Claude, Session as ClaudeSession};
5use crate::deepseek::{DeepSeek, Session as DeepSeekSession};
6use crate::utils::prettify;
7use anyhow::{anyhow, Context, Result};
8use std::io::{self, Read, Write};
9use std::sync::atomic::Ordering;
10use std::sync::mpsc;
11use std::sync::Arc;
12use std::thread;
13use std::time::Duration;
14
15#[cfg(unix)]
16fn setup_raw_mode() -> Result<libc::termios> {
17    use std::os::unix::io::AsRawFd;
18    eprintln!("DEBUG: setup_raw_mode called");
19    
20    let stdin_fd = io::stdin().as_raw_fd();
21    
22    let original_termios = unsafe {
23        let mut t: libc::termios = std::mem::zeroed();
24        libc::tcgetattr(stdin_fd, &mut t);
25        t
26    };
27    
28    let mut raw_termios = original_termios;
29    raw_termios.c_lflag &= !(libc::ICANON | libc::ECHO);
30    raw_termios.c_cc[libc::VMIN] = 0;
31    raw_termios.c_cc[libc::VTIME] = 1;
32    
33    unsafe {
34        libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw_termios);
35    }
36    
37    eprintln!("DEBUG: raw mode set");
38    Ok(original_termios)
39}
40
41#[cfg(not(unix))]
42fn setup_raw_mode() -> Result<libc::termios> {
43    Err(anyhow!("Raw mode not supported on this platform"))
44}
45
46#[cfg(unix)]
47fn restore_raw_mode(original_termios: libc::termios) -> Result<()> {
48    use std::os::unix::io::AsRawFd;
49    
50    let stdin_fd = io::stdin().as_raw_fd();
51    unsafe {
52        libc::tcsetattr(stdin_fd, libc::TCSANOW, &original_termios);
53    }
54    eprintln!("DEBUG: raw mode restored");
55    Ok(())
56}
57
58#[cfg(not(unix))]
59fn restore_raw_mode(_: libc::termios) -> Result<()> {
60    Ok(())
61}
62
63#[cfg(unix)]
64fn read_char() -> Result<Option<u8>> {
65    let mut buf = [0u8; 1];
66    let mut stdin = io::stdin();
67    match stdin.read_exact(&mut buf) {
68        Ok(_) => Ok(Some(buf[0])),
69        Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
70        Err(e) => Err(anyhow!("Read error: {}", e)),
71    }
72}
73
74#[cfg(not(unix))]
75fn read_char() -> Result<Option<u8>> {
76    Ok(None)
77}
78
79fn setup_interrupt_monitor(interrupt_flag: Arc<std::sync::atomic::AtomicBool>) -> mpsc::Receiver<()> {
80    let (tx, rx) = mpsc::channel();
81    
82    thread::spawn(move || {
83        loop {
84            if let Ok(Some(ch)) = read_char() {
85                if ch == 27 { // ESC key
86                    interrupt_flag.store(true, Ordering::Relaxed);
87                    eprintln!("\nšŸ›‘ Interrupt detected (ESC)!");
88                    let _ = tx.send(());
89                    break;
90                }
91            }
92            thread::sleep(Duration::from_millis(10));
93        }
94    });
95    
96    rx
97}
98
99pub async fn run_agent_cli(
100    use_deepseek: bool,
101    use_opus: bool,
102    use_haiku: bool,
103) -> Result<()> {
104    println!("šŸ¤– Starting Toast Agent...\n");
105
106    let config = AgentConfig::default();
107    let mut session = AgentSession::new(config);
108
109    if use_deepseek {
110        run_with_deepseek(&mut session, use_opus, use_haiku).await
111    } else {
112        run_with_claude(&mut session, use_opus, use_haiku).await
113    }
114}
115
116async fn run_with_claude(
117    session: &mut AgentSession,
118    use_opus: bool,
119    use_haiku: bool,
120) -> Result<()> {
121    let config_dir = dirs::config_dir()
122        .ok_or_else(|| anyhow!("Could not determine config directory"))?
123        .join("toast");
124
125    let cookie = std::fs::read_to_string(config_dir.join("cookie"))
126        .context("Failed to read cookie")?
127        .trim()
128        .to_string();
129
130    let org_id = if let Ok(id) = std::fs::read_to_string(config_dir.join("org_id")) {
131        id.trim().to_string()
132    } else {
133        crate::utils::extract_org_id_from_cookie(&cookie)
134            .ok_or_else(|| anyhow!("Could not extract org_id from cookie"))?
135    };
136
137    let claude_session = ClaudeSession {
138        cookie,
139        user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0".to_string(),
140        organization_id: org_id,
141    };
142
143    let model = if use_opus {
144        crate::config::OPUS_MODEL
145    } else if use_haiku {
146        crate::config::HAIKU_MODEL
147    } else {
148        crate::config::SONNET_MODEL
149    };
150
151    let claude = Claude::new(claude_session, model)?;
152    println!("Connected to Claude ({model})");
153    println!("šŸ’” Press ESC during tool execution to interrupt\n");
154
155    let chat_id = claude.create_chat().await.context("Failed to create chat")?;
156    
157    let system_prompt = session.agent().get_system_prompt();
158    claude.send_message(&chat_id, &system_prompt, &[]).await
159        .context("Failed to send system prompt")?;
160
161    let original_termios = setup_raw_mode()?;
162
163    let interrupt_flag = session.interrupt_flag();
164    let _interrupt_rx = setup_interrupt_monitor(interrupt_flag);
165
166    let mut stdout = io::stdout();
167
168    loop {
169        print!("You: ");
170        stdout.flush()?;
171
172        let mut input = String::new();
173        
174        loop {
175            match read_char() {
176                Ok(Some(ch)) => {
177                    if ch == b'\n' || ch == b'\r' {
178                        println!();
179                        break;
180                    } else if ch == 27 {
181                        println!("\nā¹ļø Cancelled input");
182                        input.clear();
183                        break;
184                    } else if ch == 127 {
185                        if !input.is_empty() {
186                            input.pop();
187                            print!("\x08 \x08");
188                            stdout.flush()?;
189                        }
190                    } else if ch >= 32 && ch < 127 {
191                        input.push(ch as char);
192                        print!("{}", ch as char);
193                        stdout.flush()?;
194                    }
195                }
196                Ok(None) => {
197                    thread::sleep(Duration::from_millis(10));
198                }
199                Err(_) => break,
200            }
201        }
202
203        let input = input.trim();
204        if input.is_empty() {
205            continue;
206        }
207        if matches!(input, "exit" | "quit" | "/exit" | "x") {
208            break;
209        }
210
211        let response = claude.send_message(&chat_id, input, &[]).await
212            .context("Failed to send message")?;
213
214        println!("\nClaude: {}\n", prettify(&response));
215
216        process_agent_response_claude(session, &claude, &chat_id, &response).await?;
217        
218        session.agent().reset();
219    }
220
221    restore_raw_mode(original_termios)?;
222
223    claude.delete_chat(&chat_id).await.ok();
224    println!("\nšŸ‘‹ Goodbye!");
225    Ok(())
226}
227
228async fn run_with_deepseek(
229    session: &mut AgentSession,
230    use_opus: bool,
231    use_haiku: bool,
232) -> Result<()> {
233    let config_dir = dirs::config_dir()
234        .ok_or_else(|| anyhow!("Could not determine config directory"))?
235        .join("toast")
236        .join("deepseek");
237
238    let auth_token = std::fs::read_to_string(config_dir.join("auth_token"))
239        .context("Failed to read auth token")?
240        .trim()
241        .to_string();
242
243    let cookies = serde_json::from_str(
244        &std::fs::read_to_string(config_dir.join("cookies.json"))
245            .context("Failed to read cookies")?
246    )?;
247
248    let deepseek_session = DeepSeekSession { auth_token, cookies };
249    let mut deepseek = DeepSeek::new(deepseek_session)?;
250
251    let model = if use_opus {
252        "deepseek-r1"
253    } else if use_haiku {
254        "deepseek-lite"
255    } else {
256        "deepseek-r1"
257    };
258
259    println!("Connected to DeepSeek ({model})");
260    println!("šŸ’” Press ESC during tool execution to interrupt\n");
261
262    let chat_id = deepseek.create_chat_session().await
263        .context("Failed to create chat session")?;
264
265    let thinking_mode = if model == "deepseek-r1" {
266        crate::deepseek::ThinkingMode::Detailed
267    } else {
268        crate::deepseek::ThinkingMode::Simple
269    };
270    let search_mode = crate::deepseek::SearchMode::Disabled;
271
272    let system_prompt = session.agent().get_system_prompt();
273    
274    let original_termios = setup_raw_mode()?;
275
276    let interrupt_flag = session.interrupt_flag();
277    let _interrupt_rx = setup_interrupt_monitor(interrupt_flag);
278
279    let mut stdout = io::stdout();
280    let mut first_message = true;
281
282    loop {
283        print!("You: ");
284        stdout.flush()?;
285
286        let mut input = String::new();
287        
288        loop {
289            match read_char() {
290                Ok(Some(ch)) => {
291                    if ch == b'\n' || ch == b'\r' {
292                        println!();
293                        break;
294                    } else if ch == 27 {
295                        println!("\nā¹ļø Cancelled input");
296                        input.clear();
297                        break;
298                    } else if ch == 127 {
299                        if !input.is_empty() {
300                            input.pop();
301                            print!("\x08 \x08");
302                            stdout.flush()?;
303                        }
304                    } else if ch >= 32 && ch < 127 {
305                        input.push(ch as char);
306                        print!("{}", ch as char);
307                        stdout.flush()?;
308                    }
309                }
310                Ok(None) => {
311                    thread::sleep(Duration::from_millis(10));
312                }
313                Err(_) => break,
314            }
315        }
316
317        let input = input.trim();
318        if input.is_empty() {
319            continue;
320        }
321        if matches!(input, "exit" | "quit" | "/exit" | "x") {
322            break;
323        }
324
325        let system_prompt_opt = if first_message {
326            first_message = false;
327            Some(system_prompt.as_str())
328        } else {
329            None
330        };
331
332        let response = deepseek.chat_completion(
333            &chat_id,
334            input,
335            None,
336            thinking_mode,
337            search_mode,
338            system_prompt_opt,
339        ).await.context("Failed to send message")?;
340
341        println!("\nDeepSeek: {}\n", prettify(&response));
342
343        process_agent_response_deepseek(
344            session,
345            &mut deepseek,
346            &chat_id,
347            &response,
348            thinking_mode,
349            search_mode,
350        ).await?;
351        
352        session.agent().reset();
353    }
354
355    restore_raw_mode(original_termios)?;
356
357    println!("\nšŸ‘‹ Goodbye!");
358    Ok(())
359}
360
361async fn process_agent_response_claude(
362    session: &mut AgentSession,
363    claude: &Claude,
364    chat_id: &str,
365    response: &str,
366) -> Result<()> {
367    let tool_results = session.agent().process_tool_calls(response).await?;
368    
369    if !tool_results.is_empty() {
370        let mut result_message = String::from("Tool execution results:\n\n");
371        for (tool_name, output) in tool_results {
372            result_message.push_str(&format!("[{tool_name}]\n{output}\n\n"));
373        }
374
375        let follow_up = claude.send_message(chat_id, &result_message, &[]).await
376            .context("Failed to send tool results")?;
377
378        println!("Claude: {}\n", prettify(&follow_up));
379
380        Box::pin(process_agent_response_claude(session, claude, chat_id, &follow_up)).await?;
381    }
382
383    Ok(())
384}
385
386async fn process_agent_response_deepseek(
387    session: &mut AgentSession,
388    deepseek: &mut DeepSeek,
389    chat_id: &str,
390    response: &str,
391    thinking_mode: crate::deepseek::ThinkingMode,
392    search_mode: crate::deepseek::SearchMode,
393) -> Result<()> {
394    let tool_results = session.agent().process_tool_calls(response).await?;
395    
396    if !tool_results.is_empty() {
397        let mut result_message = String::from("Tool execution results:\n\n");
398        for (tool_name, output) in tool_results {
399            result_message.push_str(&format!("[{tool_name}]\n{output}\n\n"));
400        }
401
402        let follow_up = deepseek.chat_completion(
403            chat_id,
404            &result_message,
405            None,
406            thinking_mode,
407            search_mode,
408            None,
409        ).await.context("Failed to send tool results")?;
410
411        println!("DeepSeek: {}\n", prettify(&follow_up));
412
413        Box::pin(process_agent_response_deepseek(
414            session,
415            deepseek,
416            chat_id,
417            &follow_up,
418            thinking_mode,
419            search_mode,
420        )).await?;
421    }
422
423    Ok(())
424}