//! brainctl: cliente CLI del introspect API. //! //! Uso: //! cargo run --example brainctl -p ente-brain -- list-rules //! cargo run --example brainctl -p ente-brain -- entropy //! cargo run --example brainctl -p ente-brain -- top 10 //! cargo run --example brainctl -p ente-brain -- crystals //! cargo run --example brainctl -p ente-brain -- crystal-kcl 0 //! //! Path del socket: $ENTE_BRAIN_SOCK o $XDG_RUNTIME_DIR/ente-brain.sock use ente_brain::introspect::{call, IntrospectRequest, IntrospectResponse}; use std::path::PathBuf; fn socket_path() -> PathBuf { if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") { return p.into(); } let runtime = std::env::var("XDG_RUNTIME_DIR") .unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into())); format!("{runtime}/ente-brain.sock").into() } #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { let args: Vec = std::env::args().collect(); let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("entropy"); let req = match cmd { "list-rules" | "rules" => IntrospectRequest::ListRules, "entropy" => IntrospectRequest::EntropySnapshot, "top" => { let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(10); IntrospectRequest::TopCorrelations { n } } "crystals" => IntrospectRequest::Crystals, "crystal-kcl" => { let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); IntrospectRequest::CrystalKcl { index: i } } "promote" => { let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); IntrospectRequest::PromoteCrystal { index: i } } "remove" => { let id_s = args.get(2).ok_or_else(|| anyhow::anyhow!("se requiere "))?; let id: ulid::Ulid = id_s.parse()?; IntrospectRequest::RemoveRule { id } } "audit" => { let limit: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(20); IntrospectRequest::ListAudit { limit } } "flush-audit" => IntrospectRequest::FlushAudit, "reload" => { let path = args.get(2).cloned(); IntrospectRequest::ReloadRules { path } } other => { eprintln!("subcomando desconocido: {other}"); eprintln!("válidos: list-rules | entropy | top | crystals | crystal-kcl | promote | remove | audit | flush-audit | reload [path]"); std::process::exit(2); } }; let path = socket_path(); let resp = call(&path, req).await?; print_response(&resp); Ok(()) } fn print_response(r: &IntrospectResponse) { match r { IntrospectResponse::Rules(rs) => { println!("{} reglas vivas:", rs.len()); for r in rs { println!(" {} prio={} kind={} actions={} wildcard={}", r.id, r.priority, r.event_kind_tag, r.action_count, r.scope_wildcard); } } IntrospectResponse::Rule(rule) => match rule { Some(r) => println!("{r:#?}"), None => println!("regla no encontrada"), }, IntrospectResponse::Entropy { value_bits, sample_size, distinct_kinds, window_full } => { println!("Shannon entropy : {value_bits:.4} bits"); println!("Sample size : {sample_size}"); println!("Distinct kinds : {distinct_kinds}"); println!("Window full : {window_full}"); } IntrospectResponse::Correlations(entries) => { println!("{} pares (top, ordenado por co-ocurrencia):", entries.len()); for e in entries { println!(" n={:>4} P(b|a)={:.3} PMI={:>6.3}b {} → {}", e.joint_count, e.conditional_prob, e.pmi_bits, e.a, e.b); } } IntrospectResponse::Crystals(cs) => { println!("{} cristales detectados:", cs.len()); for (i, c) in cs.iter().enumerate() { println!(" [{i}] {:?} → {:?} P={:.3} PMI={:.3}b n={}", c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support); } } IntrospectResponse::Kcl(s) => println!("{s}"), IntrospectResponse::Promoted { rule_id, kcl_snippet } => { println!("regla creada: {rule_id}"); println!("--- KCL para auditoría / persistencia ---"); println!("{kcl_snippet}"); } IntrospectResponse::Removed(was_present) => { if *was_present { println!("regla eliminada"); } else { println!("regla no encontrada"); } } IntrospectResponse::AuditEntries(entries) => { println!("{} entries de audit log:", entries.len()); for e in entries { let prev = e.prev_sha.map(hex_short).unwrap_or_else(|| "—".into()); let sha = hex_short(e.sha); println!(" seq={:>4} t={} prev={} sha={} {:?}", e.seq, e.timestamp_ms, prev, sha, e.action); } } IntrospectResponse::Flushed { written, head_sha, total_flushed } => { println!("flushed: {written} entries esta pasada, total acumulado: {total_flushed}"); if let Some(sha) = head_sha { println!("head sha: {}", hex_long(*sha)); } } IntrospectResponse::Reloaded { count } => { println!("reload OK: {count} reglas activas tras reload"); } IntrospectResponse::Error(e) => eprintln!("error: {e}"), } } fn hex_short(sha: [u8; 32]) -> String { sha[..4].iter().map(|b| format!("{:02x}", b)).collect::() + ".." } fn hex_long(sha: [u8; 32]) -> String { sha.iter().map(|b| format!("{:02x}", b)).collect() }