remem-ai 0.6.62

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::Result;
use serde::Serialize;

use crate::{db, memory::suppression};

use super::super::types::{MemoryAction, MemorySuppressionsAction};

pub(in crate::cli) fn run_memory_action(action: MemoryAction) -> Result<()> {
    match action {
        MemoryAction::Suppress {
            target,
            reason,
            actor,
            json,
        } => {
            let conn = db::open_db()?;
            let target = suppression::parse_target(&target)?;
            let suppression = suppression::create_suppression(
                &conn,
                &suppression::SuppressRequest {
                    target,
                    reason: reason.as_deref(),
                    actor: actor.as_deref(),
                },
            )?;
            if json {
                print_json(&SuppressionOutput {
                    status: "suppressed",
                    suppression,
                })?;
            } else {
                println!(
                    "Suppression {} active for {}.",
                    suppression.id,
                    target_label(&suppression)
                );
            }
        }
        MemoryAction::Unsuppress {
            target,
            reason,
            actor,
            json,
        } => {
            let conn = db::open_db()?;
            let suppressions = suppression::revoke_suppression_arg(
                &conn,
                &target,
                reason.as_deref(),
                actor.as_deref(),
            )?;
            if json {
                print_json(&UnsuppressionOutput {
                    status: "unsuppressed",
                    count: suppressions.len(),
                    suppressions,
                })?;
            } else {
                println!("Revoked {} suppression(s).", suppressions.len());
            }
        }
        MemoryAction::Feedback {
            target,
            value,
            source,
            context_injection_item_id,
            session_id,
            project,
            reason,
            json,
        } => {
            let conn = db::open_db()?;
            let target = suppression::parse_target(&target)?;
            let feedback = suppression::record_feedback(
                &conn,
                &suppression::FeedbackRequest {
                    target,
                    feedback: &value,
                    source: source.as_deref(),
                    context_injection_item_id,
                    session_id: session_id.as_deref(),
                    project: project.as_deref(),
                    reason: reason.as_deref(),
                },
            )?;
            if json {
                print_json(&FeedbackOutput {
                    status: "recorded",
                    feedback,
                })?;
            } else {
                println!("Feedback {} recorded.", feedback.id);
            }
        }
        MemoryAction::Suppressions {
            action:
                MemorySuppressionsAction::List {
                    include_inactive,
                    json,
                },
        } => {
            let conn = db::open_db()?;
            let suppressions = suppression::list_suppressions(&conn, include_inactive)?;
            if json {
                print_json(&SuppressionListOutput {
                    count: suppressions.len(),
                    suppressions,
                })?;
            } else if suppressions.is_empty() {
                println!("No memory suppressions found.");
            } else {
                for item in suppressions {
                    println!(
                        "{} [{}] {} {} ({})",
                        item.id,
                        item.status,
                        item.target_kind,
                        item.target_id
                            .map(|id| id.to_string())
                            .or(item.target_value)
                            .unwrap_or_default(),
                        item.reason
                    );
                }
            }
        }
        MemoryAction::Cleanup {
            cwd,
            cleanup_type,
            all_types,
            dry_run,
            plan_out,
            apply,
            plan,
            json,
        } => super::scope_cleanup::run_memory_cleanup(
            cwd.as_deref(),
            cleanup_type,
            all_types,
            dry_run,
            plan_out.as_deref(),
            apply,
            plan.as_deref(),
            json,
        )?,
    }
    Ok(())
}

fn target_label(record: &suppression::SuppressionRecord) -> String {
    record
        .target_id
        .map(|id| format!("{}:{id}", record.target_kind))
        .or_else(|| {
            record
                .target_value
                .as_ref()
                .map(|value| format!("{}:{value}", record.target_kind))
        })
        .unwrap_or_else(|| record.target_kind.clone())
}

fn print_json<T: Serialize>(value: &T) -> Result<()> {
    println!("{}", serde_json::to_string_pretty(value)?);
    Ok(())
}

#[derive(Serialize)]
struct SuppressionOutput {
    status: &'static str,
    suppression: suppression::SuppressionRecord,
}

#[derive(Serialize)]
struct UnsuppressionOutput {
    status: &'static str,
    count: usize,
    suppressions: Vec<suppression::SuppressionRecord>,
}

#[derive(Serialize)]
struct FeedbackOutput {
    status: &'static str,
    feedback: suppression::FeedbackRecord,
}

#[derive(Serialize)]
struct SuppressionListOutput {
    count: usize,
    suppressions: Vec<suppression::SuppressionRecord>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cli::memory_types::MemorySuppressionsAction;
    use crate::memory;

    #[test]
    fn memory_policy_actions_write_suppressions_and_feedback() -> Result<()> {
        let _dir = crate::db::test_support::ScopedTestDataDir::new("memory-policy-cli-actions");
        let conn = db::open_db()?;
        memory::insert_memory(
            &conn,
            Some("s1"),
            "/repo",
            None,
            "Policy target",
            "Policy target body",
            "decision",
            None,
        )?;
        drop(conn);

        run_memory_action(MemoryAction::Suppress {
            target: "memory:1".to_string(),
            reason: Some("not relevant".to_string()),
            actor: Some("test".to_string()),
            json: true,
        })?;
        run_memory_action(MemoryAction::Feedback {
            target: "memory:1".to_string(),
            value: "not-relevant".to_string(),
            source: Some("test".to_string()),
            context_injection_item_id: None,
            session_id: Some("s1".to_string()),
            project: Some("/repo".to_string()),
            reason: Some("wrong task".to_string()),
            json: true,
        })?;
        run_memory_action(MemoryAction::Suppressions {
            action: MemorySuppressionsAction::List {
                include_inactive: false,
                json: true,
            },
        })?;

        let conn = db::open_db()?;
        let active: i64 = conn.query_row(
            "SELECT COUNT(*) FROM memory_suppressions WHERE status = 'active'",
            [],
            |row| row.get(0),
        )?;
        let feedback: i64 =
            conn.query_row("SELECT COUNT(*) FROM memory_feedback", [], |row| row.get(0))?;
        assert_eq!(active, 1);
        assert_eq!(feedback, 1);
        drop(conn);

        run_memory_action(MemoryAction::Unsuppress {
            target: "memory:1".to_string(),
            reason: Some("needed again".to_string()),
            actor: Some("test".to_string()),
            json: true,
        })?;
        let conn = db::open_db()?;
        let active: i64 = conn.query_row(
            "SELECT COUNT(*) FROM memory_suppressions WHERE status = 'active'",
            [],
            |row| row.get(0),
        )?;
        assert_eq!(active, 0);
        Ok(())
    }
}