mod catalog;
mod cli;
mod db;
mod errors;
mod interactions;
mod list;
mod output;
mod recommend;
mod search;
mod show;
mod types;
use clap::Parser;
use cli::{Cli, Commands};
use errors::SupplementsError;
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<(), SupplementsError> {
if matches!(cli.command, Commands::AgentInfo) {
print_agent_info();
return Ok(());
}
let catalog_file = catalog::load_catalog();
match &cli.command {
Commands::Recommend {
slug,
stdin,
sex,
age,
category: _category,
limit,
min_evidence,
} => {
let assessment = if *stdin {
read_assessment_stdin()?
} else if let Some(s) = slug {
load_assessment_from_db(cli, s, sex.as_deref(), *age)?
} else {
return Err(SupplementsError::NoInput);
};
let result = recommend::generate_recommendations(
&assessment,
&catalog_file,
min_evidence,
*limit,
);
if use_json {
output::json::print_success(&result);
} else {
output::table::render_recommendations(&result);
}
}
Commands::List {
category,
brand,
biomarker,
limit,
} => {
let products = list::filter_products(
&catalog_file,
category.as_deref(),
brand.as_deref(),
biomarker.as_deref(),
*limit,
);
if use_json {
let refs: Vec<_> = products.iter().copied().collect();
output::json::print_success(&refs);
} else {
output::table::render_product_list(&products);
}
}
Commands::Show { product_id } => {
let (product, brand_name) = show::get_product_detail(&catalog_file, product_id)?;
if use_json {
output::json::print_success(product);
} else {
output::table::render_product_detail(product, &brand_name);
}
}
Commands::Search { query, limit } => {
let products = search::search(&catalog_file, query, *limit);
if use_json {
let refs: Vec<_> = products.iter().copied().collect();
output::json::print_success(&refs);
} else {
output::table::render_product_list(&products);
}
}
Commands::Brands => {
let brands: Vec<&types::Brand> = catalog_file.brand.iter().collect();
if use_json {
output::json::print_success(&brands);
} else {
output::table::render_brands(&brands);
}
}
Commands::Interactions { product_ids } => {
let found = interactions::check_interactions(&catalog_file, product_ids)?;
if use_json {
output::json::print_success(&found);
} else {
output::table::render_interactions(&found);
}
}
Commands::AgentInfo => unreachable!(),
}
Ok(())
}
fn read_assessment_stdin() -> Result<LabassessOutput, SupplementsError> {
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,
_sex: Option<&str>,
_age: Option<u8>,
) -> Result<LabassessOutput, SupplementsError> {
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: "none".to_string(),
})
.collect();
let total = scored.len();
Ok(LabassessOutput {
version: "1".to_string(),
status: "success".to_string(),
data: types::LabassessData {
mode: "db_minimal".to_string(),
patient: types::PatientInfo {
sex: Some(sex),
age: None,
},
scored_biomarkers: scored,
derived_values: vec![],
patterns_detected: vec![],
red_flags: vec![],
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_file = catalog::load_catalog();
let product_count = catalog_file.product.len();
let brand_count = catalog_file.brand.len();
let categories: Vec<String> = {
let mut cats: Vec<String> = catalog_file
.product
.iter()
.map(|p| p.category.clone())
.collect();
cats.sort();
cats.dedup();
cats
};
let biomarkers: Vec<String> = {
let mut bios: Vec<String> = catalog_file
.product
.iter()
.flat_map(|p| p.targets.iter().map(|t| t.biomarker.clone()))
.collect();
bios.sort();
bios.dedup();
bios
};
let info = serde_json::json!({
"name": "agent-supplements-rec",
"version": env!("CARGO_PKG_VERSION"),
"description": "Curated supplement recommendation engine for longevity biomarker optimization",
"capabilities": [
"supplement_recommendation",
"catalog_browsing",
"interaction_checking",
"biomarker_targeting",
"evidence_filtering"
],
"catalog": {
"products": product_count,
"brands": brand_count,
"categories": categories,
"targeted_biomarkers": biomarkers,
},
"input_modes": [
"stdin (pipe labassess JSON)",
"db (patient slug → labstore)"
],
"db_path": "~/.labstore/labstore.db",
"commands": {
"recommend_stdin": "labassess ... --json | agent-supplements-rec recommend --stdin",
"recommend_slug": "agent-supplements-rec recommend <patient-slug>",
"list": "agent-supplements-rec list [--category vitamin] [--brand thorne]",
"show": "agent-supplements-rec show <product-id>",
"search": "agent-supplements-rec search <query>",
"brands": "agent-supplements-rec brands",
"interactions": "agent-supplements-rec interactions <id1> <id2> [...]",
},
"pipeline": "labparse -> labstore -> labassess -> agent-supplements-rec"
});
println!("{}", serde_json::to_string_pretty(&info).unwrap());
}