remem-ai 0.5.145

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::{bail, Result};

use crate::cli::types::{GraphReviewAction, ReviewAction};
use crate::db;
use crate::graph_candidate::review as graph_review;
use crate::memory_candidate::review::{self, CandidateEdit};

pub(in crate::cli) fn run_review(action: ReviewAction) -> Result<()> {
    let mut conn = db::open_db()?;

    match action {
        ReviewAction::List { project, limit } => {
            let rows = review::list_pending(&conn, project.as_deref(), limit)?;
            if rows.is_empty() {
                println!("No pending memory candidates.");
                return Ok(());
            }
            println!("Pending memory candidates ({}):", rows.len());
            for row in rows {
                let project = row.project.as_deref().unwrap_or("<unknown project>");
                println!(
                    "  [{}] {} {} {} confidence={:.2} risk={} project={}",
                    row.id,
                    row.scope,
                    row.memory_type,
                    row.topic_key,
                    row.confidence,
                    row.risk_class,
                    project
                );
                println!("      text: {}", db::truncate_str(&row.text, 180));
                println!("      evidence: {}", row.evidence_event_ids);
                for evidence in row.evidence_preview {
                    println!("        {}", evidence);
                }
            }
        }
        ReviewAction::Approve { id } => {
            let Some(memory_id) = review::approve_candidate(&mut conn, id)? else {
                bail!("candidate {} not found", id);
            };
            println!("Approved candidate {}; promoted memory {}.", id, memory_id);
        }
        ReviewAction::Discard { id } => {
            if review::discard_candidate(&conn, id)? {
                println!("Discarded candidate {}.", id);
            } else {
                bail!("candidate {} not found or not pending_review", id);
            }
        }
        ReviewAction::Edit {
            id,
            text,
            topic_key,
            memory_type,
            scope,
        } => {
            let edit = CandidateEdit {
                scope,
                memory_type,
                topic_key,
                text,
            };
            let Some(memory_id) = review::edit_candidate(&mut conn, id, edit)? else {
                bail!("candidate {} not found", id);
            };
            println!("Edited candidate {}; promoted memory {}.", id, memory_id);
        }
    }

    Ok(())
}

pub(in crate::cli) fn run_graph_review(action: GraphReviewAction) -> Result<()> {
    let mut conn = db::open_db()?;

    match action {
        GraphReviewAction::List { project, limit } => {
            let rows = graph_review::list_pending(&conn, project.as_deref(), limit)?;
            if rows.is_empty() {
                println!("No reviewable graph candidates.");
                return Ok(());
            }
            println!("Reviewable graph candidates ({}):", rows.len());
            for row in rows {
                print_graph_candidate(&row);
            }
        }
        GraphReviewAction::Inspect { id } => {
            let Some(row) = graph_review::inspect_candidate(&conn, id)? else {
                bail!("graph candidate {} not found", id);
            };
            print_graph_candidate(&row);
        }
        GraphReviewAction::Approve { id } => {
            let Some(edge_id) = graph_review::approve_candidate(&mut conn, id)? else {
                bail!("graph candidate {} not found", id);
            };
            println!(
                "Approved graph candidate {}; promoted graph edge {}.",
                id, edge_id
            );
        }
        GraphReviewAction::Reject { id, reason } => {
            if graph_review::reject_candidate(&conn, id, &reason)? {
                println!("Rejected graph candidate {}.", id);
            } else {
                bail!("graph candidate {} not found or not reviewable", id);
            }
        }
        GraphReviewAction::Defer { id, reason } => {
            if graph_review::defer_candidate(&conn, id, &reason)? {
                println!("Deferred graph candidate {}.", id);
            } else {
                bail!("graph candidate {} not found or not reviewable", id);
            }
        }
    }

    Ok(())
}

fn print_graph_candidate(row: &graph_review::ReviewGraphCandidate) {
    let project = row.project.as_deref().unwrap_or("<unknown project>");
    println!(
        "  [{}] {} {} {} -> {} confidence={:.2} risk={} status={} project={}",
        row.id,
        row.candidate_type,
        row.edge_type,
        row.from_ref,
        row.to_ref,
        row.confidence,
        row.risk_class,
        row.review_status,
        project
    );
    println!("      evidence: {:?}", row.evidence_event_ids);
    println!("      reason: {}", db::truncate_str(&row.reason, 180));
    if let Some(edge_id) = row.promoted_edge_id {
        println!("      promoted_edge: {}", edge_id);
    }
    for evidence in &row.evidence_preview {
        println!("        {}", evidence);
    }
}