feat(cli): add interactive REPL mode with agent loop

Add proper interactive mode when no prompt is provided:

**Interactive REPL Features**:
- Starts when running `cargo run` with no arguments
- Shows welcome message with model name
- Prompts with `> ` for user input
- Each input runs through the full agent loop with tools
- Continues until Ctrl+C or EOF
- Displays tool calls and results in real-time

**Changes**:
- Detect empty prompt and enter interactive loop
- Use stdin.lines() for reading user input
- Call agent_core::run_agent_loop for each message
- Handle errors gracefully and continue
- Clean up unused imports

**Usage**:
```bash
# Interactive mode
cargo run

# Single prompt mode
cargo run -- --print "Find all Cargo.toml files"

# Tool subcommands
cargo run -- glob "**/*.rs"
```

Example session:
```
🤖 Owlen Interactive Mode
Model: qwen3:8b

> Find all markdown files
🔧 Tool call: glob with args: {"pattern":"**/*.md"}
 Tool result: ./README.md ./CLAUDE.md ./AGENTS.md
...

> exit
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-01 21:00:56 +01:00
parent e77e33ce2f
commit 6022aeb2b0

View File

@@ -1,12 +1,11 @@
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use color_eyre::eyre::{Result, eyre}; use color_eyre::eyre::{Result, eyre};
use config_agent::load_settings; use config_agent::load_settings;
use futures_util::TryStreamExt;
use hooks::{HookEvent, HookManager, HookResult}; use hooks::{HookEvent, HookManager, HookResult};
use llm_ollama::{OllamaClient, OllamaOptions, types::ChatMessage}; use llm_ollama::{OllamaClient, OllamaOptions};
use permissions::{PermissionDecision, Tool}; use permissions::{PermissionDecision, Tool};
use serde::Serialize; use serde::Serialize;
use std::io::{self, Write}; use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, ValueEnum)] #[derive(Debug, Clone, Copy, ValueEnum)]
@@ -435,12 +434,6 @@ async fn main() -> Result<()> {
} }
} }
let prompt = if args.prompt.is_empty() {
"Say hello".to_string()
} else {
args.prompt.join(" ")
};
let model = args.model.unwrap_or(settings.model); let model = args.model.unwrap_or(settings.model);
let api_key = args.api_key.or(settings.api_key); let api_key = args.api_key.or(settings.api_key);
@@ -461,6 +454,45 @@ async fn main() -> Result<()> {
stream: true, stream: true,
}; };
// Check if interactive mode (no prompt provided)
if args.prompt.is_empty() {
println!("🤖 Owlen Interactive Mode");
println!("Model: {}", opts.model);
println!("Type your message and press Enter. Press Ctrl+C to exit.\n");
use std::io::{stdin, BufRead};
let stdin = stdin();
let mut lines = stdin.lock().lines();
loop {
print!("\n> ");
std::io::stdout().flush().ok();
if let Some(Ok(line)) = lines.next() {
let prompt = line.trim();
if prompt.is_empty() {
continue;
}
// Run agent loop for this prompt
match agent_core::run_agent_loop(&client, prompt, &opts, &perms).await {
Ok(response) => {
println!("\n{}", response);
}
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(); let start_time = SystemTime::now();
// Handle different output formats // Handle different output formats