use crate::ai_agents::{AiAgentAvailability, AiAgentStreamEvent}; use crate::cli_agent_runtime::{AgentStreamRequest, LineStreamProcess}; use std::path::Path; use std::process::Stdio; struct KiroMcpConfig<'a> { vault_path: &'a str, vault_paths: &'a [String], mcp_server_path: &'a str, } pub fn check_cli() -> AiAgentAvailability { crate::kiro_discovery::check_cli() } pub fn run_agent_stream(request: AgentStreamRequest, emit: F) -> Result where F: FnMut(AiAgentStreamEvent), { let binary = crate::kiro_discovery::find_binary()?; ensure_mcp_config(&request)?; let prompt = crate::cli_agent_runtime::build_prompt(&request.message, request.system_prompt.as_deref()); let command = build_kiro_command(&binary, Path::new(&request.vault_path)); crate::cli_agent_runtime::run_ai_agent_line_stream( LineStreamProcess::new(command, "kiro-cli", "kiro").with_stdin(prompt), emit, format_kiro_error, ) } fn build_kiro_command(binary: &Path, vault_path: &Path) -> std::process::Command { let mut command = crate::hidden_command(binary); crate::cli_agent_runtime::configure_agent_command_environment(&mut command, binary); command .arg("chat") .arg("--no-interactive") .arg("--trust-all-tools") .current_dir(vault_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); command } fn ensure_mcp_config(request: &AgentStreamRequest) -> Result<(), String> { let mcp_server_path = crate::cli_agent_runtime::mcp_server_path_string()?; write_mcp_json(KiroMcpConfig { vault_path: &request.vault_path, vault_paths: &request.vault_paths, mcp_server_path: &mcp_server_path, }) } fn write_mcp_json(config: KiroMcpConfig<'_>) -> Result<(), String> { let config_dir = Path::new(config.vault_path).join(".kiro").join("settings"); std::fs::create_dir_all(&config_dir) .map_err(|e| format!("Failed to create .kiro/settings: {e}"))?; let config_path = config_dir.join("mcp.json"); let mut json_config: serde_json::Value = std::fs::read_to_string(&config_path) .ok() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_else(|| serde_json::json!({})); let servers = json_config .as_object_mut() .ok_or("Invalid mcp.json: not an object")? .entry("mcpServers") .or_insert_with(|| serde_json::json!({})); let mut server = crate::cli_agent_runtime::tolaria_node_mcp_server( config.mcp_server_path, config.vault_path, config.vault_paths, true, ); server["disabled"] = serde_json::json!(false); servers["tolaria"] = server; std::fs::write( &config_path, serde_json::to_string_pretty(&json_config) .map_err(|e| format!("JSON serialize error: {e}"))?, ) .map_err(|e| format!("Failed to write mcp.json: {e}"))?; Ok(()) } fn format_kiro_error(stderr_output: &str, status: &str) -> String { if is_auth_error(stderr_output) { return "Kiro CLI is not authenticated. Run `kiro-cli login` in your terminal to sign in." .into(); } if stderr_output.trim().is_empty() { format!("kiro-cli exited with status {status}") } else { stderr_output.lines().take(3).collect::>().join("\n") } } fn is_auth_error(stderr_output: &str) -> bool { let lower = stderr_output.to_ascii_lowercase(); ["auth", "login", "token"] .iter() .any(|needle| lower.contains(needle)) } #[cfg(test)] mod tests { use super::*; #[test] fn strip_ansi_codes_removes_terminal_colors() { assert_eq!( crate::cli_agent_runtime::strip_ansi_codes("\x1b[38;5;141m> \x1b[0mHello! \x1b[2K"), "> Hello! " ); assert_eq!( crate::cli_agent_runtime::strip_ansi_codes("plain text"), "plain text" ); } #[test] fn format_kiro_error_detects_auth_errors() { let result = format_kiro_error("Error: auth token expired", "1"); assert!(result.contains("kiro-cli login")); } #[test] fn format_kiro_error_returns_status_for_empty_stderr() { let result = format_kiro_error("", "1"); assert!(result.contains("status 1")); } #[test] fn write_mcp_json_creates_config() { let dir = tempfile::tempdir().unwrap(); let vault_path = dir.path().to_str().unwrap(); write_mcp_json(KiroMcpConfig { vault_path, vault_paths: &["/other/vault".into(), vault_path.into()], mcp_server_path: "/opt/mcp/index.js", }) .unwrap(); let config_path = dir.path().join(".kiro/settings/mcp.json"); let content: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&config_path).unwrap()).unwrap(); assert_eq!(content["mcpServers"]["tolaria"]["command"], "node"); assert_eq!( content["mcpServers"]["tolaria"]["args"][0], "/opt/mcp/index.js" ); assert_eq!( content["mcpServers"]["tolaria"]["env"]["VAULT_PATH"], vault_path ); assert_eq!( content["mcpServers"]["tolaria"]["env"]["VAULT_PATHS"], serde_json::json!(serde_json::to_string(&vec![vault_path, "/other/vault"]).unwrap()) ); assert_eq!( content["mcpServers"]["tolaria"]["env"]["WS_UI_PORT"], "9711" ); } #[test] fn write_mcp_json_merges_preserving_existing_servers() { let dir = tempfile::tempdir().unwrap(); let vault_path = dir.path().to_str().unwrap(); let config_dir = dir.path().join(".kiro/settings"); std::fs::create_dir_all(&config_dir).unwrap(); std::fs::write( config_dir.join("mcp.json"), r#"{"mcpServers":{"other":{"command":"python","args":["server.py"]}}}"#, ) .unwrap(); write_mcp_json(KiroMcpConfig { vault_path, vault_paths: &[], mcp_server_path: "/new/index.js", }) .unwrap(); let content: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(dir.path().join(".kiro/settings/mcp.json")).unwrap(), ) .unwrap(); assert_eq!(content["mcpServers"]["tolaria"]["args"][0], "/new/index.js"); assert_eq!(content["mcpServers"]["other"]["command"], "python"); } }