use anyhow::{anyhow, Context, Result};
use clap::Parser;
use ctrlc;
use regex::Regex;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::process::{self, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::api::{Attachment, Claude, Session};
use crate::config::{HAIKU_MODEL, MAX_INTERNAL_ITERS, OPUS_MODEL, SONNET_MODEL, SYSTEM_PROMPT};
use crate::utils::{extract_commands, prettify};
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(long, conflicts_with_all = ["haiku", "custom_model"])]
opus: bool,
#[clap(long, conflicts_with_all = ["opus", "custom_model"])]
haiku: bool,
#[clap(long, conflicts_with_all = ["opus", "haiku"])]
custom_model: Option<String>,
}
pub fn run() -> Result<()> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async_main())
}
fn get_config_help(file_name: &str) -> String {
let cookie_help = "To get your cookie:
1. Go to claude.ai in your browser
2. Open Developer Tools (F12 or right-click and select 'Inspect')
3. Go to the Network tab
4. Refresh the page
5. Click on any request to claude.ai
6. In the 'Headers' tab, find 'Request Headers'
7. Look for the 'Cookie' header
8. Copy the entire cookie value and save it to this folder with filename: cookie";
match file_name {
"cookie" => cookie_help.to_string(),
_ => format!("Configuration file {} is missing.", file_name),
}
}
pub fn extract_org_id_from_cookie(cookie: &str) -> Option<String> {
let re = Regex::new(r"lastActiveOrg=([0-9a-f-]+)").ok()?;
re.captures(cookie)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
pub async fn async_main() -> Result<()> {
let args = Args::parse();
let config_dir = dirs::config_dir()
.ok_or_else(|| anyhow!("Could not determine config directory"))?
.join("toast");
let cookie_path = config_dir.join("cookie");
let org_id_path = config_dir.join("org_id");
if !config_dir.exists() {
fs::create_dir_all(&config_dir).context(format!(
"Failed to create config directory at {:?}",
config_dir
))?;
return Err(anyhow!(
"Configuration directory created at {:?}\n\nPlease create the following files:\n\n1. cookie file:\n{}\n\n2",
config_dir,
get_config_help("cookie"),
));
}
let cookie = if cookie_path.exists() {
fs::read_to_string(&cookie_path)
.context(format!("Failed to read cookie from {:?}", cookie_path))?
.trim()
.to_string()
} else {
return Err(anyhow!(
"Cookie file not found at {:?}\n\n{}",
cookie_path,
get_config_help("cookie")
));
};
let org_id = if org_id_path.exists() {
fs::read_to_string(&org_id_path)
.context(format!(
"Failed to read organization ID from {:?}",
org_id_path
))?
.trim()
.to_string()
} else {
if let Some(extracted_org_id) = extract_org_id_from_cookie(&cookie) {
fs::write(&org_id_path, &extracted_org_id).context(format!(
"Failed to write organization ID to {:?}",
org_id_path
))?;
println!(
"Extracted organization ID from cookie and saved to {:?}",
org_id_path
);
extracted_org_id
} else {
return Err(anyhow!(
"Organization ID file not found at {:?} and couldn't extract it from cookie.\n\n{}",
org_id_path,
get_config_help("org_id")
));
}
};
let user_agent =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0"
.to_string();
let session = Session {
cookie,
user_agent,
organization_id: org_id,
};
let model: &str = if let Some(custom) = args.custom_model {
Box::leak(custom.into_boxed_str())
} else if args.opus {
OPUS_MODEL
} else if args.haiku {
HAIKU_MODEL
} else {
SONNET_MODEL
};
let claude = Claude::new(session.clone(), model)?;
let running = Arc::new(AtomicBool::new(true));
{
let running = running.clone();
ctrlc::set_handler(move || {
running.store(false, Ordering::SeqCst);
println!("\nGoodbye!");
process::exit(0);
})?;
}
let stdin = io::stdin();
let mut stdout = io::stdout();
let mut chat_id = String::new();
let mut system_prompt_sent = false;
while running.load(Ordering::SeqCst) {
print!("You: ");
stdout.flush()?;
let mut buf = String::new();
stdin.read_line(&mut buf)?;
let input = buf.trim_end();
if input == "" {
continue;
}
if input.eq_ignore_ascii_case("/exit") || input.eq_ignore_ascii_case("exit") || input == "x"
{
if !chat_id.is_empty() {
claude.delete_chat(&chat_id).await.ok();
}
break;
}
if chat_id.is_empty() {
chat_id = claude.create_chat().await.context("creating chat")?;
}
if let Some(caps) = crate::utils::EXEC_RE.captures(input) {
let cmd = caps[1].to_string();
if !system_prompt_sent {
claude
.send_message(&chat_id, SYSTEM_PROMPT, &[])
.await
.context("sending system prompt")?;
system_prompt_sent = true;
}
run_exec(&claude, &chat_id, &cmd).await?;
continue;
}
if let Some(caps) = crate::utils::READ_RE.captures(input) {
let paths: Vec<String> = caps[1].split_whitespace().map(String::from).collect();
let path_refs: Vec<&str> = paths.iter().map(String::as_str).collect();
if !system_prompt_sent {
claude
.send_message(&chat_id, SYSTEM_PROMPT, &[])
.await
.context("sending system prompt")?;
system_prompt_sent = true;
}
let rest = input.strip_prefix(&caps[0]).unwrap_or("").trim();
let attachments = collect_attachments(&path_refs).unwrap_or_default();
let ans = claude
.send_message(&chat_id, rest, &attachments)
.await
.context("sending user message")?;
println!("Claude:\n{}", prettify(&ans));
process_claude(&claude, &chat_id, ans).await?;
} else {
if !system_prompt_sent {
claude
.send_message(&chat_id, SYSTEM_PROMPT, &[])
.await
.context("sending system prompt")?;
system_prompt_sent = true;
}
let ans = claude
.send_message(&chat_id, input, &[])
.await
.context("sending user message")?;
println!("Claude:\n{}", prettify(&ans));
process_claude(&claude, &chat_id, ans).await?;
}
}
Ok(())
}
async fn run_exec(claude: &Claude, chat_id: &str, cmd: &str) -> Result<()> {
let out = match execute_command(cmd) {
Ok(output) => output,
Err(e) => {
eprintln!("Warning: command execution failed: {e}");
format!("Command execution failed: {e}")
}
};
let msg = format!("Command executed: {cmd}\n\n{out}");
let ans = claude.send_message(chat_id, &msg, &[]).await?;
println!("Claude:\n{}", prettify(&ans));
process_claude(claude, chat_id, ans).await
}
fn execute_command(command: &str) -> Result<String> {
let result = Command::new("sh")
.arg("-c")
.arg(command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let output = result.wait_with_output()?;
let mut msg = String::new();
if !output.stdout.is_empty() {
msg.push_str("=== STDOUT ===\n");
msg.push_str(&String::from_utf8_lossy(&output.stdout));
msg.push('\n');
}
if !output.stderr.is_empty() {
msg.push_str("=== STDERR ===\n");
msg.push_str(&String::from_utf8_lossy(&output.stderr));
msg.push('\n');
}
msg.push_str(&format!(
"Exit code: {}",
output.status.code().unwrap_or(-1)
));
Ok(msg)
}
fn collect_attachments(paths: &[&str]) -> Result<Vec<Attachment>> {
const LIMIT: usize = 5;
const SIZE_LIMIT: u64 = 10 * 1024 * 1024;
if paths.len() > LIMIT {
return Err(anyhow!("cannot attach more than {LIMIT} files"));
}
let mut atts = Vec::new();
for p in paths {
if let Ok(meta) = fs::metadata(p) {
if meta.len() > SIZE_LIMIT {
eprintln!("Warning: file {p} is larger than 10 MB, skipping");
continue;
}
if let Ok(content) = fs::read_to_string(p) {
atts.push(Attachment {
file_name: Path::new(p)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into(),
size: meta.len(),
content,
});
} else {
eprintln!("Warning: couldn't read file {p}");
}
} else {
eprintln!("Warning: couldn't access file {p}");
}
}
Ok(atts)
}
async fn process_claude(claude: &Claude, chat_id: &str, mut ans: String) -> Result<()> {
for _ in 0..MAX_INTERNAL_ITERS {
let (reads, execs) = extract_commands(&ans);
if reads.is_empty() && execs.is_empty() {
return Ok(());
}
if !reads.is_empty() {
let atts = collect_attachments(&reads.iter().map(String::as_str).collect::<Vec<_>>())
.unwrap_or_default();
let ans2 = claude
.send_message(chat_id, "read_file response:", &atts)
.await?;
println!("Claude:\n{}", prettify(&ans2));
ans = ans2;
continue;
}
if !execs.is_empty() {
let mut outputs = String::new();
for cmd in &execs {
match execute_command(cmd) {
Ok(output) => outputs.push_str(&output),
Err(e) => outputs.push_str(&format!("Command execution failed: {e}")),
}
outputs.push_str("\n\n---\n\n");
}
let ans2 = claude.send_message(chat_id, &outputs, &[]).await?;
println!("Claude:\n{}", prettify(&ans2));
ans = ans2;
continue;
}
}
println!("Max internal iterations reached, returning to user.");
Ok(())
}