mod catalog;
mod cli;
mod compare;
mod db;
mod errors;
mod list;
mod output;
mod recommend;
mod search;
mod show;
mod types;
use clap::Parser;
use cli::{Cli, Commands};
use errors::DeviceError;
use types::LabassessOutput;
fn main() {
let cli = Cli::parse();
let use_json = cli.json || !output::is_tty();
match run(&cli, use_json) {
Ok(()) => {}
Err(e) => {
if use_json {
output::json::print_error(&e.to_string());
} else {
eprintln!("error: {e}");
}
e.exit();
}
}
}
fn run(cli: &Cli, use_json: bool) -> Result<(), DeviceError> {
if matches!(cli.command, Commands::AgentInfo) {
print_agent_info();
return Ok(());
}
let catalog = catalog::load_catalog();
match &cli.command {
Commands::Recommend {
slug,
stdin,
sex: _,
age: _,
category,
max_price,
limit,
} => {
let assessment = if *stdin {
read_assessment_stdin()?
} else if let Some(s) = slug {
load_assessment_from_db(cli, s)?
} else {
return Err(DeviceError::NoInput);
};
let mut result =
recommend::generate_recommendations(&assessment, &catalog, *max_price, *limit);
if let Some(cat) = category {
let lower = cat.to_lowercase();
result
.recommendations
.retain(|r| r.device.category.to_lowercase() == lower);
result.total_recommendations = result.recommendations.len();
}
if use_json {
output::json::print_success(&result);
} else {
output::table::render_recommendations(&result);
}
}
Commands::List {
category,
brand,
tracks,
price_range,
limit,
} => {
let devices = list::list_devices(
&catalog,
category.as_deref(),
brand.as_deref(),
tracks.as_deref(),
price_range.as_deref(),
*limit,
);
if use_json {
let device_refs: Vec<_> = devices.into_iter().cloned().collect();
output::json::print_success(&device_refs);
} else {
output::table::render_device_list(&devices, &catalog.brand);
}
}
Commands::Show { device_id } => {
let (device, brand_name) = show::show_device(&catalog, device_id)?;
if use_json {
output::json::print_success(device);
} else {
output::table::render_device_detail(device, &brand_name);
}
}
Commands::Search { query, limit } => {
let devices = search::search_devices(&catalog, query, *limit);
if use_json {
let device_refs: Vec<_> = devices.into_iter().cloned().collect();
output::json::print_success(&device_refs);
} else {
output::table::render_device_list(&devices, &catalog.brand);
}
}
Commands::Brands => {
if use_json {
output::json::print_success(&catalog.brand);
} else {
output::table::render_brands(&catalog.brand);
}
}
Commands::Compare { device_ids } => {
let comparison = compare::compare_devices(&catalog, device_ids)?;
if use_json {
output::json::print_success(&comparison);
} else {
output::table::render_comparison(&comparison);
}
}
Commands::AgentInfo => unreachable!(),
}
Ok(())
}
fn read_assessment_stdin() -> Result<LabassessOutput, DeviceError> {
let input = std::io::read_to_string(std::io::stdin())?;
let assessment: LabassessOutput = serde_json::from_str(&input)?;
Ok(assessment)
}
fn load_assessment_from_db(cli: &Cli, slug: &str) -> Result<LabassessOutput, DeviceError> {
let db_path = db::resolve_db_path(cli.db.as_deref())?;
let conn = db::open(&db_path)?;
let (patient_id, _name, sex, _dob) = db::get_patient(&conn, slug)?;
let biomarkers = db::load_latest_biomarkers(&conn, patient_id)?;
let scored: Vec<types::ScoredBiomarker> = biomarkers
.into_iter()
.map(|(name, value, unit)| types::ScoredBiomarker {
name: name.clone(),
standardized_name: name,
value,
unit,
status: "unknown".to_string(),
severity: "unknown".to_string(),
})
.collect();
let total = scored.len();
Ok(LabassessOutput {
version: "1".to_string(),
status: "success".to_string(),
data: types::LabassessData {
mode: "minimal".to_string(),
patient: types::PatientInfo {
sex: Some(sex),
age: None,
},
scored_biomarkers: scored,
derived_values: Vec::new(),
patterns_detected: Vec::new(),
red_flags: Vec::new(),
summary: types::Summary {
total_markers: total,
scored: 0,
unknown: total,
optimal: 0,
good: 0,
attention: 0,
marginal: 0,
concern: 0,
},
},
})
}
fn print_agent_info() {
let catalog = catalog::load_catalog();
let device_count = catalog.device.len();
let brand_count = catalog.brand.len();
let categories: Vec<String> = {
let mut cats: Vec<String> = catalog
.device
.iter()
.map(|d| d.category.clone())
.collect();
cats.sort();
cats.dedup();
cats
};
let info = serde_json::json!({
"name": "agent-device-rec",
"version": env!("CARGO_PKG_VERSION"),
"description": "Health device recommendation engine for longevity monitoring",
"capabilities": [
"device_recommendation",
"device_catalog_browsing",
"device_comparison",
"pattern_to_device_mapping",
"red_flag_monitoring"
],
"catalog": {
"devices": device_count,
"brands": brand_count,
"categories": categories
},
"db_path": "~/.labstore/labstore.db",
"commands": {
"recommend_stdin": "labassess ... --json | agent-device-rec recommend --stdin",
"recommend_db": "agent-device-rec recommend <slug>",
"list": "agent-device-rec list [--category cgm] [--brand withings]",
"show": "agent-device-rec show <device-id>",
"search": "agent-device-rec search <query>",
"brands": "agent-device-rec brands",
"compare": "agent-device-rec compare <id1> <id2> [<id3>...]"
},
"pipeline": "labparse -> labstore -> labassess -> agent-device-rec"
});
println!("{}", serde_json::to_string_pretty(&info).unwrap());
}