cortex-agent 0.2.0

Self-learning AI agent with persistent memory, tools, plugins, and a beautiful terminal UI
use std::sync::Arc;

use crate::tool::{param_string, ToolSpec};

pub fn skill_tools(store: Option<Arc<std::sync::Mutex<crate::memory::store::MemoryStore>>>) -> Vec<ToolSpec> {
    vec![
        create_skill_tool(store.clone()),
        get_skill_tool(store.clone()),
        update_skill_tool(store.clone()),
        delete_skill_tool(store.clone()),
        list_skills_tool(store),
    ]
}

fn get_store(store: &Option<Arc<std::sync::Mutex<crate::memory::store::MemoryStore>>>) -> Result<std::sync::MutexGuard<'_, crate::memory::store::MemoryStore>, String> {
    store.as_ref().and_then(|s| s.lock().ok()).ok_or_else(|| "Memory is not initialized.".into())
}

fn create_skill_tool(store: Option<Arc<std::sync::Mutex<crate::memory::store::MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[
        ("name", param_string("Short unique name (lowercase, hyphens)")),
        ("description", param_string("Brief summary of when to use this skill")),
        ("content", param_string("Full markdown with steps, examples, and pitfalls")),
        ("category", serde_json::json!({"type": "string", "description": "Grouping category", "default": "general"})),
    ]);
    ToolSpec::new("create_skill", "Create a new reusable skill/procedure", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let description = args.get("description").and_then(|v| v.as_str()).unwrap_or("");
            let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
            let category = args.get("category").and_then(|v| v.as_str()).unwrap_or("general");
            if let Ok(Some(existing)) = guard.get_skill(name) {
                return Ok(format!("Skill '{}' already exists (v{}). Use update_skill to modify it.", name, existing.version));
            }
            let sid = guard.save_skill(name, description, content, category).map_err(|e| format!("Failed to create skill: {}", e))?;
            Ok(format!("Skill '{}' created (v1, id={}).", name, sid))
        }),
    )
}

fn get_skill_tool(store: Option<Arc<std::sync::Mutex<crate::memory::store::MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[("name", param_string("The skill's name"))]);
    ToolSpec::new("get_skill", "Retrieve the full content of a saved skill", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("");
            match guard.get_skill(name).map_err(|e| format!("Error: {}", e))? {
                Some(s) => Ok(format!("# {} (v{}) [{}]\n{}\n\n---\n{}", s.name, s.version, s.category, s.description, s.content)),
                None => {
                    let available: Vec<String> = guard.list_skills(None).unwrap_or_default().iter().map(|s| s.name.clone()).collect();
                    let avail_str = if available.is_empty() { "(none)".into() } else { available.join(", ") };
                    Ok(format!("Skill '{}' not found. Available skills: {}", name, avail_str))
                }
            }
        }),
    )
}

fn update_skill_tool(store: Option<Arc<std::sync::Mutex<crate::memory::store::MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[
        ("name", param_string("The skill's name")),
        ("description", serde_json::json!({"type": "string", "description": "New description", "default": ""})),
        ("content", serde_json::json!({"type": "string", "description": "New full content", "default": ""})),
        ("category", serde_json::json!({"type": "string", "description": "New category", "default": ""})),
    ]);
    ToolSpec::new("update_skill", "Update an existing skill. Only provided fields are changed.", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let description = args.get("description").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
            let content = args.get("content").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
            let category = args.get("category").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
            if description.is_none() && content.is_none() && category.is_none() {
                return Err("Nothing to update. Provide at least one field.".into());
            }
            if guard.update_skill(name, description, content, category).map_err(|e| format!("Failed to update: {}", e))? {
                if let Ok(Some(s)) = guard.get_skill(name) { return Ok(format!("Skill '{}' updated (now v{}).", name, s.version)); }
                Ok(format!("Skill '{}' updated.", name))
            } else { Ok(format!("Skill '{}' not found.", name)) }
        }),
    )
}

fn delete_skill_tool(store: Option<Arc<std::sync::Mutex<crate::memory::store::MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[("name", param_string("The skill's name"))]);
    ToolSpec::new("delete_skill", "Delete a skill by name", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if guard.delete_skill(name).map_err(|e| format!("Failed to delete: {}", e))? {
                Ok(format!("Skill '{}' deleted.", name))
            } else { Ok(format!("Skill '{}' not found.", name)) }
        }),
    )
}

fn list_skills_tool(store: Option<Arc<std::sync::Mutex<crate::memory::store::MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[("category", serde_json::json!({"type": "string", "description": "Optional category filter", "default": ""}))]);
    ToolSpec::new("list_skills", "List all available skills, optionally filtered by category", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let category = args.get("category").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
            let results = guard.list_skills(category).map_err(|e| format!("Failed to list skills: {}", e))?;
            if results.is_empty() {
                let suffix = category.map(|c| format!(" in category \"{}\"", c)).unwrap_or_default();
                return Ok(format!("No skills found{}.", suffix));
            }
            let suffix = category.map(|c| format!(" [{}]", c)).unwrap_or_default();
            let mut lines = vec![format!("Skills{}:", suffix)];
            for s in &results {
                let desc_short = if s.description.len() > 100 { format!("{}...", &s.description[..100]) } else { s.description.clone() };
                lines.push(format!("  📋 {} (v{}) [{}]", s.name, s.version, s.category));
                lines.push(format!("     {}", desc_short));
            }
            Ok(lines.join("\n"))
        }),
    )
}