fur-cli 1.9.0

Turn your AI chats into a durable, local-first diary. Save messages, attach notes, organize conversations, and stop losing context every time the model forgets you exist.
Documentation
use rand::prelude::IndexedRandom;
use serde_json::{json, Value};
use std::fs;
use std::path::Path;

pub fn load_avatars() -> Value {
    let path = Path::new(".fur/avatars.json");
    if path.exists() {
        let content = fs::read_to_string(path).unwrap_or_default();
        serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
    } else {
        json!({})
    }
}

pub fn save_avatars(avatars: &Value) {
    let path = Path::new(".fur/avatars.json");
    if let Ok(serialized) = serde_json::to_string_pretty(avatars) {
        let _ = fs::write(path, serialized);
    }
}

pub fn resolve_avatar(avatars: &Value, key: &str) -> (String, String) {
    // If key matches a known avatar name → return (name, emoji)
    if let Some(emoji) = avatars.get(key).and_then(|v| v.as_str()) {
        return (key.to_string(), emoji.to_string());
    }

    // If key looks like an emoji already → reverse-lookup name
    if let Some((name, _)) = avatars
        .as_object()
        .and_then(|map| map.iter().find(|(_, v)| v.as_str() == Some(key)))
    {
        return (name.clone(), key.to_string());
    }

    (key.to_string(), "🐾".to_string()) // fallback
}

/// Return true if the name clearly looks like a bot/LLM.
fn is_bot_name(name: &str) -> bool {
    let n = name.to_lowercase();

    // lean markers (substring is fine for most)
    const MARKERS: [&str; 8] = [
        "gpt", "claude", "gemini", "bard", "grok", "bot", "ai", "llm",
    ];

    // fast path: any simple substring marker
    if MARKERS.iter().any(|m| n.contains(m)) {
        return true;
    }

    // safer whole-word match for "agent" (avoid hitting "management")
    if n.split(|c: char| !c.is_alphanumeric())
        .any(|tok| tok == "agent")
    {
        return true;
    }

    false
}

/// Emoji selection for new/unknown avatars:
/// - Fox stays reserved for main via your `save_avatars_with_main`
/// - Bots → 🤖
pub fn get_random_emoji_for_name(name: &str) -> String {
    if is_bot_name(name) {
        return "🤖".to_string();
    }
    let pool = ["👤"];
    pool.choose(&mut rand::rng()).unwrap_or(&"👔").to_string()
}