use anyhow::Result;
use serde_json::json;
use std::io::Read;
use crate::kernel::Kernel;
pub struct RunOptions {
pub json: bool,
pub session_id: Option<String>,
pub context_file: Option<String>,
pub exit_code: bool,
}
pub async fn cmd_run(kernel: &Kernel, prompt: &str, opts: &RunOptions) -> Result<i32> {
let start = std::time::Instant::now();
let effective_prompt = build_effective_prompt(prompt, &opts.context_file)?;
tracing::info!(
prompt_len = effective_prompt.len(),
session_id = ?opts.session_id,
"Processing run command"
);
kernel.handle().security.audit(
"cli",
oxios_kernel::audit_trail::AuditAction::Other {
detail: format!(
"run: {}",
effective_prompt.chars().take(100).collect::<String>()
),
},
"cli-user",
);
let result = kernel
.execute_prompt_with_session(&effective_prompt, opts.session_id.as_deref())
.await?;
let duration_ms = start.elapsed().as_millis() as u64;
let exit_code = if opts.exit_code && !result.evaluation_passed {
1
} else {
0
};
if opts.json {
let json_output = json!({
"response": result.response,
"session_id": result.session_id,
"space_id": result.space_id.map(|id| id.to_string()),
"space_tag": result.space_tag,
"seed_id": result.seed_id.map(|id| id.to_string()),
"agent_id": result.agent_id.map(|id| id.to_string()),
"phase_reached": result.phase_reached.to_string(),
"evaluation_passed": result.evaluation_passed,
"exit_code": exit_code,
"duration_ms": duration_ms,
});
println!(
"{}",
serde_json::to_string_pretty(&json_output).unwrap_or_default()
);
} else {
println!("{}", result.response);
if let Some(ref seed_id) = result.seed_id {
println!("\nSeed: {}", seed_id);
}
if let Some(ref session_id) = result.session_id {
println!("Session: {}", session_id);
}
if !result.evaluation_passed {
eprintln!("\n⚠️ Evaluation did not fully pass.");
if let Some(ref output) = result.output {
eprintln!("Notes: {}", output);
}
}
}
Ok(exit_code)
}
fn build_effective_prompt(prompt: &str, context_file: &Option<String>) -> Result<String> {
let Some(ref path) = context_file else {
return Ok(prompt.to_string());
};
let (label, content) = if path == "-" {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
if buf.is_empty() {
return Err(anyhow::anyhow!("stdin is empty, no context to read"));
}
("stdin".to_string(), buf)
} else {
let expanded = oxios_kernel::config::expand_home(path);
let content = std::fs::read_to_string(&expanded)
.map_err(|e| anyhow::anyhow!("failed to read context file '{}': {}", path, e))?;
(path.clone(), content)
};
Ok(format!(
"--- Context ({}) ---\n{}\n--- End Context ---\n\n{}",
label, content, prompt
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_effective_prompt_no_context() {
let result = build_effective_prompt("hello", &None).unwrap();
assert_eq!(result, "hello");
}
#[test]
fn test_build_effective_prompt_with_file() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test.rs");
std::fs::write(&file_path, "fn main() {}").unwrap();
let result = build_effective_prompt(
"review this",
&Some(file_path.to_str().unwrap().to_string()),
)
.unwrap();
assert!(result.contains("--- Context"));
assert!(result.contains("fn main() {}"));
assert!(result.contains("--- End Context ---"));
assert!(result.contains("review this"));
}
}