gigi-cli 1.0.1

Gigi — A Claude Code-like AI coding assistant CLI in Rust
use anyhow::{Context, Result};
use colored::*;
use std::path::Path;

use crate::agent::Agent;

// =============================================================================
// /memory — Persistent memory notes stored across sessions
//
// Usage:
//   /memory              — Show all memory notes
//   /memory add <text>   — Add a memory note
//   /memory clear        — Clear all memory notes
// =============================================================================

const MEMORY_FILE: &str = "memory.json";

#[derive(serde::Serialize, serde::Deserialize, Default)]
struct MemoryStore {
    notes: Vec<MemoryNote>,
}

#[derive(serde::Serialize, serde::Deserialize)]
struct MemoryNote {
    text: String,
    added_at: String,
}

pub async fn handle(agent: &mut Agent, args: &str) -> Result<()> {
    let memory_path = agent.config.session_dir.join(MEMORY_FILE);

    if args.is_empty() || args == "show" {
        // Show all memory notes
        let store = load_memory(&memory_path).await;

        if store.notes.is_empty() {
            println!("{}", "No memory notes saved.".dimmed());
            println!(
                "{}",
                "Use '/memory add <text>' to add a note.".dimmed()
            );
        } else {
            println!("{}", "\n━━━ Memory Notes ━━━".bold());
            for (i, note) in store.notes.iter().enumerate() {
                println!(
                    "  {}. {} {}",
                    i + 1,
                    note.text,
                    format!("({})", note.added_at).dimmed()
                );
            }
        }
    } else if let Some(text) = args.strip_prefix("add ") {
        let text = text.trim();
        if text.is_empty() {
            println!("{}", "Please provide text to remember.".yellow());
            return Ok(());
        }

        let mut store = load_memory(&memory_path).await;
        store.notes.push(MemoryNote {
            text: text.to_string(),
            added_at: chrono::Utc::now().format("%Y-%m-%d %H:%M").to_string(),
        });
        save_memory(&memory_path, &store).await?;

        println!("{}", format!("✓ Remembered: {}", text).green());
    } else if args == "clear" {
        let store = MemoryStore::default();
        save_memory(&memory_path, &store).await?;
        println!("{}", "✓ All memory notes cleared.".green());
    } else {
        println!("{}", "Usage: /memory [show|add <text>|clear]".yellow());
    }

    Ok(())
}

async fn load_memory(path: &Path) -> MemoryStore {
    if path.exists() {
        match tokio::fs::read_to_string(path).await {
            Ok(json) => serde_json::from_str(&json).unwrap_or_default(),
            Err(_) => MemoryStore::default(),
        }
    } else {
        MemoryStore::default()
    }
}

async fn save_memory(path: &Path, store: &MemoryStore) -> Result<()> {
    // Ensure parent directory exists
    if let Some(parent) = path.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }

    let json = serde_json::to_string_pretty(store)
        .context("Failed to serialize memory")?;

    tokio::fs::write(path, json)
        .await
        .context("Failed to write memory file")?;

    Ok(())
}