minni 0.1.1

Local memory, task, and codebase indexing tool for AI agents
Documentation
use crate::cli::ContextCommands;
use crate::context::{ContextManager, ItemType};
use crate::db::Database;
use anyhow::Result;
use colored::Colorize;
use std::env;

pub fn run(command: ContextCommands) -> Result<()> {
    let project_root = env::current_dir()?;

    if !Database::minni_dir_exists(&project_root) {
        println!(
            "{} Minni not initialized. Run {} first.",
            "Error:".red(),
            "minni init".cyan()
        );
        return Ok(());
    }

    let db = Database::open(&project_root)?;
    let ctx_mgr = ContextManager::new(&db);

    match command {
        ContextCommands::Save { name, description } => {
            let context = ctx_mgr.save_context(name, description)?;
            println!("{} Context saved!", "".green());
            println!("  ID: {}", context.id.cyan());
            println!("  Name: {}", context.name.cyan());
            println!("  Project: {}", ctx_mgr.project_name().yellow());
        }

        ContextCommands::Load { id } => match ctx_mgr.load_context(&id)? {
            Some(full_ctx) => {
                println!("{}", full_ctx.to_markdown());
            }
            None => {
                println!("{} Context not found: {}", "Error:".red(), id);
            }
        },

        ContextCommands::List { verbose } => {
            let contexts = ctx_mgr.list_contexts()?;

            if contexts.is_empty() {
                println!("{} No saved contexts.", "!".yellow());
                return Ok(());
            }

            println!("{} Saved contexts:\n", "".blue());

            for ctx in contexts {
                if verbose {
                    println!(
                        "{} {} ({})",
                        "".cyan(),
                        ctx.name.bold(),
                        ctx.project_name.yellow()
                    );
                    println!("    ID: {}", ctx.id.dimmed());
                    if let Some(desc) = &ctx.description {
                        println!("    Description: {}", desc);
                    }
                    println!("    Items: {}", ctx.item_count);
                    println!("    Created: {}", ctx.created_at);
                    println!("    Updated: {}", ctx.updated_at);
                    println!();
                } else {
                    let desc = ctx
                        .description
                        .as_deref()
                        .map(|d| format!(" - {}", d))
                        .unwrap_or_default();
                    println!(
                        "{} {} ({}){}",
                        "".cyan(),
                        ctx.name.bold(),
                        ctx.project_name.yellow(),
                        desc.dimmed()
                    );
                }
            }
        }

        ContextCommands::Delete { id } => {
            if ctx_mgr.delete_context(&id)? {
                println!("{} Context deleted: {}", "".green(), id);
            } else {
                println!("{} Context not found: {}", "Error:".red(), id);
            }
        }

        ContextCommands::Show { id } => {
            match ctx_mgr.load_context(&id)? {
                Some(full_ctx) => {
                    // Output as JSON for programmatic consumption
                    println!("{}", full_ctx.to_json()?);
                }
                None => {
                    println!("{} Context not found: {}", "Error:".red(), id);
                }
            }
        }

        ContextCommands::Add { key, value } => {
            // Try to get or create a default context for current project
            let contexts = ctx_mgr.list_contexts_by_project(ctx_mgr.project_name())?;
            let context_name = if let Some(latest) = contexts.first() {
                latest.name.clone()
            } else {
                // Create a new context
                let ctx = ctx_mgr.save_context(None, None)?;
                ctx.name
            };

            let item_type = ItemType::from_str(&key);
            ctx_mgr.add_item(&context_name, &key, &value, item_type)?;
            println!(
                "{} Added to context '{}':",
                "".green(),
                context_name.cyan()
            );
            println!("  {}: {}", key.yellow(), value);
        }

        ContextCommands::Snapshot {
            name,
            include_diff,
            conversation,
        } => {
            println!("{} Creating session snapshot...", "".blue());

            // Read conversation from stdin if requested
            let conv = if conversation {
                println!("Reading conversation from stdin (Ctrl+D to finish)...");
                use std::io::{self, Read};
                let mut buffer = String::new();
                io::stdin().read_to_string(&mut buffer)?;

                // Parse simple format: "USER: ...\nASSISTANT: ..."
                Some(parse_conversation(&buffer))
            } else {
                None
            };

            let snapshot = ctx_mgr.create_snapshot(
                name.unwrap_or_else(|| {
                    format!("snapshot-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"))
                }),
                Some("Session snapshot".to_string()),
                include_diff,
                conv,
            )?;

            // Auto-export to .minni/snapshots/
            let snapshot_dir = project_root.join(".minni").join("snapshots");
            std::fs::create_dir_all(&snapshot_dir)?;
            let filename = format!("{}.json", snapshot.name);
            let output_path = snapshot_dir.join(&filename);

            ctx_mgr.export_snapshot(&snapshot, &output_path.to_string_lossy())?;

            println!("{} Snapshot created!", "".green());
            println!("  Name: {}", snapshot.name.cyan());
            println!(
                "  Files changed: {}",
                snapshot.modified_files.len().to_string().yellow()
            );
            if let Some(branch) = &snapshot.git_branch {
                println!("  Git branch: {}", branch.yellow());
            }
            println!("  Saved to: {}", output_path.display().to_string().cyan());
        }

        ContextCommands::Export { id, output } => {
            match ctx_mgr.load_context(&id)? {
                Some(full_ctx) => {
                    let output_path = output.unwrap_or_else(|| format!("{}.json", id));

                    // Convert to snapshot format
                    let snapshot = crate::context::ContextSnapshot {
                        version: "1.0".to_string(),
                        created_at: full_ctx.context.created_at.clone(),
                        project_name: ctx_mgr.project_name().to_string(),
                        project_path: project_root.to_string_lossy().to_string(),
                        name: full_ctx.context.name.clone(),
                        description: full_ctx.context.description.clone(),
                        conversation: None,
                        modified_files: vec![],
                        git_diff: None,
                        git_branch: None,
                        tasks: vec![],
                        notes: vec![],
                        relevant_files: vec![],
                        code_snippets: vec![],
                    };

                    ctx_mgr.export_snapshot(&snapshot, &output_path)?;

                    println!("{} Context exported!", "".green());
                    println!("  File: {}", output_path.cyan());
                }
                None => {
                    println!("{} Context not found: {}", "Error:".red(), id);
                }
            }
        }

        ContextCommands::Import { file, name } => {
            println!("{} Importing context from {}...", "".blue(), file.cyan());

            let snapshot = ctx_mgr.import_snapshot(&file)?;
            let context_id = ctx_mgr.snapshot_to_context(snapshot.clone(), name)?;

            println!("{} Context imported!", "".green());
            println!("  ID: {}", context_id.cyan());
            println!("  Name: {}", snapshot.name.cyan());
            println!("  Original project: {}", snapshot.project_name.yellow());
        }
    }

    Ok(())
}

