use clap::Parser; use color_eyre::eyre::{Result, eyre}; use config_agent::load_settings; use futures_util::TryStreamExt; use hooks::{HookEvent, HookManager, HookResult}; use llm_ollama::{OllamaClient, OllamaOptions, types::ChatMessage}; use permissions::{PermissionDecision, Tool}; use std::io::{self, Write}; #[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 }, } #[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, #[arg()] prompt: Vec, #[command(subcommand)] cmd: Option, } #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; let args = Args::parse(); let mut settings = load_settings(None).unwrap_or_default(); // 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 hook_mgr = HookManager::new("."); 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)?; println!("{}", s); 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/ let command_path = format!(".owlen/commands/{}.md", command_name); // Read the command file let content = match tools_fs::read_file(&command_path) { Ok(c) => c, Err(_) => { return Err(eyre!( "Slash command '{}' not found at {}", command_name, command_path )); } }; // 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.")); } } } } } let prompt = if args.prompt.is_empty() { "Say hello".to_string() } else { args.prompt.join(" ") }; let model = args.model.unwrap_or(settings.model); let api_key = args.api_key.or(settings.api_key); // Use Ollama Cloud when model has "-cloud" suffix AND API key is set 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 = args.ollama_url.unwrap_or(settings.ollama_url); let mut client = OllamaClient::new(base_url); if let Some(key) = api_key { client = client.with_api_key(key); } client }; let opts = OllamaOptions { model, stream: true, }; let msgs = vec![ChatMessage { role: "user".into(), content: prompt, }]; let mut stream = client.chat_stream(&msgs, &opts).await?; while let Some(chunk) = stream.try_next().await? { if let Some(m) = chunk.message { if let Some(c) = m.content { print!("{c}"); io::stdout().flush()?; } } if matches!(chunk.done, Some(true)) { break; } } println!(); // Newline after response Ok(()) }