use crate::opencode_cli::AgentStreamRequest; use std::path::Path; use std::process::Stdio; pub(crate) fn build_command( binary: &Path, request: &AgentStreamRequest, ) -> Result { let mut command = crate::hidden_command(binary); command .args(build_args()) .arg(build_prompt(request)) .env( "OPENCODE_CONFIG_CONTENT", build_config(&request.vault_path)?, ) .current_dir(&request.vault_path) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); Ok(command) } fn build_args() -> Vec { vec!["run".into(), "--format".into(), "json".into()] } fn build_prompt(request: &AgentStreamRequest) -> String { match request .system_prompt .as_ref() .map(|prompt| prompt.trim()) .filter(|prompt| !prompt.is_empty()) { Some(system_prompt) => format!( "System instructions:\n{system_prompt}\n\nUser request:\n{}", request.message ), None => request.message.clone(), } } fn build_config(vault_path: &str) -> Result { let mcp_server = crate::mcp::mcp_server_dir()?.join("index.js"); let mcp_server_path = mcp_server .to_str() .ok_or("Invalid MCP server path")? .to_string(); serde_json::to_string(&serde_json::json!({ "$schema": "https://opencode.ai/config.json", "permission": permission_config(), "mcp": { "tolaria": { "type": "local", "command": ["node", mcp_server_path], "environment": { "VAULT_PATH": vault_path }, "enabled": true } } })) .map_err(|error| format!("Failed to serialize opencode config: {error}")) } fn permission_config() -> serde_json::Value { serde_json::json!({ "read": "allow", "edit": "allow", "glob": "allow", "grep": "allow", "list": "allow", "external_directory": "deny", "bash": "deny" }) } #[cfg(test)] mod tests { use super::*; use std::ffi::OsStr; use std::path::PathBuf; fn request() -> AgentStreamRequest { AgentStreamRequest { message: "Rename the note".into(), system_prompt: None, vault_path: "/tmp/vault".into(), } } #[test] fn args_use_documented_safe_run_mode() { let args = build_args(); assert_eq!(args, vec!["run", "--format", "json"]); assert!(!args.contains(&"--dangerously-skip-permissions".to_string())); assert!(!args.contains(&"--dir".to_string())); assert!(!args.contains(&"--thinking".to_string())); } #[test] fn command_sets_vault_cwd_and_mcp_config() { let command = build_command(&PathBuf::from("opencode"), &request()).unwrap(); let actual_args: Vec<&OsStr> = command.get_args().collect(); let config_value = command .get_envs() .find(|(key, _)| *key == OsStr::new("OPENCODE_CONFIG_CONTENT")) .and_then(|(_, value)| value); assert_eq!(command.get_program(), OsStr::new("opencode")); assert_eq!(actual_args[0], OsStr::new("run")); assert_eq!(actual_args[1], OsStr::new("--format")); assert_eq!(actual_args[2], OsStr::new("json")); assert_eq!(actual_args.last(), Some(&OsStr::new("Rename the note"))); assert_eq!(command.get_current_dir(), Some(Path::new("/tmp/vault"))); assert!(config_value.is_some()); } #[test] fn config_includes_permissions_and_tolaria_mcp_server() { if let Ok(config) = build_config("/tmp/vault") { let json: serde_json::Value = serde_json::from_str(&config).unwrap(); assert_eq!(json["permission"]["edit"], "allow"); assert_eq!(json["permission"]["external_directory"], "deny"); assert_eq!(json["permission"]["bash"], "deny"); assert_eq!(json["mcp"]["tolaria"]["type"], "local"); assert_eq!(json["mcp"]["tolaria"]["command"][0], "node"); assert_eq!( json["mcp"]["tolaria"]["environment"]["VAULT_PATH"], "/tmp/vault" ); assert!(json["mcp"]["tolaria"]["command"][1] .as_str() .unwrap() .ends_with("index.js")); } } #[test] fn prompt_keeps_system_prompt_first() { let prompt = build_prompt(&AgentStreamRequest { system_prompt: Some("Be concise".into()), ..request() }); assert!(prompt.starts_with("System instructions:\nBe concise")); assert!(prompt.contains("User request:\nRename the note")); } }