adaptive_memory 0.1.0

An associative memory system using spreading activation with SQLite FTS5 full-text search
Documentation
use std::path::PathBuf;
use std::process;

use clap::{Parser, Subcommand};

use adaptive_memory::{
    default_db_path, MemoryError, MemoryStore, SearchParams, DEFAULT_LIMIT, MAX_STRENGTHEN_SET,
};

#[derive(Parser)]
#[command(name = "adaptive-memory")]
#[command(about = "Adaptive memory system with spreading activation", long_about = None)]
struct Cli {
    /// Path to the database file (default: ~/.adaptive_memory.db)
    #[arg(long, global = true)]
    db: Option<PathBuf>,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Initialize the database (creates if not exists)
    Init,

    /// Add a new memory
    Add {
        /// The memory text
        text: String,

        /// Optional source identifier
        #[arg(short, long)]
        source: Option<String>,

        /// Optional datetime override (RFC3339 format, e.g. "2024-01-15T10:30:00Z")
        #[arg(short, long)]
        datetime: Option<String>,
    },

    /// Search for memories using text query and spreading activation
    Search {
        /// Search query (required, cannot be empty)
        query: String,

        /// Maximum number of results to return (also used as seed count)
        #[arg(short, long, default_value_t = DEFAULT_LIMIT)]
        limit: usize,

        /// Decay factor for relationship strength over memory distance (0 = no decay)
        #[arg(long, default_value_t = 0.0)]
        decay: f64,

        /// Energy decay per hop during spreading activation
        #[arg(long, default_value_t = 0.5)]
        energy_decay: f64,

        /// Context window: fetch N memories before/after each result (like grep -B/-A)
        #[arg(short, long, default_value_t = 0)]
        context: usize,
    },

    /// Strengthen relationships between memories
    Strengthen {
        /// Comma-separated list of memory IDs (max 10)
        ids: String,
    },
}

fn main() {
    let cli = Cli::parse();
    let db_path = cli.db.unwrap_or_else(default_db_path);

    let result = run(cli.command, &db_path);

    if let Err(e) = result {
        eprintln!("Error: {}", e);
        process::exit(1);
    }
}

fn run(command: Commands, db_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    match command {
        Commands::Init => {
            let store = MemoryStore::open(db_path)?;
            let result = serde_json::json!({
                "success": true,
                "database": db_path.display().to_string(),
                "message": "Database initialized successfully",
                "max_memory_id": store.max_memory_id()
            });
            println!("{}", serde_json::to_string_pretty(&result)?);
        }

        Commands::Add {
            text,
            source,
            datetime,
        } => {
            let mut store = MemoryStore::open(db_path)?;
            let result = store.add_with_options(&text, source.as_deref(), datetime.as_deref())?;
            println!("{}", serde_json::to_string_pretty(&result)?);
        }

        Commands::Search {
            query,
            limit,
            decay,
            energy_decay,
            context,
        } => {
            let store = MemoryStore::open(db_path)?;
            let params = SearchParams {
                limit,
                decay_factor: decay,
                energy_decay,
                context,
            };
            let result = store.search(&query, &params)?;
            println!("{}", serde_json::to_string_pretty(&result)?);
        }

        Commands::Strengthen { ids } => {
            let ids = parse_ids(&ids)?;
            let mut store = MemoryStore::open(db_path)?;
            let result = store.strengthen(&ids)?;
            println!("{}", serde_json::to_string_pretty(&result)?);
        }
    }

    Ok(())
}

fn parse_ids(ids_str: &str) -> Result<Vec<i64>, MemoryError> {
    let ids: Result<Vec<i64>, _> = ids_str
        .split(',')
        .map(|s| s.trim().parse::<i64>())
        .collect();

    let ids = ids.map_err(|e| MemoryError::InvalidInput(format!("Invalid ID format: {}", e)))?;

    if ids.is_empty() {
        return Err(MemoryError::InvalidInput(
            "At least one memory ID is required".to_string(),
        ));
    }

    if ids.len() > MAX_STRENGTHEN_SET {
        return Err(MemoryError::InvalidInput(format!(
            "Cannot strengthen more than {} memories at once (got {})",
            MAX_STRENGTHEN_SET,
            ids.len()
        )));
    }

    Ok(ids)
}