fn parse_conversation(text: &str) -> Vec<crate::context::ConversationTurn> {
    let mut turns = Vec::new();
    let mut current_role = String::new();
    let mut current_content = String::new();

    for line in text.lines() {
        if line.starts_with("USER:") {
            if !current_role.is_empty() {
                turns.push(crate::context::ConversationTurn {
                    role: current_role.clone(),
                    content: current_content.trim().to_string(),
                    timestamp: chrono::Utc::now().to_rfc3339(),
                });
            }
            current_role = "user".to_string();
            current_content = line[5..].to_string();
        } else if line.starts_with("ASSISTANT:") {
            if !current_role.is_empty() {
                turns.push(crate::context::ConversationTurn {
                    role: current_role.clone(),
                    content: current_content.trim().to_string(),
                    timestamp: chrono::Utc::now().to_rfc3339(),
                });
            }
            current_role = "assistant".to_string();
            current_content = line[10..].to_string();
        } else if !current_role.is_empty() {
            current_content.push('\n');
            current_content.push_str(line);
        }
    }

    if !current_role.is_empty() {
        turns.push(crate::context::ConversationTurn {
            role: current_role,
            content: current_content.trim().to_string(),
            timestamp: chrono::Utc::now().to_rfc3339(),
        });
    }

    turns
}