mod capture_tui;
mod cli;
mod config;
mod db;
mod explain_cmd;
mod install_skill;
mod login_cmd;
mod output;
mod tui;
mod upload;
use clap::Parser;
use cli::{Cli, Command};
use capture_tui::run_capture_tui;
use db::open_store;
use explain_cmd::run_explain;
use install_skill::run_install_skill;
use login_cmd::run_login;
use output::{
print_comparison, print_comparison_unavailable, print_history, print_probe_result,
print_watch_result, print_zero_baseline_notice,
};
use lobe_core::engine::history::compare_ttfb;
use lobe_core::engine::proxy::SimpleProbe;
use lobe_core::error::{Result, TloxError};
use tokio::time::{sleep, Duration};
use tui::run_watch_tui;
#[tokio::main]
async fn main() {
if let Err(error) = run().await {
eprintln!("{}", friendly_error(&error));
std::process::exit(1);
}
}
async fn run() -> Result<()> {
let cli = Cli::parse();
let store = open_store(cli.db.as_deref())?;
match cli.command {
Command::Probe { url } => {
let probe = SimpleProbe::new();
let result = probe.probe_and_store(&store, &url).await?;
print_probe_result(&result);
}
Command::History { target, limit } => {
let results = match target.as_deref() {
Some(target) => store.recent_results_for_target(target, limit)?,
None => store.recent_results(limit)?,
};
print_history(&results);
}
Command::Compare { url, threshold } => {
let results = store.recent_results_for_target(&url, 2)?;
if results.len() < 2 {
print_comparison_unavailable(&url);
} else {
let current = &results[0];
let previous = &results[1];
match compare_ttfb(previous, current, threshold) {
Some(summary) => print_comparison(current, previous, &summary),
None => print_zero_baseline_notice(&url),
}
}
}
Command::Watch {
url,
interval,
threshold,
count,
tui,
} => {
if tui {
run_watch_tui(store, url, interval, threshold, count).await?;
return Ok(());
}
let probe = SimpleProbe::new();
let mut remaining = count;
loop {
let result = probe.probe_and_store(&store, &url).await?;
let results = store.recent_results_for_target(&url, 2)?;
let (comparison, comparison_note) = if results.len() < 2 {
(None, Some("first-run"))
} else {
match compare_ttfb(&results[1], &results[0], threshold) {
Some(summary) => (Some(summary), None),
None => (None, Some("previous-ttfb-zero")),
}
};
print_watch_result(&result, comparison.as_ref(), comparison_note);
if let Some(remaining_count) = remaining.as_mut() {
*remaining_count -= 1;
if *remaining_count == 0 {
break;
}
}
sleep(Duration::from_secs(interval)).await;
}
}
Command::Capture {
upstream,
listen,
upload,
} => {
run_capture_tui(listen, upstream, upload).await?;
}
Command::Login {
token,
web_url,
api_url,
} => {
run_login(token, web_url, api_url).await?;
}
Command::Explain {
session,
model,
output,
} => {
run_explain(session, model, output).await?;
}
Command::InstallSkill {
target,
global,
force,
} => {
run_install_skill(target, global, force)?;
}
}
Ok(())
}
pub fn friendly_error(error: &TloxError) -> String {
match error {
TloxError::MissingApiKey => concat!(
"ANTHROPIC_API_KEY is not set.\n\n",
"`lobe explain` calls Anthropic's Claude API with your key.\n",
"1. Get a key: https://console.anthropic.com/settings/keys\n",
"2. Export it in your shell:\n\n",
" export ANTHROPIC_API_KEY=sk-ant-...\n\n",
" Or add that line to ~/.zshrc or ~/.bashrc to persist it.\n",
)
.to_string(),
other => other.to_string(),
}
}