mod auth;
mod bluesky;
mod car;
mod cli;
mod error;
mod http;
mod mcp;
#[cfg(feature = "experimental-sentencepiece")]
mod sentencepiece;
#[cfg(not(feature = "experimental-sentencepiece"))]
#[allow(dead_code)]
mod sentencepiece_stub;
mod tools;
#[cfg(test)]
mod tests_mcp_adjustments;
use anyhow::Result;
use auth::{LoginManager, LoginRequest};
use clap::Parser;
use cli::{Cli, Commands};
use tracing::info;
#[tokio::main]
async fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
run_cli_mode().await
} else {
run_mcp_mode().await
}
}
async fn run_cli_mode() -> Result<()> {
let cli = Cli::parse();
let log_level = if cli.quiet {
"error"
} else if cli.verbose {
"debug"
} else {
"info"
};
tracing_subscriber::fmt()
.with_env_filter(log_level)
.with_writer(std::io::stderr) .init();
let result = match cli.command {
Some(Commands::Profile(args)) => execute_profile_cli(args).await,
Some(Commands::Search(args)) => execute_search_cli(args).await,
Some(Commands::Login(args)) => execute_login_cli(args).await,
Some(Commands::Feed(args)) => execute_feed_cli(args).await,
Some(Commands::Thread(args)) => execute_thread_cli(args).await,
Some(Commands::Post(args)) => execute_post_cli(args).await,
Some(Commands::React(args)) => execute_react_cli(args).await,
None => {
eprintln!("Error: No command specified. Use --help for usage information.");
std::process::exit(1);
}
};
match result {
Ok(output) => {
println!("{}", output);
Ok(())
}
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(get_exit_code(&e));
}
}
}
async fn execute_profile_cli(args: cli::ProfileArgs) -> Result<String> {
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(120),
tools::profile::execute_profile(args),
)
.await;
match result {
Ok(Ok(tool_result)) => {
Ok(tool_result
.content
.first()
.map(|c| c.text.clone())
.unwrap_or_default())
}
Ok(Err(e)) => Err(anyhow::anyhow!(e.message())),
Err(_) => Err(anyhow::anyhow!("Request exceeded 120 second timeout")),
}
}
async fn execute_search_cli(args: cli::SearchArgs) -> Result<String> {
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(120),
tools::search::execute_search(args),
)
.await;
match result {
Ok(Ok(tool_result)) => {
Ok(tool_result
.content
.first()
.map(|c| c.text.clone())
.unwrap_or_default())
}
Ok(Err(e)) => Err(anyhow::anyhow!(e.message())),
Err(_) => Err(anyhow::anyhow!("Request exceeded 120 second timeout")),
}
}
async fn execute_login_cli(args: cli::LoginCommand) -> Result<String> {
use std::io::{self, Write};
let manager = LoginManager::new()?;
let mut command = args;
loop {
let request = LoginRequest {
payload: command.clone(),
interactive: true,
};
let outcome = manager
.execute(request)
.await
.map_err(|e| anyhow::anyhow!(e.message()))?;
if let Some(elicitation) = outcome.elicitation {
if !outcome.message.is_empty() {
eprintln!("{}", outcome.message);
}
match elicitation.field.as_str() {
"handle" => {
eprint!("{}: ", elicitation.message);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let value = input.trim();
command.handle = if value.is_empty() {
None
} else {
Some(value.to_string())
};
}
"password" => {
eprint!("{}: ", elicitation.message);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
command.password = Some(input.trim().to_string());
}
other => {
return Err(anyhow::anyhow!(format!(
"Unsupported login prompt field: {}",
other
)));
}
}
continue;
}
return Ok(outcome.message);
}
}
async fn execute_feed_cli(args: cli::FeedArgs) -> Result<String> {
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(120),
tools::feed::execute_feed(args),
)
.await;
match result {
Ok(Ok(tool_result)) => {
Ok(tool_result
.content
.first()
.map(|c| c.text.clone())
.unwrap_or_default())
}
Ok(Err(e)) => Err(anyhow::anyhow!(e.message())),
Err(_) => Err(anyhow::anyhow!("Request exceeded 120 second timeout")),
}
}
async fn execute_thread_cli(args: cli::ThreadArgs) -> Result<String> {
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(120),
tools::thread::execute_thread(args),
)
.await;
match result {
Ok(Ok(tool_result)) => {
Ok(tool_result
.content
.first()
.map(|c| c.text.clone())
.unwrap_or_default())
}
Ok(Err(e)) => Err(anyhow::anyhow!(e.message())),
Err(_) => Err(anyhow::anyhow!("Request exceeded 120 second timeout")),
}
}
async fn execute_post_cli(args: cli::PostArgs) -> Result<String> {
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(120),
tools::post::execute_post(args),
)
.await;
match result {
Ok(Ok(tool_result)) => {
Ok(tool_result
.content
.first()
.map(|c| c.text.clone())
.unwrap_or_default())
}
Ok(Err(e)) => Err(anyhow::anyhow!(e.message())),
Err(_) => Err(anyhow::anyhow!("Request exceeded 120 second timeout")),
}
}
async fn execute_react_cli(args: cli::ReactArgs) -> Result<String> {
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(120),
tools::react::execute_react(args),
)
.await;
match result {
Ok(Ok(tool_result)) => {
Ok(tool_result
.content
.first()
.map(|c| c.text.clone())
.unwrap_or_default())
}
Ok(Err(e)) => Err(anyhow::anyhow!(e.message())),
Err(_) => Err(anyhow::anyhow!("Request exceeded 120 second timeout")),
}
}
fn get_exit_code(err: &anyhow::Error) -> i32 {
let err_str = err.to_string().to_lowercase();
if err_str.contains("invalid") || err_str.contains("usage") {
1 } else if err_str.contains("network") || err_str.contains("connection") {
2 } else if err_str.contains("not found") {
3 } else if err_str.contains("timeout") {
4 } else {
5 }
}
async fn run_mcp_mode() -> Result<()> {
tracing_subscriber::fmt::init();
info!("Starting autoreply MCP Server");
mcp::handle_stdio().await?;
Ok(())
}