remem-ai 0.6.17

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::{fs::OpenOptions, io::Write, path::Path};

use anyhow::{Context, Result};

use crate::{
    cli::query_types::{ProfileSnapshotFormatArg, UserProfileAction},
    db,
    user_context::profile_snapshot::{render_markdown_profile_snapshot, ProfileSnapshotRequest},
};

use super::shared::resolve_cwd_project;

pub(in crate::cli) fn run_user_profile(action: UserProfileAction) -> Result<()> {
    match action {
        UserProfileAction::Export {
            format,
            output,
            project,
            owner_scope,
            owner_key,
            include_suppressed,
            include_sensitive,
            include_inactive,
            include_deleted,
            include_manual_summaries,
        } => {
            let project = project.unwrap_or_else(|| resolve_cwd_project().1);
            let source_of_truth = db::absolute_data_dir()?.join("remem.db");
            let conn = db::open_db_read_only()?;
            match format {
                ProfileSnapshotFormatArg::Markdown => {
                    let markdown = render_markdown_profile_snapshot(
                        &conn,
                        &ProfileSnapshotRequest {
                            project: &project,
                            owner_scope: owner_scope.db_value(),
                            owner_key: owner_key.as_deref(),
                            source_of_truth: &source_of_truth,
                            include_suppressed,
                            include_sensitive,
                            include_inactive,
                            include_deleted,
                            include_manual_summaries,
                        },
                    )?;
                    write_snapshot_output(&markdown, output.as_deref())?;
                }
            }
        }
    }
    Ok(())
}

fn write_snapshot_output(markdown: &str, output: Option<&Path>) -> Result<()> {
    let Some(path) = output else {
        print!("{markdown}");
        return Ok(());
    };
    write_new_file(path, markdown.as_bytes())
}

fn write_new_file(path: &Path, contents: &[u8]) -> Result<()> {
    let mut options = OpenOptions::new();
    options.write(true).create_new(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        options.mode(0o600);
    }
    let mut file = options
        .open(path)
        .with_context(|| format!("create profile snapshot {}", path.display()))?;
    file.write_all(contents)
        .with_context(|| format!("write profile snapshot {}", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn profile_snapshot_output_refuses_to_overwrite_existing_file() -> Result<()> {
        let dir = crate::db::test_support::ScopedTestDataDir::new("profile-output-overwrite");
        std::fs::create_dir_all(&dir.path)?;
        let path = dir.path.join("profile.md");
        std::fs::write(&path, "existing")?;

        let err = write_snapshot_output("new", Some(&path)).expect_err("must not overwrite");
        assert!(err.to_string().contains("create profile snapshot"));
        assert_eq!(std::fs::read_to_string(&path)?, "existing");
        Ok(())
    }

    #[test]
    fn profile_snapshot_output_writes_new_file() -> Result<()> {
        let dir = crate::db::test_support::ScopedTestDataDir::new("profile-output-new");
        std::fs::create_dir_all(&dir.path)?;
        let path: std::path::PathBuf = dir.path.join("profile.md");

        write_snapshot_output("snapshot", Some(&path))?;

        assert_eq!(std::fs::read_to_string(&path)?, "snapshot");
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
            assert_eq!(mode, 0o600);
        }
        Ok(())
    }
}