mod commands; mod messages; mod engine; mod state; mod agent_manager; mod tool_registry; use clap::{Parser, ValueEnum}; use color_eyre::eyre::{Result, eyre}; use config_agent::load_settings; use hooks::{HookEvent, HookManager, HookResult}; use llm_core::{AuthMethod, ChatOptions, LlmProvider, ProviderType}; use llm_anthropic::AnthropicClient; use llm_ollama::OllamaClient; use llm_openai::OpenAIClient; use permissions::{PermissionDecision, Tool}; use plugins::PluginManager; use serde::Serialize; use std::io::Write; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; pub use commands::{BuiltinCommands, CommandResult}; #[derive(Debug, Clone, Copy, ValueEnum)] enum OutputFormat { Text, Json, StreamJson, } #[derive(Serialize)] struct SessionOutput { session_id: String, messages: Vec, stats: Stats, #[serde(skip_serializing_if = "Option::is_none")] result: Option, #[serde(skip_serializing_if = "Option::is_none")] tool: Option, } #[derive(Serialize)] struct Stats { total_tokens: u64, #[serde(skip_serializing_if = "Option::is_none")] prompt_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] completion_tokens: Option, duration_ms: u64, } #[derive(Serialize)] struct StreamEvent { #[serde(rename = "type")] event_type: String, #[serde(skip_serializing_if = "Option::is_none")] session_id: Option, #[serde(skip_serializing_if = "Option::is_none")] content: Option, #[serde(skip_serializing_if = "Option::is_none")] stats: Option, } /// Application context shared across the session pub struct AppContext { pub plugin_manager: PluginManager, pub config: config_agent::Settings, } impl AppContext { pub fn new() -> Result { let config = load_settings(None).unwrap_or_default(); let mut plugin_manager = PluginManager::new(); // Non-fatal: just log warnings, don't fail startup if let Err(e) = plugin_manager.load_all() { eprintln!("Warning: Failed to load some plugins: {}", e); } Ok(Self { plugin_manager, config, }) } /// Print loaded plugins and available commands pub fn print_plugin_info(&self) { let plugins = self.plugin_manager.plugins(); if !plugins.is_empty() { println!("\nLoaded {} plugin(s):", plugins.len()); for plugin in plugins { println!(" - {} v{}", plugin.manifest.name, plugin.manifest.version); if let Some(desc) = &plugin.manifest.description { println!(" {}", desc); } } } let commands = self.plugin_manager.all_commands(); if !commands.is_empty() { println!("\nAvailable plugin commands:"); for (name, _path) in &commands { println!(" /{}", name); } } } } fn generate_session_id() -> String { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(); format!("session-{}", timestamp) } /// Create an LLM provider based on settings and CLI arguments fn create_provider( settings: &config_agent::Settings, model_override: Option<&str>, api_key_override: Option<&str>, ollama_url_override: Option<&str>, ) -> Result<(Arc, String)> { // Determine which provider to use let provider_type = settings.get_provider().unwrap_or(ProviderType::Ollama); // Get or create auth manager let auth_manager = auth_manager::AuthManager::new() .map_err(|e| eyre!("Failed to initialize auth manager: {}", e))?; // Get authentication for this provider let auth = auth_manager.get_auth(provider_type); // Determine the model to use let model = model_override .map(|s| s.to_string()) .unwrap_or_else(|| settings.get_effective_model().to_string()); match provider_type { ProviderType::Ollama => { // Handle Ollama Cloud vs local let api_key = api_key_override .map(|s| s.to_string()) .or_else(|| settings.api_key.clone()); let use_cloud = model.ends_with("-cloud") && api_key.is_some(); let client = if use_cloud { OllamaClient::with_cloud().with_api_key(api_key.unwrap()) } else { let base_url = ollama_url_override .map(|s| s.to_string()) .unwrap_or_else(|| settings.ollama_url.clone()); let mut client = OllamaClient::new(base_url); if let Some(key) = api_key { client = client.with_api_key(key); } client }; Ok((Arc::new(client) as Arc, model)) } ProviderType::Anthropic => { // Try CLI override, then auth manager, then settings let auth_method = api_key_override .map(|k| AuthMethod::ApiKey(k.to_string())) .or_else(|| auth.ok()) .or_else(|| settings.anthropic_api_key.clone().map(AuthMethod::ApiKey)) .ok_or_else(|| eyre!( "Anthropic requires authentication. Run 'owlen login anthropic' or set ANTHROPIC_API_KEY" ))?; let client = AnthropicClient::with_auth(auth_method).with_model(&model); Ok((Arc::new(client) as Arc, model)) } ProviderType::OpenAI => { // Try CLI override, then auth manager, then settings let auth_method = api_key_override .map(|k| AuthMethod::ApiKey(k.to_string())) .or_else(|| auth.ok()) .or_else(|| settings.openai_api_key.clone().map(AuthMethod::ApiKey)) .ok_or_else(|| eyre!( "OpenAI requires authentication. Run 'owlen login openai' or set OPENAI_API_KEY" ))?; let client = OpenAIClient::with_auth(auth_method).with_model(&model); Ok((Arc::new(client) as Arc, model)) } } } fn output_tool_result( format: OutputFormat, tool: &str, result: serde_json::Value, session_id: &str, ) -> Result<()> { match format { OutputFormat::Text => { // For text, just print the result as-is if let Some(s) = result.as_str() { println!("{}", s); } else { println!("{}", serde_json::to_string_pretty(&result)?); } } OutputFormat::Json => { let output = SessionOutput { session_id: session_id.to_string(), messages: vec![], stats: Stats { total_tokens: 0, prompt_tokens: None, completion_tokens: None, duration_ms: 0, }, result: Some(result), tool: Some(tool.to_string()), }; println!("{}", serde_json::to_string(&output)?); } OutputFormat::StreamJson => { // For stream-json, emit session_start, result, and session_end let session_start = StreamEvent { event_type: "session_start".to_string(), session_id: Some(session_id.to_string()), content: None, stats: None, }; println!("{}", serde_json::to_string(&session_start)?); let result_event = StreamEvent { event_type: "tool_result".to_string(), session_id: None, content: Some(serde_json::to_string(&result)?), stats: None, }; println!("{}", serde_json::to_string(&result_event)?); let session_end = StreamEvent { event_type: "session_end".to_string(), session_id: None, content: None, stats: Some(Stats { total_tokens: 0, prompt_tokens: None, completion_tokens: None, duration_ms: 0, }), }; println!("{}", serde_json::to_string(&session_end)?); } } Ok(()) } #[derive(clap::Subcommand, Debug)] enum Cmd { Read { path: String }, Glob { pattern: String }, Grep { root: String, pattern: String }, Write { path: String, content: String }, Edit { path: String, old_string: String, new_string: String }, Bash { command: String, #[arg(long)] timeout: Option }, Slash { command_name: String, args: Vec }, /// Authenticate with an LLM provider (anthropic, openai) Login { /// Provider to authenticate with (anthropic, openai) provider: String, }, /// Remove stored credentials for a provider Logout { /// Provider to log out from (anthropic, openai, ollama) provider: String, }, /// Show authentication status for all providers Auth, } #[derive(Parser, Debug)] #[command(name = "code", version)] struct Args { #[arg(long)] ollama_url: Option, #[arg(long)] model: Option, #[arg(long)] api_key: Option, #[arg(long)] print: bool, /// Override the permission mode (plan, acceptEdits, code) #[arg(long)] mode: Option, /// Output format (text, json, stream-json) #[arg(long, value_enum, default_value = "text")] output_format: OutputFormat, /// Disable TUI and use legacy text-based REPL #[arg(long)] no_tui: bool, #[arg()] prompt: Vec, #[command(subcommand)] cmd: Option, } #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; let args = Args::parse(); // Initialize application context with plugins let app_context = AppContext::new()?; let mut settings = app_context.config.clone(); // Override mode if specified via CLI if let Some(mode) = args.mode { settings.mode = mode; } // Create permission manager from settings let perms = settings.create_permission_manager(); // Create hook manager let mut hook_mgr = HookManager::new("."); // Register plugin hooks for plugin in app_context.plugin_manager.plugins() { if let Ok(Some(hooks_config)) = plugin.load_hooks_config() { for (event, command, pattern, timeout) in plugin.register_hooks_with_manager(&hooks_config) { hook_mgr.register_hook(event, command, pattern, timeout); } } } // Generate session ID let session_id = generate_session_id(); let output_format = args.output_format; if let Some(cmd) = args.cmd { match cmd { Cmd::Read { path } => { // Check permission match perms.check(Tool::Read, None) { PermissionDecision::Allow => { // Check PreToolUse hook let event = HookEvent::PreToolUse { tool: "Read".to_string(), args: serde_json::json!({"path": &path}), }; match hook_mgr.execute(&event, Some(5000)).await? { HookResult::Deny => { return Err(eyre!("Hook denied Read operation")); } HookResult::Allow => {} } let s = tools_fs::read_file(&path)?; output_tool_result(output_format, "Read", serde_json::json!(s), &session_id)?; return Ok(()); } PermissionDecision::Ask => { return Err(eyre!( "Permission denied: Read operation requires approval. Use --mode code to allow." )); } PermissionDecision::Deny => { return Err(eyre!("Permission denied: Read operation is blocked.")); } } } Cmd::Glob { pattern } => { // Check permission match perms.check(Tool::Glob, None) { PermissionDecision::Allow => { // Check PreToolUse hook let event = HookEvent::PreToolUse { tool: "Glob".to_string(), args: serde_json::json!({"pattern": &pattern}), }; match hook_mgr.execute(&event, Some(5000)).await? { HookResult::Deny => { return Err(eyre!("Hook denied Glob operation")); } HookResult::Allow => {} } for p in tools_fs::glob_list(&pattern)? { println!("{}", p); } return Ok(()); } PermissionDecision::Ask => { return Err(eyre!( "Permission denied: Glob operation requires approval. Use --mode code to allow." )); } PermissionDecision::Deny => { return Err(eyre!("Permission denied: Glob operation is blocked.")); } } } Cmd::Grep { root, pattern } => { // Check permission match perms.check(Tool::Grep, None) { PermissionDecision::Allow => { // Check PreToolUse hook let event = HookEvent::PreToolUse { tool: "Grep".to_string(), args: serde_json::json!({"root": &root, "pattern": &pattern}), }; match hook_mgr.execute(&event, Some(5000)).await? { HookResult::Deny => { return Err(eyre!("Hook denied Grep operation")); } HookResult::Allow => {} } for (path, line_number, text) in tools_fs::grep(&root, &pattern)? { println!("{path}:{line_number}:{text}") } return Ok(()); } PermissionDecision::Ask => { return Err(eyre!( "Permission denied: Grep operation requires approval. Use --mode code to allow." )); } PermissionDecision::Deny => { return Err(eyre!("Permission denied: Grep operation is blocked.")); } } } Cmd::Write { path, content } => { // Check permission match perms.check(Tool::Write, None) { PermissionDecision::Allow => { // Check PreToolUse hook let event = HookEvent::PreToolUse { tool: "Write".to_string(), args: serde_json::json!({"path": &path, "content": &content}), }; match hook_mgr.execute(&event, Some(5000)).await? { HookResult::Deny => { return Err(eyre!("Hook denied Write operation")); } HookResult::Allow => {} } tools_fs::write_file(&path, &content)?; println!("File written: {}", path); return Ok(()); } PermissionDecision::Ask => { return Err(eyre!( "Permission denied: Write operation requires approval. Use --mode acceptEdits or --mode code to allow." )); } PermissionDecision::Deny => { return Err(eyre!("Permission denied: Write operation is blocked.")); } } } Cmd::Edit { path, old_string, new_string } => { // Check permission match perms.check(Tool::Edit, None) { PermissionDecision::Allow => { // Check PreToolUse hook let event = HookEvent::PreToolUse { tool: "Edit".to_string(), args: serde_json::json!({"path": &path, "old_string": &old_string, "new_string": &new_string}), }; match hook_mgr.execute(&event, Some(5000)).await? { HookResult::Deny => { return Err(eyre!("Hook denied Edit operation")); } HookResult::Allow => {} } tools_fs::edit_file(&path, &old_string, &new_string)?; println!("File edited: {}", path); return Ok(()); } PermissionDecision::Ask => { return Err(eyre!( "Permission denied: Edit operation requires approval. Use --mode acceptEdits or --mode code to allow." )); } PermissionDecision::Deny => { return Err(eyre!("Permission denied: Edit operation is blocked.")); } } } Cmd::Bash { command, timeout } => { // Check permission with command context for pattern matching match perms.check(Tool::Bash, Some(&command)) { PermissionDecision::Allow => { // Check PreToolUse hook let event = HookEvent::PreToolUse { tool: "Bash".to_string(), args: serde_json::json!({"command": &command, "timeout": timeout}), }; match hook_mgr.execute(&event, Some(5000)).await? { HookResult::Deny => { return Err(eyre!("Hook denied Bash operation")); } HookResult::Allow => {} } let mut session = tools_bash::BashSession::new().await?; let output = session.execute(&command, timeout).await?; // Print stdout if !output.stdout.is_empty() { print!("{}", output.stdout); } // Print stderr to stderr if !output.stderr.is_empty() { eprint!("{}", output.stderr); } session.close().await?; // Exit with same code as command if !output.success { std::process::exit(output.exit_code); } return Ok(()); } PermissionDecision::Ask => { return Err(eyre!( "Permission denied: Bash operation requires approval. Use --mode code to allow." )); } PermissionDecision::Deny => { return Err(eyre!("Permission denied: Bash operation is blocked.")); } } } Cmd::Slash { command_name, args } => { // Check permission match perms.check(Tool::SlashCommand, None) { PermissionDecision::Allow => { // Check PreToolUse hook let event = HookEvent::PreToolUse { tool: "SlashCommand".to_string(), args: serde_json::json!({"command_name": &command_name, "args": &args}), }; match hook_mgr.execute(&event, Some(5000)).await? { HookResult::Deny => { return Err(eyre!("Hook denied SlashCommand operation")); } HookResult::Allow => {} } // Look for command file in .owlen/commands/ first let local_command_path = format!(".owlen/commands/{}.md", command_name); // Try local commands first, then plugin commands let content = if let Ok(c) = tools_fs::read_file(&local_command_path) { c } else if let Some(plugin_path) = app_context.plugin_manager.all_commands().get(&command_name) { // Found in plugins tools_fs::read_file(&plugin_path.to_string_lossy())? } else { return Err(eyre!( "Slash command '{}' not found in .owlen/commands/ or plugins", command_name )); }; // Parse with arguments let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); let slash_cmd = tools_slash::parse_slash_command(&content, &args_refs)?; // Resolve file references let resolved_body = slash_cmd.resolve_file_refs()?; // Print the resolved command body println!("{}", resolved_body); return Ok(()); } PermissionDecision::Ask => { return Err(eyre!( "Permission denied: Slash command requires approval. Use --mode code to allow." )); } PermissionDecision::Deny => { return Err(eyre!("Permission denied: Slash command is blocked.")); } } } Cmd::Login { provider } => { let provider_type = provider.parse::().ok() .ok_or_else(|| eyre!( "Unknown provider: {}. Supported: anthropic, openai, ollama", provider ))?; let auth_manager = auth_manager::AuthManager::new() .map_err(|e| eyre!("Failed to initialize auth manager: {}", e))?; // Check if OAuth is available for this provider let oauth_available = match provider_type { llm_core::ProviderType::Anthropic => llm_anthropic::AnthropicAuth::is_oauth_available(), llm_core::ProviderType::OpenAI => llm_openai::OpenAIAuth::is_oauth_available(), llm_core::ProviderType::Ollama => false, }; if oauth_available { // Use OAuth device flow println!("Starting OAuth login for {}...", provider); auth_manager.login(provider_type).await .map_err(|e| eyre!("Login failed: {}", e))?; println!("\nāœ… Successfully logged in to {}!", provider); } else { // OAuth not available, prompt for API key println!("\nšŸ” {} Login\n", provider.to_uppercase()); // Show provider-specific instructions let (console_url, local_note) = match provider_type { llm_core::ProviderType::Anthropic => ( "https://console.anthropic.com/settings/keys", None, ), llm_core::ProviderType::OpenAI => ( "https://platform.openai.com/api-keys", None, ), llm_core::ProviderType::Ollama => ( "https://ollama.com/settings/keys", Some("Note: Local Ollama doesn't require authentication.\nThis is for Ollama Cloud access.\n"), ), }; if let Some(note) = local_note { println!("{}", note); } println!("Get your API key from: {}\n", console_url); // Offer to open browser print!("Open browser to get API key? [Y/n]: "); std::io::Write::flush(&mut std::io::stdout())?; let mut open_browser = String::new(); std::io::stdin().read_line(&mut open_browser)?; let open_browser = open_browser.trim().to_lowercase(); if open_browser.is_empty() || open_browser == "y" || open_browser == "yes" { if let Err(e) = open::that(console_url) { println!("Could not open browser: {}", e); } else { println!("Opening browser...\n"); } } println!("Enter your API key below (input is hidden):\n"); // Prompt for API key print!("API Key: "); std::io::Write::flush(&mut std::io::stdout())?; // Read API key (hide input) let api_key = rpassword::read_password() .map_err(|e| eyre!("Failed to read API key: {}", e))?; let api_key = api_key.trim(); if api_key.is_empty() { return Err(eyre!("API key cannot be empty. Login cancelled.")); } // Validate API key format (basic check) match provider_type { llm_core::ProviderType::Anthropic => { if !api_key.starts_with("sk-ant-") { println!("\nāš ļø Warning: Anthropic API keys typically start with 'sk-ant-'"); } } llm_core::ProviderType::OpenAI => { if !api_key.starts_with("sk-") { println!("\nāš ļø Warning: OpenAI API keys typically start with 'sk-'"); } } llm_core::ProviderType::Ollama => { // Ollama Cloud API keys - no specific format validation } } // Store the API key auth_manager.store_api_key(provider_type, api_key) .map_err(|e| eyre!("Failed to store API key: {}", e))?; println!("\nāœ… API key stored successfully!"); } println!("Credentials stored in: {}", auth_manager.storage_name()); return Ok(()); } Cmd::Logout { provider } => { let provider_type = provider.parse::().ok() .ok_or_else(|| eyre!( "Unknown provider: {}. Supported: anthropic, openai, ollama", provider ))?; let auth_manager = auth_manager::AuthManager::new() .map_err(|e| eyre!("Failed to initialize auth manager: {}", e))?; auth_manager.logout(provider_type) .map_err(|e| eyre!("Logout failed: {}", e))?; println!("Successfully logged out from {}.", provider); return Ok(()); } Cmd::Auth => { let auth_manager = auth_manager::AuthManager::new() .map_err(|e| eyre!("Failed to initialize auth manager: {}", e))?; println!("\nšŸ” Authentication Status\n"); println!("Storage: {}\n", auth_manager.storage_name()); println!("{:<12} {:<15} {:<30}", "Provider", "Status", "Details"); println!("{}", "-".repeat(57)); for status in auth_manager.status() { let status_icon = if status.authenticated { "āœ…" } else { "āŒ" }; let status_text = if status.authenticated { "Authenticated" } else { "Not authenticated" }; let details = status.message.unwrap_or_else(|| "-".to_string()); println!( "{:<12} {} {:<12} {}", status.provider, status_icon, status_text, details ); } println!("\nTo authenticate: owlen login "); println!("To logout: owlen logout "); return Ok(()); } } } // Create provider based on settings and CLI args let (client, model) = create_provider( &settings, args.model.as_deref(), args.api_key.as_deref(), args.ollama_url.as_deref(), )?; let opts = ChatOptions::new(&model); // Initialize async engine infrastructure let (tx, rx) = tokio::sync::mpsc::channel::(100); // Keep tx for future use (will be passed to UI/REPL) let _tx = tx.clone(); // Create shared state let state = Arc::new(tokio::sync::Mutex::new(crate::state::AppState::new())); // Spawn the Engine Loop let client_clone = client.clone(); let tx_clone = tx.clone(); let state_clone = state.clone(); tokio::spawn(async move { engine::run_engine_loop(rx, tx_clone, client_clone, state_clone).await; }); // Check if interactive mode (no prompt provided) if args.prompt.is_empty() { // Use TUI mode unless --no-tui flag is set or not a TTY if !args.no_tui && atty::is(atty::Stream::Stdout) { // Start background token refresh for long-running TUI sessions let auth_manager = Arc::new( auth_manager::AuthManager::new() .map_err(|e| eyre!("Failed to initialize auth manager: {}", e))? ); let _token_refresher = auth_manager.clone().start_background_refresh(); // Launch TUI with multi-provider support // Note: For now, TUI doesn't use plugin manager directly // In the future, we'll integrate plugin commands into TUI return ui::run_with_providers(auth_manager, perms, settings).await; } // Legacy text-based REPL println!("šŸ¤– Owlen Interactive Mode"); println!("Model: {}", opts.model); println!("Mode: {:?}", settings.mode); // Show loaded plugins let plugins = app_context.plugin_manager.plugins(); if !plugins.is_empty() { println!("Plugins: {} loaded", plugins.len()); } println!("Type your message or /help for commands. Press Ctrl+C to exit.\n"); use std::io::{stdin, BufRead}; let stdin = stdin(); let mut lines = stdin.lock().lines(); let mut stats = agent_core::SessionStats::new(); let mut history = agent_core::SessionHistory::new(); let mut checkpoint_mgr = agent_core::CheckpointManager::new( std::path::PathBuf::from(".owlen/checkpoints") ); loop { print!("> "); std::io::stdout().flush().ok(); if let Some(Ok(line)) = lines.next() { let input = line.trim(); if input.is_empty() { continue; } // Handle slash commands if input.starts_with('/') { match input { "/help" => { println!("\nšŸ“– Available Commands:"); println!(" /help - Show this help message"); println!(" /status - Show session status"); println!(" /permissions - Show permission settings"); println!(" /cost - Show token usage and timing"); println!(" /history - Show conversation history"); println!(" /checkpoint - Save current session state"); println!(" /checkpoints - List all saved checkpoints"); println!(" /rewind - Restore session from checkpoint"); println!(" /clear - Clear conversation history"); println!(" /plugins - Show loaded plugins and commands"); println!(" /exit - Exit interactive mode"); // Show plugin commands if any are loaded let plugin_commands = app_context.plugin_manager.all_commands(); if !plugin_commands.is_empty() { println!("\nšŸ“¦ Plugin Commands:"); for (name, _path) in &plugin_commands { println!(" /{}", name); } } } "/status" => { println!("\nšŸ“Š Session Status:"); println!(" Model: {}", opts.model); println!(" Mode: {:?}", settings.mode); println!(" Messages: {}", stats.total_messages); println!(" Tools: {} calls", stats.total_tool_calls); let elapsed = stats.start_time.elapsed().unwrap_or_default(); println!(" Uptime: {}", agent_core::SessionStats::format_duration(elapsed)); } "/permissions" => { println!("\nšŸ”’ Permission Settings:"); println!(" Mode: {:?}", perms.mode()); println!("\n Read-only tools: Read, Grep, Glob, NotebookRead"); match perms.mode() { permissions::Mode::Plan => { println!(" āœ… Allowed (plan mode)"); println!("\n Write tools: Write, Edit, NotebookEdit"); println!(" ā“ Ask permission"); println!("\n System tools: Bash"); println!(" ā“ Ask permission"); } permissions::Mode::AcceptEdits => { println!(" āœ… Allowed"); println!("\n Write tools: Write, Edit, NotebookEdit"); println!(" āœ… Allowed (acceptEdits mode)"); println!("\n System tools: Bash"); println!(" ā“ Ask permission"); } permissions::Mode::Code => { println!(" āœ… Allowed"); println!("\n Write tools: Write, Edit, NotebookEdit"); println!(" āœ… Allowed (code mode)"); println!("\n System tools: Bash"); println!(" āœ… Allowed (code mode)"); } } } "/cost" => { println!("\nšŸ’° Token Usage & Timing:"); println!(" Est. Tokens: ~{}", stats.estimated_tokens); println!(" Total Time: {}", agent_core::SessionStats::format_duration(stats.total_duration)); if stats.total_messages > 0 { let avg_time = stats.total_duration / stats.total_messages as u32; println!(" Avg/Message: {}", agent_core::SessionStats::format_duration(avg_time)); } println!("\n Note: Ollama is free - no cost incurred!"); } "/history" => { println!("\nšŸ“œ Conversation History:"); if history.user_prompts.is_empty() { println!(" (No messages yet)"); } else { for (i, (user, assistant)) in history.user_prompts.iter() .zip(history.assistant_responses.iter()).enumerate() { println!("\n [{}] User: {}", i + 1, user); println!(" Assistant: {}...", assistant.chars().take(100).collect::()); } } if !history.tool_calls.is_empty() { println!("\n Tool Calls: {}", history.tool_calls.len()); } } "/checkpoint" => { let checkpoint_id = format!("checkpoint-{}", SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() ); match checkpoint_mgr.save_checkpoint( checkpoint_id.clone(), stats.clone(), &history, ) { Ok(checkpoint) => { println!("\nšŸ’¾ Checkpoint saved: {}", checkpoint_id); if !checkpoint.file_diffs.is_empty() { println!(" Files tracked: {}", checkpoint.file_diffs.len()); } } Err(e) => { eprintln!("\nāŒ Failed to save checkpoint: {}", e); } } } "/checkpoints" => { match checkpoint_mgr.list_checkpoints() { Ok(checkpoints) => { if checkpoints.is_empty() { println!("\nšŸ“‹ No checkpoints saved yet"); } else { println!("\nšŸ“‹ Saved Checkpoints:"); for (i, cp_id) in checkpoints.iter().enumerate() { println!(" [{}] {}", i + 1, cp_id); } println!("\n Use /rewind to restore"); } } Err(e) => { eprintln!("\nāŒ Failed to list checkpoints: {}", e); } } } "/clear" => { history.clear(); stats = agent_core::SessionStats::new(); println!("\nšŸ—‘ļø Session history cleared!"); } "/plugins" => { let plugins = app_context.plugin_manager.plugins(); if plugins.is_empty() { println!("\nšŸ“¦ No plugins loaded"); println!(" Place plugins in:"); println!(" - ~/.config/owlen/plugins (user plugins)"); println!(" - .owlen/plugins (project plugins)"); } else { println!("\nšŸ“¦ Loaded Plugins:"); for plugin in plugins { println!("\n {} v{}", plugin.manifest.name, plugin.manifest.version); if let Some(desc) = &plugin.manifest.description { println!(" {}", desc); } if let Some(author) = &plugin.manifest.author { println!(" Author: {}", author); } let commands = plugin.all_command_names(); if !commands.is_empty() { println!(" Commands: {}", commands.join(", ")); } let agents = plugin.all_agent_names(); if !agents.is_empty() { println!(" Agents: {}", agents.join(", ")); } let skills = plugin.all_skill_names(); if !skills.is_empty() { println!(" Skills: {}", skills.join(", ")); } } } } "/exit" => { println!("\nšŸ‘‹ Goodbye!"); break; } cmd if cmd.starts_with("/rewind ") => { let checkpoint_id = cmd.strip_prefix("/rewind ").unwrap().trim(); match checkpoint_mgr.rewind_to(checkpoint_id) { Ok(restored_files) => { println!("\nāŖ Rewound to checkpoint: {}", checkpoint_id); if !restored_files.is_empty() { println!(" Restored files:"); for file in restored_files { println!(" - {}", file.display()); } } // Load the checkpoint to restore history and stats if let Ok(checkpoint) = checkpoint_mgr.load_checkpoint(checkpoint_id) { stats = checkpoint.stats; history.user_prompts = checkpoint.user_prompts; history.assistant_responses = checkpoint.assistant_responses; history.tool_calls = checkpoint.tool_calls; println!(" Session state restored"); } } Err(e) => { eprintln!("\nāŒ Failed to rewind: {}", e); } } } _ => { println!("\nāŒ Unknown command: {}", input); println!(" Type /help for available commands"); } } continue; } // Regular message - run through agent loop history.add_user_message(input.to_string()); let start = SystemTime::now(); let ctx = agent_core::ToolContext::new(); match agent_core::run_agent_loop(&client, input, &opts, &perms, &ctx).await { Ok(response) => { println!("\n{}", response); history.add_assistant_message(response.clone()); // Update stats let duration = start.elapsed().unwrap_or_default(); let tokens = (input.len() + response.len()) / 4; // Rough estimate stats.record_message(tokens, duration); } Err(e) => { eprintln!("\nāŒ Error: {}", e); } } } else { break; } } return Ok(()); } // Non-interactive mode - process single prompt let prompt = args.prompt.join(" "); let start_time = SystemTime::now(); // Handle different output formats let ctx = agent_core::ToolContext::new(); match output_format { OutputFormat::Text => { // Text format: Use agent orchestrator with tool calling let response = agent_core::run_agent_loop(&client, &prompt, &opts, &perms, &ctx).await?; println!("{}", response); } OutputFormat::Json => { // JSON format: Use agent loop and output as JSON let response = agent_core::run_agent_loop(&client, &prompt, &opts, &perms, &ctx).await?; let duration_ms = start_time.elapsed().unwrap().as_millis() as u64; let estimated_tokens = ((prompt.len() + response.len()) / 4) as u64; let output = SessionOutput { session_id, messages: vec![ serde_json::json!({"role": "user", "content": prompt}), serde_json::json!({"role": "assistant", "content": response}), ], stats: Stats { total_tokens: estimated_tokens, prompt_tokens: Some((prompt.len() / 4) as u64), completion_tokens: Some((response.len() / 4) as u64), duration_ms, }, result: None, tool: None, }; println!("{}", serde_json::to_string(&output)?); } OutputFormat::StreamJson => { // Stream-JSON format: emit session_start, response, and session_end let session_start = StreamEvent { event_type: "session_start".to_string(), session_id: Some(session_id.clone()), content: None, stats: None, }; println!("{}", serde_json::to_string(&session_start)?); let response = agent_core::run_agent_loop(&client, &prompt, &opts, &perms, &ctx).await?; let chunk_event = StreamEvent { event_type: "chunk".to_string(), session_id: None, content: Some(response.clone()), stats: None, }; println!("{}", serde_json::to_string(&chunk_event)?); let duration_ms = start_time.elapsed().unwrap().as_millis() as u64; let estimated_tokens = ((prompt.len() + response.len()) / 4) as u64; let session_end = StreamEvent { event_type: "session_end".to_string(), session_id: None, content: None, stats: Some(Stats { total_tokens: estimated_tokens, prompt_tokens: Some((prompt.len() / 4) as u64), completion_tokens: Some((response.len() / 4) as u64), duration_ms, }), }; println!("{}", serde_json::to_string(&session_end)?); } } Ok(()) }