roboticus-cli 0.11.4

CLI commands and migration engine for the Roboticus agent runtime
Documentation
use super::*;

pub async fn cmd_memory(
    url: &str,
    tier: &str,
    session_id: Option<&str>,
    query: Option<&str>,
    limit: Option<i64>,
    json: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
    let c = RoboticusClient::new(url)?;
    match tier {
        "working" => {
            let sid = session_id.ok_or("--session required for working memory. Use 'roboticus sessions list' to find session IDs.")?;
            let data = c
                .get(&format!("/api/memory/working/{sid}"))
                .await
                .map_err(|e| {
                    RoboticusClient::check_connectivity_hint(&*e);
                    e
                })?;
            if json {
                println!("{}", serde_json::to_string_pretty(&data)?);
                return Ok(());
            }
            heading("Working Memory");
            let entries = data["entries"].as_array();
            match entries {
                Some(arr) if !arr.is_empty() => {
                    let widths = [12, 14, 36, 10];
                    table_header(&["ID", "Type", "Content", "Importance"], &widths);
                    for e in arr {
                        table_row(
                            &[
                                format!(
                                    "{MONO}{}{RESET}",
                                    truncate_id(e["id"].as_str().unwrap_or(""), 9)
                                ),
                                e["entry_type"].as_str().unwrap_or("").to_string(),
                                truncate_id(e["content"].as_str().unwrap_or(""), 33),
                                e["importance"].to_string(),
                            ],
                            &widths,
                        );
                    }
                    eprintln!();
                    eprintln!("    {DIM}{} entries{RESET}", arr.len());
                }
                _ => empty_state("No working memory entries"),
            }
        }
        "episodic" => {
            let lim = limit.unwrap_or(20);
            let data = c
                .get(&format!("/api/memory/episodic?limit={lim}"))
                .await
                .map_err(|e| {
                    RoboticusClient::check_connectivity_hint(&*e);
                    e
                })?;
            if json {
                println!("{}", serde_json::to_string_pretty(&data)?);
                return Ok(());
            }
            heading("Episodic Memory");
            let entries = data["entries"].as_array();
            match entries {
                Some(arr) if !arr.is_empty() => {
                    let widths = [12, 16, 36, 10];
                    table_header(&["ID", "Classification", "Content", "Importance"], &widths);
                    for e in arr {
                        table_row(
                            &[
                                format!(
                                    "{MONO}{}{RESET}",
                                    truncate_id(e["id"].as_str().unwrap_or(""), 9)
                                ),
                                e["classification"].as_str().unwrap_or("").to_string(),
                                truncate_id(e["content"].as_str().unwrap_or(""), 33),
                                e["importance"].to_string(),
                            ],
                            &widths,
                        );
                    }
                    eprintln!();
                    eprintln!("    {DIM}{} entries (limit: {lim}){RESET}", arr.len());
                }
                _ => empty_state("No episodic memory entries"),
            }
        }
        "semantic" => {
            let category = session_id.unwrap_or("general");
            let data = c
                .get(&format!("/api/memory/semantic/{category}"))
                .await
                .map_err(|e| {
                    RoboticusClient::check_connectivity_hint(&*e);
                    e
                })?;
            if json {
                println!("{}", serde_json::to_string_pretty(&data)?);
                return Ok(());
            }
            heading(&format!("Semantic Memory [{category}]"));
            let entries = data["entries"].as_array();
            match entries {
                Some(arr) if !arr.is_empty() => {
                    let widths = [20, 34, 12];
                    table_header(&["Key", "Value", "Confidence"], &widths);
                    for e in arr {
                        table_row(
                            &[
                                format!("{ACCENT}{}{RESET}", e["key"].as_str().unwrap_or("")),
                                truncate_id(e["value"].as_str().unwrap_or(""), 31),
                                format!("{:.2}", e["confidence"].as_f64().unwrap_or(0.0)),
                            ],
                            &widths,
                        );
                    }
                    eprintln!();
                    eprintln!("    {DIM}{} entries{RESET}", arr.len());
                }
                _ => empty_state("No semantic memory entries in this category"),
            }
        }
        "search" => {
            let q = query.ok_or("--query/-q required for memory search")?;
            let data = c
                .get(&format!("/api/memory/search?q={}", urlencoding(q)))
                .await
                .map_err(|e| {
                    RoboticusClient::check_connectivity_hint(&*e);
                    e
                })?;
            if json {
                println!("{}", serde_json::to_string_pretty(&data)?);
                return Ok(());
            }
            heading(&format!("Memory Search: \"{q}\""));
            let results = data["results"].as_array();
            match results {
                Some(arr) if !arr.is_empty() => {
                    for (i, r) in arr.iter().enumerate() {
                        let fallback = r.to_string();
                        let text = r.as_str().unwrap_or(&fallback);
                        eprintln!("    {DIM}{:>3}.{RESET} {text}", i + 1);
                    }
                    eprintln!();
                    eprintln!("    {DIM}{} results{RESET}", arr.len());
                }
                _ => empty_state("No results found"),
            }
        }
        _ => {
            return Err(format!(
                "unknown memory tier: {tier}. Use: working, episodic, semantic, search"
            )
            .into());
        }
    }
    eprintln!();
    Ok(())
}

/// Trigger a full consolidation cycle (index backfill, dedup, decay, cleanup).
pub async fn cmd_memory_consolidate(base_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let (_, bold, _, green, _, _, _, reset, _) = colors();
    let (ok, _, _, _, _) = icons();
    println!("\n  {bold}Running memory consolidation...{reset}\n");
    let resp = super::http_client()?
        .post(format!("{base_url}/api/memory/consolidate"))
        .send()
        .await?;
    let data: serde_json::Value = resp.json().await?;
    let total = data["total_actions"].as_u64().unwrap_or(0);
    let ms = data["duration_ms"].as_u64().unwrap_or(0);
    println!("  {ok} {green}Consolidation complete{reset} ({total} actions, {ms}ms)");
    if let Some(indexed) = data["indexed"].as_u64().filter(|n| *n > 0) {
        println!("    Indexed: {indexed}");
    }
    if let Some(deduped) = data["deduped"].as_u64().filter(|n| *n > 0) {
        println!("    Deduped: {deduped}");
    }
    if let Some(decayed) = data["decayed"].as_u64().filter(|n| *n > 0) {
        println!("    Decayed: {decayed}");
    }
    if let Some(pruned) = data["pruned"].as_u64().filter(|n| *n > 0) {
        println!("    Pruned: {pruned}");
    }
    if let Some(orphans) = data["orphans_cleaned"].as_u64().filter(|n| *n > 0) {
        println!("    Orphans cleaned: {orphans}");
    }
    println!();
    Ok(())
}

/// Backfill all missing memory index entries.
pub async fn cmd_memory_reindex(base_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let (_, bold, _, green, _, _, _, reset, _) = colors();
    let (ok, _, _, _, _) = icons();
    println!("\n  {bold}Reindexing memory...{reset}\n");
    let resp = super::http_client()?
        .post(format!("{base_url}/api/memory/reindex"))
        .send()
        .await?;
    let data: serde_json::Value = resp.json().await?;
    let indexed = data["indexed"].as_u64().unwrap_or(0);
    println!("  {ok} {green}Reindex complete{reset}: {indexed} entries indexed");
    println!();
    Ok(())
}