remem-ai 0.3.8

Persistent memory for Claude Code — single binary, zero subprocesses
Documentation
use anyhow::Result;

use crate::db;

use super::native::sync_native_memory;

pub async fn session_init() -> Result<()> {
    let timer = crate::log::Timer::start("session-init", "");
    let input = std::io::read_to_string(std::io::stdin())?;
    crate::log::debug(
        "session-init",
        &format!("raw input: {}", crate::db::truncate_str(&input, 500)),
    );

    let Some((_adapter, event)) = crate::adapter::detect_adapter(&input) else {
        anyhow::bail!("no adapter matched session_init input");
    };

    crate::log::info(
        "session-init",
        &format!("project={} session={}", event.project, event.session_id),
    );

    let conn = db::open_db()?;
    db::upsert_session(&conn, &event.session_id, &event.project, None)?;

    timer.done(&format!("project={}", event.project));
    Ok(())
}

pub async fn observe() -> Result<()> {
    let input = std::io::read_to_string(std::io::stdin())?;

    let Some((adapter, event)) = crate::adapter::detect_adapter(&input) else {
        return Ok(());
    };
    if adapter.should_skip(&event) || should_skip_bash_event(adapter, &event) {
        return Ok(());
    }

    let Some(summary) = adapter.classify_event(&event) else {
        return Ok(());
    };

    let branch = event.cwd.as_deref().and_then(db::detect_git_branch);
    let _commit_sha = event.cwd.as_deref().and_then(db::detect_git_commit);

    let conn = db::open_db()?;
    crate::memory::insert_event(
        &conn,
        &event.session_id,
        &event.project,
        &summary.event_type,
        &summary.summary,
        summary.detail.as_deref(),
        summary.files_json.as_deref(),
        summary.exit_code,
    )?;

    let tool_input_str = event.tool_input.as_ref().map(|value| value.to_string());
    let tool_response_str = event.tool_response.as_ref().map(|value| value.to_string());
    db::enqueue_pending(
        &conn,
        &event.session_id,
        &event.project,
        &event.tool_name,
        tool_input_str.as_deref(),
        tool_response_str.as_deref(),
        event.cwd.as_deref(),
    )?;

    crate::log::info(
        "observe",
        &format!(
            "EVENT {} project={} branch={:?}",
            summary.summary, event.project, branch
        ),
    );

    if matches!(event.tool_name.as_str(), "Write" | "Edit") {
        if let Some(file_path) = event
            .tool_input
            .as_ref()
            .and_then(|value| value["file_path"].as_str())
        {
            if let Err(error) =
                sync_native_memory(&conn, &event.session_id, file_path, branch.as_deref())
            {
                crate::log::warn("observe", &format!("native memory sync failed: {}", error));
            }
        }
    }

    Ok(())
}

fn should_skip_bash_event(
    adapter: &dyn crate::adapter::ToolAdapter,
    event: &crate::adapter::ParsedHookEvent,
) -> bool {
    if event.tool_name != "Bash" {
        return false;
    }

    event
        .tool_input
        .as_ref()
        .and_then(|value| value["command"].as_str())
        .is_some_and(|command| adapter.should_skip_bash(command))
}