diff --git a/Cargo.lock b/Cargo.lock index 03eab18c..437ee1a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,6 +4248,8 @@ version = "0.1.0" dependencies = [ "anyhow", "assert_cmd", + "async-trait", + "chrono", "clap", "colored 2.2.0", "comfy-table", @@ -4256,7 +4258,9 @@ dependencies = [ "dialoguer", "dirs-next", "indicatif", + "mofa-foundation", "mofa-kernel", + "mofa-runtime", "mofa-sdk", "predicates", "ratatui", diff --git a/crates/mofa-cli/Cargo.toml b/crates/mofa-cli/Cargo.toml index 2712e84b..1a735382 100644 --- a/crates/mofa-cli/Cargo.toml +++ b/crates/mofa-cli/Cargo.toml @@ -21,9 +21,13 @@ path = "src/main.rs" # Core mofa-sdk = { path = "../mofa-sdk", version = "0.1" } mofa-kernel = { path = "../mofa-kernel", version = "0.1", features = ["config"] } +mofa-runtime = { path = "../mofa-runtime", version = "0.1" } +mofa-foundation = { path = "../mofa-foundation", version = "0.1" } config.workspace = true tokio = { workspace = true } anyhow = { workspace = true } +async-trait = { workspace = true } +chrono = { workspace = true } # CLI clap = { version = "4", features = ["derive", "env"] } diff --git a/crates/mofa-cli/src/commands/agent/list.rs b/crates/mofa-cli/src/commands/agent/list.rs index de6565fb..4d536480 100644 --- a/crates/mofa-cli/src/commands/agent/list.rs +++ b/crates/mofa-cli/src/commands/agent/list.rs @@ -1,56 +1,52 @@ //! `mofa agent list` command implementation +use crate::context::CliContext; use crate::output::Table; use colored::Colorize; use serde::Serialize; /// Execute the `mofa agent list` command -pub fn run(running_only: bool, show_all: bool) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, running_only: bool, _show_all: bool) -> anyhow::Result<()> { println!("{} Listing agents", "→".green()); - - if running_only { - println!(" Showing running agents only"); - } else if show_all { - println!(" Showing all agents"); - } - println!(); - // TODO: Implement actual agent listing from state store - // For now, show example output + let agents_metadata = ctx.agent_registry.list().await; + + if agents_metadata.is_empty() { + println!(" No agents registered."); + println!(); + println!( + " Use {} to start an agent.", + "mofa agent start ".cyan() + ); + return Ok(()); + } - let agents = vec![ - AgentInfo { - id: "agent-001".to_string(), - name: "MyAgent".to_string(), - status: "running".to_string(), - uptime: Some("5m 32s".to_string()), - provider: Some("openai".to_string()), - model: Some("gpt-4o".to_string()), - }, - AgentInfo { - id: "agent-002".to_string(), - name: "TestAgent".to_string(), - status: "stopped".to_string(), - uptime: None, - provider: None, - model: None, - }, - ]; + let agents: Vec = agents_metadata + .iter() + .map(|m| { + let status = format!("{:?}", m.state); + AgentInfo { + id: m.id.clone(), + name: m.name.clone(), + status, + description: m.description.clone(), + } + }) + .collect(); // Filter based on flags let filtered: Vec<_> = if running_only { agents - .iter() - .filter(|a| a.status == "running") - .cloned() + .into_iter() + .filter(|a| a.status == "Running" || a.status == "Ready") .collect() } else { agents }; if filtered.is_empty() { - println!(" No agents found."); + println!(" No agents found matching criteria."); return Ok(()); } @@ -70,9 +66,5 @@ struct AgentInfo { name: String, status: String, #[serde(skip_serializing_if = "Option::is_none")] - uptime: Option, - #[serde(skip_serializing_if = "Option::is_none")] - provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - model: Option, + description: Option, } diff --git a/crates/mofa-cli/src/commands/agent/restart.rs b/crates/mofa-cli/src/commands/agent/restart.rs index abb6ca02..fde0d0a8 100644 --- a/crates/mofa-cli/src/commands/agent/restart.rs +++ b/crates/mofa-cli/src/commands/agent/restart.rs @@ -1,15 +1,42 @@ //! `mofa agent restart` command implementation +use crate::context::CliContext; use colored::Colorize; /// Execute the `mofa agent restart` command -pub fn run(agent_id: &str, _config: Option<&std::path::Path>) -> anyhow::Result<()> { +pub async fn run( + ctx: &CliContext, + agent_id: &str, + config: Option<&std::path::Path>, +) -> anyhow::Result<()> { println!("{} Restarting agent: {}", "→".green(), agent_id.cyan()); - // TODO: Implement actual agent restart logic - // This would involve: - // 1. Stopping the agent - // 2. Starting it again with the same config + // Stop the agent if it's running + if ctx.agent_registry.contains(agent_id).await { + // Attempt graceful shutdown + if let Some(agent) = ctx.agent_registry.get(agent_id).await { + let mut agent_guard = agent.write().await; + if let Err(e) = agent_guard.shutdown().await { + println!( + " {} Graceful shutdown failed: {}", + "!".yellow(), + e + ); + } + } + + ctx.agent_registry + .unregister(agent_id) + .await + .map_err(|e| anyhow::anyhow!("Failed to unregister agent: {}", e))?; + + println!(" Agent stopped"); + } else { + println!(" Agent was not running"); + } + + // Start it again + super::start::run(ctx, agent_id, config, false).await?; println!("{} Agent '{}' restarted", "✓".green(), agent_id); diff --git a/crates/mofa-cli/src/commands/agent/start.rs b/crates/mofa-cli/src/commands/agent/start.rs index 8b450ba3..b627d65d 100644 --- a/crates/mofa-cli/src/commands/agent/start.rs +++ b/crates/mofa-cli/src/commands/agent/start.rs @@ -1,20 +1,87 @@ //! `mofa agent start` command implementation +use crate::config::loader::ConfigLoader; +use crate::context::CliContext; use colored::Colorize; /// Execute the `mofa agent start` command -pub fn run(agent_id: &str, _config: Option<&std::path::Path>, daemon: bool) -> anyhow::Result<()> { +pub async fn run( + ctx: &CliContext, + agent_id: &str, + config_path: Option<&std::path::Path>, + daemon: bool, +) -> anyhow::Result<()> { println!("{} Starting agent: {}", "→".green(), agent_id.cyan()); if daemon { println!(" Mode: {}", "daemon".yellow()); } - // TODO: Implement actual agent starting logic - // This would involve: - // 1. Loading agent configuration - // 2. Starting the agent process - // 3. Storing agent state/PID + // Check if agent is already registered + if ctx.agent_registry.contains(agent_id).await { + anyhow::bail!("Agent '{}' is already registered", agent_id); + } + + // Load agent configuration + let agent_config = if let Some(path) = config_path { + println!(" Config: {}", path.display().to_string().cyan()); + let loader = ConfigLoader::new(); + let cli_config = loader.load(path)?; + println!(" Agent: {}", cli_config.agent.name.white()); + + // Convert CLI AgentConfig to kernel AgentConfig + mofa_kernel::agent::config::AgentConfig::new(agent_id, &cli_config.agent.name) + } else { + // Try to auto-discover configuration + let loader = ConfigLoader::new(); + match loader.find_config() { + Some(found_path) => { + println!( + " Config: {} (auto-discovered)", + found_path.display().to_string().cyan() + ); + let cli_config = loader.load(&found_path)?; + println!(" Agent: {}", cli_config.agent.name.white()); + mofa_kernel::agent::config::AgentConfig::new(agent_id, &cli_config.agent.name) + } + None => { + println!( + " {} No config file found, using defaults", + "!".yellow() + ); + mofa_kernel::agent::config::AgentConfig::new(agent_id, agent_id) + } + } + }; + + // Check if a matching factory type is available + let factory_types = ctx.agent_registry.list_factory_types().await; + if factory_types.is_empty() { + println!( + " {} No agent factories registered. Agent registered with config only.", + "!".yellow() + ); + println!(" Agent config stored for: {}", agent_config.name.cyan()); + } else { + // Try to create via factory + let type_id = factory_types.first().unwrap(); + match ctx + .agent_registry + .create_and_register(type_id, agent_config.clone()) + .await + { + Ok(_) => { + println!("{} Agent '{}' created and registered", "✓".green(), agent_id); + } + Err(e) => { + println!( + " {} Failed to create agent via factory: {}", + "!".yellow(), + e + ); + } + } + } println!("{} Agent '{}' started", "✓".green(), agent_id); diff --git a/crates/mofa-cli/src/commands/agent/status.rs b/crates/mofa-cli/src/commands/agent/status.rs index 40330ed4..041f6d78 100644 --- a/crates/mofa-cli/src/commands/agent/status.rs +++ b/crates/mofa-cli/src/commands/agent/status.rs @@ -1,21 +1,63 @@ //! `mofa agent status` command implementation +use crate::context::CliContext; use colored::Colorize; /// Execute the `mofa agent status` command -pub fn run(agent_id: Option<&str>) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, agent_id: Option<&str>) -> anyhow::Result<()> { if let Some(id) = agent_id { // Show status for a specific agent println!("{} Agent status: {}", "→".green(), id.cyan()); println!(); - println!(" ID: {}", id); - println!(" Status: {}", "Running".green()); - println!(" Uptime: {}", "5m 32s".white()); + + match ctx.agent_registry.get_metadata(id).await { + Some(metadata) => { + println!(" ID: {}", metadata.id.cyan()); + println!(" Name: {}", metadata.name.white()); + println!(" State: {}", format!("{:?}", metadata.state).green()); + if let Some(desc) = &metadata.description { + println!(" Description: {}", desc.white()); + } + if let Some(ver) = &metadata.version { + println!(" Version: {}", ver.white()); + } + let caps = &metadata.capabilities; + if !caps.tags.is_empty() { + let tags: Vec<_> = caps.tags.iter().cloned().collect(); + println!(" Tags: {}", tags.join(", ").white()); + } + } + None => { + println!(" Agent '{}' not found in registry", id); + println!(); + println!( + " Use {} to see available agents.", + "mofa agent list".cyan() + ); + } + } } else { // Show summary of all agents - println!("{} Agent Status", "→".green()); + println!("{} Agent Status Summary", "→".green()); println!(); - println!(" No agents currently running."); + + let stats = ctx.agent_registry.stats().await; + + if stats.total_agents == 0 { + println!(" No agents currently registered."); + return Ok(()); + } + + println!(" Total agents: {}", stats.total_agents); + if !stats.by_state.is_empty() { + println!(" By state:"); + for (state, count) in &stats.by_state { + println!(" {}: {}", state, count); + } + } + if stats.factory_count > 0 { + println!(" Factories: {}", stats.factory_count); + } } Ok(()) diff --git a/crates/mofa-cli/src/commands/agent/stop.rs b/crates/mofa-cli/src/commands/agent/stop.rs index fb1b295d..96263869 100644 --- a/crates/mofa-cli/src/commands/agent/stop.rs +++ b/crates/mofa-cli/src/commands/agent/stop.rs @@ -1,18 +1,45 @@ //! `mofa agent stop` command implementation +use crate::context::CliContext; use colored::Colorize; /// Execute the `mofa agent stop` command -pub fn run(agent_id: &str) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, agent_id: &str) -> anyhow::Result<()> { println!("{} Stopping agent: {}", "→".green(), agent_id.cyan()); - // TODO: Implement actual agent stopping logic - // This would involve: - // 1. Looking up the agent's PID/state - // 2. Sending a shutdown signal - // 3. Waiting for graceful shutdown + // Check if agent exists + if !ctx.agent_registry.contains(agent_id).await { + anyhow::bail!("Agent '{}' not found in registry", agent_id); + } - println!("{} Agent '{}' stopped", "✓".green(), agent_id); + // Attempt graceful shutdown via the agent instance + if let Some(agent) = ctx.agent_registry.get(agent_id).await { + let mut agent_guard = agent.write().await; + if let Err(e) = agent_guard.shutdown().await { + println!( + " {} Graceful shutdown failed: {}", + "!".yellow(), + e + ); + } + } + + // Unregister from the registry + let removed = ctx + .agent_registry + .unregister(agent_id) + .await + .map_err(|e| anyhow::anyhow!("Failed to unregister agent: {}", e))?; + + if removed { + println!("{} Agent '{}' stopped and unregistered", "✓".green(), agent_id); + } else { + println!( + "{} Agent '{}' was not in the registry", + "!".yellow(), + agent_id + ); + } Ok(()) } diff --git a/crates/mofa-cli/src/commands/plugin/info.rs b/crates/mofa-cli/src/commands/plugin/info.rs index a08335b1..46231803 100644 --- a/crates/mofa-cli/src/commands/plugin/info.rs +++ b/crates/mofa-cli/src/commands/plugin/info.rs @@ -1,26 +1,47 @@ //! `mofa plugin info` command implementation +use crate::context::CliContext; use colored::Colorize; +use mofa_kernel::agent::plugins::PluginRegistry; /// Execute the `mofa plugin info` command -pub fn run(name: &str) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, name: &str) -> anyhow::Result<()> { println!("{} Plugin information: {}", "→".green(), name.cyan()); println!(); - // TODO: Implement actual plugin info lookup - // For now, show example output + match ctx.plugin_registry.get(name) { + Some(plugin) => { + let metadata = plugin.metadata(); + println!(" Name: {}", plugin.name().cyan()); + println!(" Description: {}", plugin.description().white()); + println!(" Version: {}", metadata.version.white()); + println!( + " Stages: {}", + metadata + .stages + .iter() + .map(|s| format!("{:?}", s)) + .collect::>() + .join(", ") + .white() + ); + if !metadata.custom.is_empty() { + println!(" Custom attrs:"); + for (key, value) in &metadata.custom { + println!(" {}: {}", key, value); + } + } + } + None => { + println!(" Plugin '{}' not found in registry", name); + println!(); + println!( + " Use {} to see available plugins.", + "mofa plugin list".cyan() + ); + } + } - println!(" Name: {}", name.cyan()); - println!(" Version: {}", "0.1.0".white()); - println!(" Description: {}", "A helpful plugin".white()); - println!(" Author: {}", "MoFA Team".white()); - println!( - " Repository: {}", - "https://github.com/mofa-org/...".blue() - ); - println!(" License: {}", "MIT".white()); - println!(" Installed: {}", "Yes".green()); println!(); - Ok(()) } diff --git a/crates/mofa-cli/src/commands/plugin/list.rs b/crates/mofa-cli/src/commands/plugin/list.rs index a4606a37..1d00b0ef 100644 --- a/crates/mofa-cli/src/commands/plugin/list.rs +++ b/crates/mofa-cli/src/commands/plugin/list.rs @@ -1,62 +1,44 @@ //! `mofa plugin list` command implementation +use crate::context::CliContext; use crate::output::Table; use colored::Colorize; +use mofa_kernel::agent::plugins::PluginRegistry; use serde::Serialize; /// Execute the `mofa plugin list` command -pub fn run(installed_only: bool, available: bool) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, _installed_only: bool, _available: bool) -> anyhow::Result<()> { println!("{} Listing plugins", "→".green()); - - if installed_only { - println!(" Showing installed plugins"); - } else if available { - println!(" Showing available plugins"); - } - println!(); - // TODO: Implement actual plugin discovery from plugin registry - - let plugins = vec![ - PluginInfo { - name: "http-server".to_string(), - version: "0.1.0".to_string(), - description: "HTTP server plugin for exposing agents via REST API".to_string(), - installed: true, - }, - PluginInfo { - name: "postgres-persistence".to_string(), - version: "0.1.0".to_string(), - description: "PostgreSQL persistence plugin for session storage".to_string(), - installed: true, - }, - PluginInfo { - name: "web-scraper".to_string(), - version: "0.2.0".to_string(), - description: "Web scraping tool for content extraction".to_string(), - installed: false, - }, - PluginInfo { - name: "code-interpreter".to_string(), - version: "0.1.0".to_string(), - description: "Sandboxed code execution environment".to_string(), - installed: false, - }, - ]; - - let filtered: Vec<_> = if installed_only { - plugins.iter().filter(|p| p.installed).cloned().collect() - } else { - plugins - }; + let plugins = ctx.plugin_registry.list(); - if filtered.is_empty() { - println!(" No plugins found."); + if plugins.is_empty() { + println!(" No plugins registered."); + println!(); + println!(" Plugins can be registered programmatically via the SDK."); return Ok(()); } - let json = serde_json::to_value(&filtered)?; + let infos: Vec = plugins + .iter() + .map(|p| { + let metadata = p.metadata(); + PluginInfo { + name: p.name().to_string(), + version: metadata.version.clone(), + description: p.description().to_string(), + stages: metadata + .stages + .iter() + .map(|s| format!("{:?}", s)) + .collect::>() + .join(", "), + } + }) + .collect(); + + let json = serde_json::to_value(&infos)?; if let Some(arr) = json.as_array() { let table = Table::from_json_array(arr); println!("{}", table); @@ -70,5 +52,5 @@ struct PluginInfo { name: String, version: String, description: String, - installed: bool, + stages: String, } diff --git a/crates/mofa-cli/src/commands/plugin/uninstall.rs b/crates/mofa-cli/src/commands/plugin/uninstall.rs index f5eb7f98..6bba45c0 100644 --- a/crates/mofa-cli/src/commands/plugin/uninstall.rs +++ b/crates/mofa-cli/src/commands/plugin/uninstall.rs @@ -1,19 +1,45 @@ //! `mofa plugin uninstall` command implementation +use crate::context::CliContext; use colored::Colorize; +use dialoguer::Confirm; +use mofa_kernel::agent::plugins::PluginRegistry; /// Execute the `mofa plugin uninstall` command -pub fn run(name: &str, _force: bool) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, name: &str, force: bool) -> anyhow::Result<()> { + // Check if plugin exists + if !ctx.plugin_registry.contains(name) { + anyhow::bail!("Plugin '{}' not found in registry", name); + } + + if !force { + let confirmed = Confirm::new() + .with_prompt(format!("Uninstall plugin '{}'?", name)) + .default(false) + .interact()?; + + if !confirmed { + println!("{} Cancelled", "→".yellow()); + return Ok(()); + } + } + println!("{} Uninstalling plugin: {}", "→".green(), name.cyan()); - // TODO: Implement actual plugin uninstallation - // This would involve: - // 1. Checking if plugin is installed - // 2. Confirming uninstallation (unless --force) - // 3. Removing plugin files - // 4. Updating plugin registry + let removed = ctx + .plugin_registry + .unregister(name) + .map_err(|e| anyhow::anyhow!("Failed to unregister plugin: {}", e))?; - println!("{} Plugin '{}' uninstalled", "✓".green(), name); + if removed { + println!("{} Plugin '{}' uninstalled", "✓".green(), name); + } else { + println!( + "{} Plugin '{}' was not in the registry", + "!".yellow(), + name + ); + } Ok(()) } diff --git a/crates/mofa-cli/src/commands/session/delete.rs b/crates/mofa-cli/src/commands/session/delete.rs index b1b1dc0a..363e9d69 100644 --- a/crates/mofa-cli/src/commands/session/delete.rs +++ b/crates/mofa-cli/src/commands/session/delete.rs @@ -1,23 +1,40 @@ //! `mofa session delete` command implementation +use crate::context::CliContext; use colored::Colorize; +use dialoguer::Confirm; /// Execute the `mofa session delete` command -pub fn run(session_id: &str, force: bool) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, session_id: &str, force: bool) -> anyhow::Result<()> { if !force { - println!("{} Delete session: {}?", "→".yellow(), session_id.cyan()); - println!(" This action cannot be undone."); - println!(); - println!(" Use --force to skip confirmation."); - // TODO: Add actual confirmation prompt - return Ok(()); + let confirmed = Confirm::new() + .with_prompt(format!("Delete session '{}'? This cannot be undone", session_id)) + .default(false) + .interact()?; + + if !confirmed { + println!("{} Cancelled", "→".yellow()); + return Ok(()); + } } println!("{} Deleting session: {}", "→".green(), session_id.cyan()); - // TODO: Implement actual session deletion from persistence layer + let deleted = ctx + .session_manager + .delete(session_id) + .await + .map_err(|e| anyhow::anyhow!("Failed to delete session: {}", e))?; - println!("{} Session '{}' deleted", "✓".green(), session_id); + if deleted { + println!("{} Session '{}' deleted", "✓".green(), session_id); + } else { + println!( + "{} Session '{}' not found", + "!".yellow(), + session_id + ); + } Ok(()) } diff --git a/crates/mofa-cli/src/commands/session/export.rs b/crates/mofa-cli/src/commands/session/export.rs index 4fa8bc97..1e0404e4 100644 --- a/crates/mofa-cli/src/commands/session/export.rs +++ b/crates/mofa-cli/src/commands/session/export.rs @@ -1,37 +1,49 @@ //! `mofa session export` command implementation +use crate::context::CliContext; use colored::Colorize; use std::path::PathBuf; /// Execute the `mofa session export` command -pub fn run(session_id: &str, output: PathBuf, format: &str) -> anyhow::Result<()> { +pub async fn run( + ctx: &CliContext, + session_id: &str, + output: PathBuf, + format: &str, +) -> anyhow::Result<()> { println!("{} Exporting session: {}", "→".green(), session_id.cyan()); println!(" Format: {}", format.yellow()); println!(" Output: {}", output.display().to_string().cyan()); println!(); - // TODO: Implement actual session export from persistence layer + let session = ctx.session_manager.get_or_create(session_id).await; + + if session.is_empty() { + println!( + "{} Session '{}' has no messages to export", + "!".yellow(), + session_id + ); + return Ok(()); + } + + let session_data = serde_json::json!({ + "session_id": session.key, + "created_at": session.created_at.to_rfc3339(), + "updated_at": session.updated_at.to_rfc3339(), + "metadata": session.metadata, + "messages": session.messages.iter().map(|m| { + serde_json::json!({ + "role": m.role, + "content": m.content, + "timestamp": m.timestamp.to_rfc3339(), + }) + }).collect::>(), + }); let output_str = match format { - "json" => { - let content = serde_json::json!({ - "session_id": session_id, - "agent_id": "agent-001", - "created_at": "2024-01-15T10:30:00Z", - "messages": [ - {"role": "user", "content": "Hello!"}, - {"role": "assistant", "content": "Hi there! How can I help you?"} - ], - "status": "active" - }); - serde_json::to_string_pretty(&content)? - } - "yaml" => { - format!( - "session_id: {}\nagent_id: agent-001\ncreated_at: 2024-01-15T10:30:00Z\nmessages:\n - role: user\n content: Hello!\n - role: assistant\n content: Hi there! How can I help you?\nstatus: active\n", - session_id - ) - } + "json" => serde_json::to_string_pretty(&session_data)?, + "yaml" => serde_yaml::to_string(&session_data)?, _ => anyhow::bail!("Unsupported export format: {}", format), }; diff --git a/crates/mofa-cli/src/commands/session/list.rs b/crates/mofa-cli/src/commands/session/list.rs index 4b30083a..0d7cd9d3 100644 --- a/crates/mofa-cli/src/commands/session/list.rs +++ b/crates/mofa-cli/src/commands/session/list.rs @@ -1,11 +1,12 @@ //! `mofa session list` command implementation +use crate::context::CliContext; use crate::output::Table; use colored::Colorize; use serde::Serialize; /// Execute the `mofa session list` command -pub fn run(agent_id: Option<&str>, limit: Option) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, agent_id: Option<&str>, limit: Option) -> anyhow::Result<()> { println!("{} Listing sessions", "→".green()); if let Some(agent) = agent_id { @@ -18,39 +19,51 @@ pub fn run(agent_id: Option<&str>, limit: Option) -> anyhow::Result<()> { println!(); - // TODO: Implement actual session listing from persistence layer + let keys = ctx + .session_manager + .list() + .await + .map_err(|e| anyhow::anyhow!("Failed to list sessions: {}", e))?; - let sessions = vec![ - SessionInfo { - session_id: "sess-001".to_string(), - agent_id: "agent-001".to_string(), - created_at: "2024-01-15 10:30:00".to_string(), - message_count: 12, - status: "active".to_string(), - }, - SessionInfo { - session_id: "sess-002".to_string(), - agent_id: "agent-001".to_string(), - created_at: "2024-01-15 09:15:00".to_string(), - message_count: 8, - status: "active".to_string(), - }, - ]; + if keys.is_empty() { + println!(" No sessions found."); + return Ok(()); + } - let filtered: Vec<_> = if let Some(agent) = agent_id { - sessions - .iter() - .filter(|s| s.agent_id == agent) - .cloned() - .collect() - } else { - sessions - }; + let mut sessions = Vec::new(); + for key in &keys { + let session = ctx.session_manager.get_or_create(key).await; + + // Filter by agent_id if provided (check metadata or key prefix) + if let Some(agent) = agent_id { + let matches = session + .metadata + .get("agent_id") + .and_then(|v| v.as_str()) + .map(|v| v == agent) + .unwrap_or_else(|| session.key.contains(agent)); + if !matches { + continue; + } + } + sessions.push(SessionInfo { + session_id: session.key.clone(), + created_at: session.created_at.format("%Y-%m-%d %H:%M:%S").to_string(), + message_count: session.len(), + status: if session.is_empty() { + "empty".to_string() + } else { + "active".to_string() + }, + }); + } + + // Apply limit let limited: Vec<_> = if let Some(n) = limit { - filtered.into_iter().take(n).collect() + sessions.into_iter().take(n).collect() } else { - filtered + sessions }; if limited.is_empty() { @@ -70,7 +83,6 @@ pub fn run(agent_id: Option<&str>, limit: Option) -> anyhow::Result<()> { #[derive(Debug, Clone, Serialize)] struct SessionInfo { session_id: String, - agent_id: String, created_at: String, message_count: usize, status: String, diff --git a/crates/mofa-cli/src/commands/session/show.rs b/crates/mofa-cli/src/commands/session/show.rs index ef94f290..c6f83380 100644 --- a/crates/mofa-cli/src/commands/session/show.rs +++ b/crates/mofa-cli/src/commands/session/show.rs @@ -1,50 +1,89 @@ //! `mofa session show` command implementation +use crate::context::CliContext; use colored::Colorize; /// Execute the `mofa session show` command -pub fn run(session_id: &str, format: Option<&str>) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, session_id: &str, format: Option<&str>) -> anyhow::Result<()> { println!("{} Session details: {}", "→".green(), session_id.cyan()); println!(); - // TODO: Implement actual session retrieval from persistence layer - + let session = ctx.session_manager.get_or_create(session_id).await; let output_format = format.unwrap_or("text"); match output_format { "json" => { let json = serde_json::json!({ - "session_id": session_id, - "agent_id": "agent-001", - "created_at": "2024-01-15T10:30:00Z", - "messages": [ - {"role": "user", "content": "Hello!"}, - {"role": "assistant", "content": "Hi there! How can I help you?"} - ], - "status": "active" + "session_id": session.key, + "created_at": session.created_at.to_rfc3339(), + "updated_at": session.updated_at.to_rfc3339(), + "message_count": session.len(), + "metadata": session.metadata, + "messages": session.messages.iter().map(|m| { + serde_json::json!({ + "role": m.role, + "content": m.content, + "timestamp": m.timestamp.to_rfc3339(), + }) + }).collect::>(), }); println!("{}", serde_json::to_string_pretty(&json)?); } "yaml" => { - println!("session_id: {}", session_id); - println!("agent_id: agent-001"); - println!("created_at: 2024-01-15T10:30:00Z"); - println!("messages:"); - println!(" - role: user"); - println!(" content: Hello!"); - println!(" - role: assistant"); - println!(" content: Hi there! How can I help you?"); - println!("status: active"); + let yaml = serde_json::json!({ + "session_id": session.key, + "created_at": session.created_at.to_rfc3339(), + "updated_at": session.updated_at.to_rfc3339(), + "message_count": session.len(), + "metadata": session.metadata, + "messages": session.messages.iter().map(|m| { + serde_json::json!({ + "role": m.role, + "content": m.content, + "timestamp": m.timestamp.to_rfc3339(), + }) + }).collect::>(), + }); + println!("{}", serde_yaml::to_string(&yaml)?); } _ => { - println!(" Session ID: {}", session_id.cyan()); - println!(" Agent ID: {}", "agent-001".white()); - println!(" Created: {}", "2024-01-15 10:30:00".white()); - println!(" Status: {}", "active".green()); + println!(" Session ID: {}", session.key.cyan()); + println!( + " Created: {}", + session + .created_at + .format("%Y-%m-%d %H:%M:%S") + .to_string() + .white() + ); + println!( + " Updated: {}", + session + .updated_at + .format("%Y-%m-%d %H:%M:%S") + .to_string() + .white() + ); + println!(" Messages: {}", session.len()); + if !session.metadata.is_empty() { + println!(" Metadata: {:?}", session.metadata); + } println!(); - println!(" Messages:"); - println!(" User: Hello!"); - println!(" Assistant: Hi there! How can I help you?"); + + if session.is_empty() { + println!(" (no messages)"); + } else { + println!(" Messages:"); + for msg in &session.messages { + let role_display = match msg.role.as_str() { + "user" => "User".green(), + "assistant" => "Assistant".cyan(), + "system" => "System".yellow(), + other => other.white(), + }; + println!(" {}: {}", role_display, msg.content); + } + } } } diff --git a/crates/mofa-cli/src/commands/tool/info.rs b/crates/mofa-cli/src/commands/tool/info.rs index 39607748..682694fe 100644 --- a/crates/mofa-cli/src/commands/tool/info.rs +++ b/crates/mofa-cli/src/commands/tool/info.rs @@ -1,23 +1,89 @@ //! `mofa tool info` command implementation +use crate::context::CliContext; use colored::Colorize; +use mofa_kernel::agent::components::tool::ToolRegistry; /// Execute the `mofa tool info` command -pub fn run(name: &str) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, name: &str) -> anyhow::Result<()> { println!("{} Tool information: {}", "→".green(), name.cyan()); println!(); - // TODO: Implement actual tool info lookup + match ctx.tool_registry.get(name) { + Some(tool) => { + let metadata = tool.metadata(); + println!(" Name: {}", tool.name().cyan()); + println!(" Description: {}", tool.description().white()); + if let Some(category) = &metadata.category { + println!(" Category: {}", category.white()); + } + if !metadata.tags.is_empty() { + println!(" Tags: {}", metadata.tags.join(", ").white()); + } + println!( + " Dangerous: {}", + if metadata.is_dangerous { + "Yes".red() + } else { + "No".green() + } + ); + println!( + " Needs network: {}", + if metadata.requires_network { + "Yes".yellow() + } else { + "No".white() + } + ); + println!( + " Needs FS: {}", + if metadata.requires_filesystem { + "Yes".yellow() + } else { + "No".white() + } + ); + println!( + " Confirmation: {}", + if tool.requires_confirmation() { + "Required".yellow() + } else { + "Not required".white() + } + ); - println!(" Name: {}", name.cyan()); - println!(" Description: {}", "A helpful tool".white()); - println!(" Version: {}", "1.0.0".white()); - println!(" Enabled: {}", "Yes".green()); - println!( - " Parameters: {}", - "query (required), limit (optional)".white() - ); - println!(); + // Show parameter schema + let schema = tool.parameters_schema(); + if !schema.is_null() { + println!(); + println!(" Parameters:"); + println!( + "{}", + serde_json::to_string_pretty(&schema)? + .lines() + .map(|l| format!(" {}", l)) + .collect::>() + .join("\n") + ); + } + + // Show source + if let Some(source) = ctx.tool_registry.get_source(name) { + println!(); + println!(" Source: {:?}", source); + } + } + None => { + println!(" Tool '{}' not found in registry", name); + println!(); + println!( + " Use {} to see available tools.", + "mofa tool list".cyan() + ); + } + } + println!(); Ok(()) } diff --git a/crates/mofa-cli/src/commands/tool/list.rs b/crates/mofa-cli/src/commands/tool/list.rs index 669ddb10..fe895d75 100644 --- a/crates/mofa-cli/src/commands/tool/list.rs +++ b/crates/mofa-cli/src/commands/tool/list.rs @@ -1,58 +1,43 @@ //! `mofa tool list` command implementation +use crate::context::CliContext; use crate::output::Table; use colored::Colorize; +use mofa_kernel::agent::components::tool::ToolRegistry; use serde::Serialize; /// Execute the `mofa tool list` command -pub fn run(available: bool, enabled: bool) -> anyhow::Result<()> { +pub async fn run(ctx: &CliContext, _available: bool, _enabled: bool) -> anyhow::Result<()> { println!("{} Listing tools", "→".green()); - - if available { - println!(" Showing available tools"); - } else if enabled { - println!(" Showing enabled tools"); - } - println!(); - // TODO: Implement actual tool discovery from tool registry - - let tools = vec![ - ToolInfo { - name: "web-search".to_string(), - description: "Search the web for information".to_string(), - enabled: true, - }, - ToolInfo { - name: "calculator".to_string(), - description: "Perform mathematical calculations".to_string(), - enabled: true, - }, - ToolInfo { - name: "code-executor".to_string(), - description: "Execute code in a sandboxed environment".to_string(), - enabled: false, - }, - ToolInfo { - name: "file-operations".to_string(), - description: "Read, write, and manipulate files".to_string(), - enabled: false, - }, - ]; - - let filtered: Vec<_> = if enabled { - tools.iter().filter(|t| t.enabled).cloned().collect() - } else { - tools - }; + let descriptors = ctx.tool_registry.list(); - if filtered.is_empty() { - println!(" No tools found."); + if descriptors.is_empty() { + println!(" No tools registered."); + println!(); + println!(" Tools can be registered programmatically via the SDK."); return Ok(()); } - let json = serde_json::to_value(&filtered)?; + let tools: Vec = descriptors + .iter() + .map(|d| { + let source = ctx + .tool_registry + .get_source(&d.name) + .map(|s| format!("{:?}", s)) + .unwrap_or_else(|| "unknown".to_string()); + ToolInfo { + name: d.name.clone(), + description: d.description.clone(), + category: d.metadata.category.clone().unwrap_or_default(), + source, + } + }) + .collect(); + + let json = serde_json::to_value(&tools)?; if let Some(arr) = json.as_array() { let table = Table::from_json_array(arr); println!("{}", table); @@ -65,5 +50,6 @@ pub fn run(available: bool, enabled: bool) -> anyhow::Result<()> { struct ToolInfo { name: String, description: String, - enabled: bool, + category: String, + source: String, } diff --git a/crates/mofa-cli/src/context.rs b/crates/mofa-cli/src/context.rs new file mode 100644 index 00000000..c4cbcc85 --- /dev/null +++ b/crates/mofa-cli/src/context.rs @@ -0,0 +1,47 @@ +//! CLI context providing access to backend services + +use crate::utils::paths; +use mofa_foundation::agent::session::SessionManager; +use mofa_foundation::agent::tools::registry::ToolRegistry; +use mofa_runtime::agent::plugins::SimplePluginRegistry; +use mofa_runtime::agent::registry::AgentRegistry; +use std::path::PathBuf; +use std::sync::Arc; + +/// Shared context for CLI commands, holding references to backend services +pub struct CliContext { + /// Session manager with file-based persistence + pub session_manager: SessionManager, + /// In-memory agent registry + pub agent_registry: AgentRegistry, + /// In-memory plugin registry + pub plugin_registry: Arc, + /// In-memory tool registry + pub tool_registry: ToolRegistry, + /// Platform-specific data directory (~/.local/share/mofa or equivalent) + pub data_dir: PathBuf, + /// Platform-specific config directory (~/.config/mofa or equivalent) + pub config_dir: PathBuf, +} + +impl CliContext { + /// Initialize the CLI context with default backend services + pub async fn new() -> anyhow::Result { + let data_dir = paths::ensure_mofa_data_dir()?; + let config_dir = paths::ensure_mofa_config_dir()?; + + let sessions_dir = data_dir.join("sessions"); + let session_manager = SessionManager::with_jsonl(&sessions_dir) + .await + .map_err(|e| anyhow::anyhow!("Failed to initialize session manager: {}", e))?; + + Ok(Self { + session_manager, + agent_registry: AgentRegistry::new(), + plugin_registry: Arc::new(SimplePluginRegistry::new()), + tool_registry: ToolRegistry::new(), + data_dir, + config_dir, + }) + } +} diff --git a/crates/mofa-cli/src/main.rs b/crates/mofa-cli/src/main.rs index 2465b1df..a7660953 100644 --- a/crates/mofa-cli/src/main.rs +++ b/crates/mofa-cli/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod commands; mod config; +mod context; mod output; mod render; mod tui; @@ -12,6 +13,7 @@ mod widgets; use clap::Parser; use cli::Cli; use colored::Colorize; +use context::CliContext; fn main() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -23,20 +25,39 @@ fn main() -> anyhow::Result<()> { tracing_subscriber::fmt().with_env_filter("info").init(); } + let rt = tokio::runtime::Runtime::new()?; + // Launch TUI if requested or no command provided if cli.tui || cli.command.is_none() { // Run TUI mode - tokio::runtime::Runtime::new()?.block_on(tui::run())?; + rt.block_on(tui::run())?; Ok(()) } else { - // Run CLI command as usual - run_command(cli) + // Run CLI command + rt.block_on(run_command(cli)) } } -fn run_command(cli: Cli) -> anyhow::Result<()> { +async fn run_command(cli: Cli) -> anyhow::Result<()> { use cli::Commands; + // Initialize context for commands that need backend services + let needs_context = matches!( + &cli.command, + Some( + Commands::Agent(_) + | Commands::Plugin { .. } + | Commands::Session { .. } + | Commands::Tool { .. } + ) + ); + + let ctx = if needs_context { + Some(CliContext::new().await?) + } else { + None + }; + match cli.command { Some(Commands::New { name, @@ -89,33 +110,36 @@ fn run_command(cli: Cli) -> anyhow::Result<()> { } }, - Some(Commands::Agent(agent_cmd)) => match agent_cmd { - cli::AgentCommands::Create { - non_interactive, - config, - } => { - commands::agent::create::run(non_interactive, config)?; - } - cli::AgentCommands::Start { - agent_id, - config, - daemon, - } => { - commands::agent::start::run(&agent_id, config.as_deref(), daemon)?; - } - cli::AgentCommands::Stop { agent_id } => { - commands::agent::stop::run(&agent_id)?; - } - cli::AgentCommands::Restart { agent_id, config } => { - commands::agent::restart::run(&agent_id, config.as_deref())?; - } - cli::AgentCommands::Status { agent_id } => { - commands::agent::status::run(agent_id.as_deref())?; - } - cli::AgentCommands::List { running, all } => { - commands::agent::list::run(running, all)?; + Some(Commands::Agent(agent_cmd)) => { + let ctx = ctx.as_ref().unwrap(); + match agent_cmd { + cli::AgentCommands::Create { + non_interactive, + config, + } => { + commands::agent::create::run(non_interactive, config)?; + } + cli::AgentCommands::Start { + agent_id, + config, + daemon, + } => { + commands::agent::start::run(ctx, &agent_id, config.as_deref(), daemon).await?; + } + cli::AgentCommands::Stop { agent_id } => { + commands::agent::stop::run(ctx, &agent_id).await?; + } + cli::AgentCommands::Restart { agent_id, config } => { + commands::agent::restart::run(ctx, &agent_id, config.as_deref()).await?; + } + cli::AgentCommands::Status { agent_id } => { + commands::agent::status::run(ctx, agent_id.as_deref()).await?; + } + cli::AgentCommands::List { running, all } => { + commands::agent::list::run(ctx, running, all).await?; + } } - }, + } Some(Commands::Config { action }) => match action { cli::ConfigCommands::Value(value_cmd) => match value_cmd { @@ -140,51 +164,63 @@ fn run_command(cli: Cli) -> anyhow::Result<()> { } }, - Some(Commands::Plugin { action }) => match action { - cli::PluginCommands::List { - installed, - available, - } => { - commands::plugin::list::run(installed, available)?; - } - cli::PluginCommands::Info { name } => { - commands::plugin::info::run(&name)?; - } - cli::PluginCommands::Uninstall { name, force } => { - commands::plugin::uninstall::run(&name, force)?; + Some(Commands::Plugin { action }) => { + let ctx = ctx.as_ref().unwrap(); + match action { + cli::PluginCommands::List { + installed, + available, + } => { + commands::plugin::list::run(ctx, installed, available).await?; + } + cli::PluginCommands::Info { name } => { + commands::plugin::info::run(ctx, &name).await?; + } + cli::PluginCommands::Uninstall { name, force } => { + commands::plugin::uninstall::run(ctx, &name, force).await?; + } } - }, + } - Some(Commands::Session { action }) => match action { - cli::SessionCommands::List { agent, limit } => { - commands::session::list::run(agent.as_deref(), limit)?; - } - cli::SessionCommands::Show { session_id, format } => { - commands::session::show::run( - &session_id, - format.map(|f| f.to_string()).as_deref(), - )?; - } - cli::SessionCommands::Delete { session_id, force } => { - commands::session::delete::run(&session_id, force)?; - } - cli::SessionCommands::Export { - session_id, - output, - format, - } => { - commands::session::export::run(&session_id, output, &format.to_string())?; + Some(Commands::Session { action }) => { + let ctx = ctx.as_ref().unwrap(); + match action { + cli::SessionCommands::List { agent, limit } => { + commands::session::list::run(ctx, agent.as_deref(), limit).await?; + } + cli::SessionCommands::Show { session_id, format } => { + commands::session::show::run( + ctx, + &session_id, + format.map(|f| f.to_string()).as_deref(), + ) + .await?; + } + cli::SessionCommands::Delete { session_id, force } => { + commands::session::delete::run(ctx, &session_id, force).await?; + } + cli::SessionCommands::Export { + session_id, + output, + format, + } => { + commands::session::export::run(ctx, &session_id, output, &format.to_string()) + .await?; + } } - }, + } - Some(Commands::Tool { action }) => match action { - cli::ToolCommands::List { available, enabled } => { - commands::tool::list::run(available, enabled)?; - } - cli::ToolCommands::Info { name } => { - commands::tool::info::run(&name)?; + Some(Commands::Tool { action }) => { + let ctx = ctx.as_ref().unwrap(); + match action { + cli::ToolCommands::List { available, enabled } => { + commands::tool::list::run(ctx, available, enabled).await?; + } + cli::ToolCommands::Info { name } => { + commands::tool::info::run(ctx, &name).await?; + } } - }, + } None => { // Should have been handled by TUI check above