#![forbid(unsafe_code)]
use anyhow::{Context, Result};
use clap::{CommandFactory, FromArgMatches};
use srz::cache::Cache;
use srz::cli::{Cli, ResolvedAction};
use srz::commands::{self, with_db};
use srz::config::Config;
use srz::db::NewSummaryRecord;
use srz::render;
use srz::scraper::WebScraper;
use srz::ui::Ui;
#[tokio::main]
async fn main() -> Result<()> {
let cmd = Cli::command();
let matches = cmd.get_matches();
let mut args = match Cli::from_arg_matches(&matches) {
Ok(args) => args,
Err(err) => err.exit(),
};
let (config, config_path) = match &args.config {
Some(p) => {
let cfg = Config::load_from_path(p)?;
(cfg, Some(p.clone()))
}
None => Config::load_default(),
};
args.apply_config(&config, Some(&matches));
if args.quiet || args.json {
Ui::set_quiet(true);
}
if args.verbose {
Ui::set_verbose(true);
}
let action = match args.resolve_action() {
Ok(action) => action,
Err(msg) => {
Ui::error(msg);
std::process::exit(1);
}
};
let mut cache = Cache::new(!args.no_cache);
if let Some(ref cache_cfg) = config.cache {
cache.set_policy(cache_cfg.to_policy());
}
match action {
ResolvedAction::Cache(cache_cmd) => {
commands::handle_cache(cache_cmd, &cache)?;
return Ok(());
}
ResolvedAction::Config(config_cmd) => {
commands::handle_config(config_cmd, &config, config_path.as_deref(), &args)?;
return Ok(());
}
ResolvedAction::History { limit } => {
return with_db(|db| commands::handle_history(db, limit, args.json));
}
ResolvedAction::Stats => {
return with_db(|db| commands::handle_stats(db, args.json));
}
ResolvedAction::Find { query } => {
return with_db(|db| commands::handle_find(db, query, &cache, args.json));
}
ResolvedAction::Models => {
commands::handle_models(&args, args.json).await?;
return Ok(());
}
ResolvedAction::Doctor => {
commands::handle_doctor(&args, &config, config_path.as_deref(), &cache, args.json)
.await?;
return Ok(());
}
ResolvedAction::Recommend => {
commands::handle_recommend(args.json)?;
return Ok(());
}
ResolvedAction::Index => {
return with_db(|db| commands::handle_index(db, &cache, args.open));
}
ResolvedAction::Browse => {
return with_db(|db| srz::ui::run_tui(db, &cache));
}
ResolvedAction::Completions { shell } => {
clap_complete::generate(shell, &mut Cli::command(), "srz", &mut std::io::stdout());
return Ok(());
}
ResolvedAction::Upgrade {
check,
force,
version,
} => {
srz::upgrade::run_upgrade(check, force, version.as_deref(), args.json).await?;
return Ok(());
}
_ => {}
}
cache.maybe_auto_prune();
if args.no_cache {
Ui::info("Cache bypassed (`--no-cache`). Forcing fresh fetch and generation.");
}
let llm = commands::init_llm_with_capabilities_or_exit(&args, Some(&cache)).await;
let scraper = match WebScraper::new() {
Ok(s) => s,
Err(err) => {
Ui::error(&format!("Failed to initialize scraper: {}", err));
std::process::exit(1);
}
};
let summary_result = match action {
ResolvedAction::Ask {
target,
question,
interactive,
list_chats,
resume,
show_chat,
delete_chat,
} => {
if let Err(err) = commands::handle_ask(
target,
question,
interactive,
list_chats,
resume,
show_chat,
delete_chat,
&scraper,
&llm,
&cache,
&args,
)
.await
{
print_execution_error(&err);
std::process::exit(1);
}
return Ok(());
}
ResolvedAction::Web { url } => {
commands::handle_web(url, &scraper, &llm, &cache, &args).await
}
ResolvedAction::File { path } => commands::handle_file(&path, &scraper, &llm, &args).await,
ResolvedAction::Stdin => commands::handle_stdin(&scraper, &llm, &args).await,
ResolvedAction::Book { title } => {
commands::handle_book(title, None, &scraper, &llm, &cache, &args).await
}
ResolvedAction::News {
topic,
timeframe,
limit,
} => commands::handle_news(&topic, timeframe, limit, &scraper, &llm, &cache, &args).await,
_ => unreachable!(),
};
match summary_result {
Ok(outcome) => {
if args.json {
println!("{}", serde_json::to_string_pretty(&outcome)?);
} else if let Some(output_path) = &args.output {
let content = if render::is_html_path(output_path) {
std::borrow::Cow::Owned(render::render_html_document(
&outcome.title,
&outcome.summary,
))
} else {
std::borrow::Cow::Borrowed(outcome.summary.as_str())
};
srz::fs_util::write_private_file(output_path, content.as_bytes())
.with_context(|| format!("Failed to write output to {:?}", output_path))?;
Ui::success(&format!(
"Summary successfully saved to {}",
output_path.display()
));
} else if !outcome.streamed_to_stdout {
srz::ui::display_summary(
&outcome.title,
&outcome.summary,
args.effective_paging(),
args.plain,
)?;
}
if !args.json
&& let Ok(files) = render::ensure_view_files(
&cache.views_dir(),
&outcome.title,
&outcome.summary,
!outcome.cached,
)
{
Ui::success(&format!(
"Styled HTML view: file://{}",
files.html_path.display()
));
Ui::success(&format!(
"Markdown file: file://{}",
files.md_path.display()
));
if args.open
&& let Err(err) = render::open_in_browser(&files.html_path)
{
Ui::warn(&format!("Could not open browser: {}", err));
}
}
if !args.json
&& !args.plain
&& !args.quiet
&& let Some(hint) = &outcome.hint
{
println!("\n\x1b[2m{}\x1b[0m", hint);
}
if !outcome.cached
&& let Some(db) = commands::open_db_or_warn()
{
let record = NewSummaryRecord {
target_type: outcome.target_type,
title: &outcome.title,
author: outcome.author.as_deref(),
url: outcome.url.as_deref(),
style: args.style.as_str(),
model: &args.model,
word_count: outcome.word_count,
prompt_tokens: outcome.prompt_tokens,
completion_tokens: outcome.completion_tokens,
duration_ms: outcome.duration_ms,
tokens_per_second: outcome.tokens_per_second,
summary: &outcome.summary,
};
let _ = db.record_summary(&record);
let _ = commands::update_index_dashboard(&db, &cache);
}
}
Err(err) => {
print_execution_error(&err);
std::process::exit(1);
}
}
Ok(())
}
fn print_execution_error(err: &anyhow::Error) {
let err_msg = format!("{:#}", err);
Ui::error(&format!("Summarization failed: {}", err_msg));
let is_connection_error = err_msg.contains("Failed to communicate with LLM endpoint")
|| err_msg.contains("Failed to reach LLM endpoint")
|| err_msg.contains("connection closed")
|| err_msg.contains("Connection refused")
|| err_msg.contains("connection error")
|| err_msg.contains("error trying to connect")
|| err_msg.contains("is listening on port 9931");
if is_connection_error {
let hw = srz::system::hardware::HardwareProfile::detect();
println!();
Ui::info("💡 Quick Start: Your local LLM backend is not reachable.");
Ui::info(&format!(
"Based on your machine ({:.1} GB RAM detected), start your server with:",
hw.total_memory_gb
));
println!(" \x1b[1;32m{}\x1b[0m", hw.recommended_launch_command);
Ui::info(
"Or run `srz recommend` to view alternative models, Ollama commands, and profiles.\n",
);
}
}