remem-ai 0.5.207

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::io::Read;

use anyhow::Result;

use crate::cli::cwd::resolve_cwd_arg;
use crate::cli::types::{RuleActionArg, RuleHostArg, RulesAction};
use crate::db;
use crate::rules::{self, RuleAction, RulePredicate};

pub(in crate::cli) fn run_rules(action: RulesAction) -> Result<()> {
    let (project_arg, mutation) = match action {
        RulesAction::List { project } => (project, None),
        RulesAction::Disable { rule_id } => (None, Some(RuleMutation::Disabled(rule_id, true))),
        RulesAction::Enable { rule_id } => (None, Some(RuleMutation::Disabled(rule_id, false))),
        RulesAction::SetAction {
            rule_id,
            action,
            host,
        } => (
            None,
            Some(RuleMutation::Action(rule_id, action.into(), host)),
        ),
        RulesAction::Eval { host } => return run_rules_eval(host),
    };
    let project = db::project_from_cwd(&resolve_cwd_arg(project_arg));
    let data_dir = db::absolute_data_dir()?;

    if let Some(mutation) = mutation {
        let conn = db::open_db()?;
        let (rule_id, message) = match mutation {
            RuleMutation::Disabled(rule_id, disabled) => {
                rules::set_rule_disabled(&conn, &data_dir, &project, &rule_id, disabled)?;
                let state = if disabled { "disabled" } else { "enabled" };
                (rule_id, format!("Rule override saved: {state}"))
            }
            RuleMutation::Action(rule_id, action, host) => {
                rules::set_rule_action(
                    &conn,
                    &data_dir,
                    &project,
                    &rule_id,
                    action,
                    host == Some(RuleHostArg::ClaudeCode),
                )?;
                (
                    rule_id,
                    format!("Rule action override saved: {}", action_label(action)),
                )
            }
        };
        println!("{message} ({rule_id}); pending worker rebuild.");
        return Ok(());
    }

    let project_rules = rules::list_project_rules(&data_dir, &project)?;
    if project_rules.rules.is_empty() {
        println!("No compiled rules for project '{}'.", project_rules.project);
        return Ok(());
    }
    println!(
        "Compiled rules for '{}' (compiled_at_epoch={}):",
        project_rules.project, project_rules.compiled_at_epoch
    );
    for rule in project_rules.rules {
        let (predicate_kind, predicate_data) = predicate_display(&rule.predicate);
        let override_action = rule
            .override_state
            .action_override
            .map(action_label)
            .unwrap_or("default");
        println!(
            "  {} source_memory={} reinforcement={} predicate={} data={} base_action={} effective_action={} override={} disabled={}",
            rule.rule_id,
            rule.source_memory_id,
            rule.reinforcement_count,
            predicate_kind,
            predicate_data,
            action_label(rule.action),
            action_label(rule.effective_action()),
            override_action,
            rule.override_state.disabled
        );
    }
    Ok(())
}

enum RuleMutation {
    Disabled(String, bool),
    Action(String, RuleAction, Option<RuleHostArg>),
}

fn run_rules_eval(host: Option<RuleHostArg>) -> Result<()> {
    let data_dir = match db::absolute_data_dir() {
        Ok(data_dir) => data_dir,
        Err(error) => {
            crate::log::error(
                "rules-eval",
                &format!("resolve remem data directory: {error:#}"),
            );
            return Ok(());
        }
    };
    let mut raw = String::new();
    if let Err(error) = std::io::stdin().read_to_string(&mut raw) {
        rules::log_evaluation_error_once_with_diagnostic(
            &data_dir,
            None,
            None,
            &[rules::EvaluationDiagnosticCode::HookInputRead],
            &format!("read Claude PreToolUse hook input: {error}"),
        );
        return Ok(());
    }
    let session_hint = rules::session_id_hint(&raw);
    let project_hint = rules::project_hint(&raw);
    let config = match crate::runtime_config::rule_compilation_config() {
        Ok(config) => config,
        Err(error) => {
            rules::log_evaluation_error_once_with_diagnostic(
                &data_dir,
                session_hint.as_deref(),
                None,
                &[rules::EvaluationDiagnosticCode::Config],
                &format!("read rule compilation config: {error:#}"),
            );
            return Ok(());
        }
    };
    let evaluated = match rules::evaluate_pre_tool_use_with_diagnostics(
        &raw,
        host.map(rule_host_label),
        &data_dir,
        config.enabled,
    ) {
        Ok(evaluated) => evaluated,
        Err(error) => {
            rules::log_evaluation_error_once_with_diagnostic(
                &data_dir,
                session_hint.as_deref(),
                project_hint.as_deref(),
                &[rules::EvaluationDiagnosticCode::HookInput],
                &format!("{error:#}"),
            );
            return Ok(());
        }
    };
    if !evaluated.evaluation.diagnostics.is_empty() {
        rules::log_evaluation_error_once_with_diagnostic(
            &data_dir,
            evaluated.evaluation.session_id.as_deref(),
            evaluated.project.as_deref(),
            &evaluated.diagnostic_codes,
            &evaluated.evaluation.diagnostics.join("; "),
        );
        return Ok(());
    }
    if let Some(output) = evaluated.evaluation.output {
        match serde_json::to_string(&output) {
            Ok(output) => println!("{output}"),
            Err(error) => rules::log_evaluation_error_once_with_diagnostic(
                &data_dir,
                evaluated.evaluation.session_id.as_deref(),
                evaluated.project.as_deref(),
                &[rules::EvaluationDiagnosticCode::OutputSerialize],
                &format!("serialize Claude PreToolUse hook output: {error}"),
            ),
        }
    }
    Ok(())
}

fn rule_host_label(host: RuleHostArg) -> &'static str {
    match host {
        RuleHostArg::ClaudeCode => crate::runtime_config::CLAUDE_HOST,
        RuleHostArg::CodexCli => crate::runtime_config::CODEX_HOST,
    }
}

impl From<RuleActionArg> for RuleAction {
    fn from(value: RuleActionArg) -> Self {
        match value {
            RuleActionArg::Warn => Self::Warn,
            RuleActionArg::Block => Self::Block,
        }
    }
}

fn action_label(action: RuleAction) -> &'static str {
    match action {
        RuleAction::Warn => "warn",
        RuleAction::Block => "block",
    }
}

fn predicate_display(predicate: &RulePredicate) -> (&'static str, String) {
    match predicate {
        RulePredicate::CommandRegex { pattern, .. } => ("command_regex", format!("{pattern:?}")),
        RulePredicate::CommitTrailerForbidden { trailer, .. } => {
            ("commit_trailer_forbidden", format!("{trailer:?}"))
        }
    }
}