diff --git a/libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs b/libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs index 50576531fd..501dbc8ce5 100644 --- a/libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs +++ b/libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs @@ -120,6 +120,58 @@ impl Default for DriverConfig { } } +/// Path to the persistent JSON config file shared by the CLI and MCP session. +pub fn config_file_path() -> std::path::PathBuf { + let home = std::env::var("HOME").unwrap_or_default(); + std::path::PathBuf::from(format!("{home}/.cua-driver/config.json")) +} + +/// Load `DriverConfig` from `~/.cua-driver/config.json`, falling back to +/// defaults for any missing or unrecognised keys. Called at MCP startup so +/// that `cua-driver config set capture_mode vision` (CLI) carries over into +/// the next MCP session without requiring a per-call `set_config`. +pub fn load_driver_config() -> DriverConfig { + let mut cfg = DriverConfig::default(); + let path = config_file_path(); + let text = match std::fs::read_to_string(&path) { + Ok(t) => t, + Err(_) => return cfg, // no file yet — use defaults + }; + let json: serde_json::Value = match serde_json::from_str(&text) { + Ok(v) => v, + Err(_) => return cfg, // malformed file — use defaults + }; + if let Some(v) = json.get("capture_mode").and_then(|v| v.as_str()) { + cfg.capture_mode = v.to_owned(); + } + if let Some(v) = json.get("max_image_dimension").and_then(|v| v.as_u64()) { + if let Ok(v32) = u32::try_from(v) { + cfg.max_image_dimension = v32; + } + } + cfg +} + +/// Persist a single key/value pair to `~/.cua-driver/config.json`. +/// Merges with any existing file contents so other keys are preserved. +/// Returns `Err` if the directory cannot be created or the file cannot be written. +pub fn write_driver_config_key(key: &str, value: &serde_json::Value) -> Result<(), String> { + let path = config_file_path(); + let mut json: serde_json::Value = path + .exists() + .then(|| std::fs::read_to_string(&path).ok()) + .flatten() + .and_then(|t| serde_json::from_str(&t).ok()) + .unwrap_or_else(|| serde_json::json!({})); + json[key] = value.clone(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let body = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?; + std::fs::write(&path, body).map_err(|e| e.to_string())?; + Ok(()) +} + /// Shared state passed to all tools. pub struct ToolState { pub element_cache: Arc, @@ -136,7 +188,9 @@ impl Default for ToolState { cursor_registry: Arc::new(CursorRegistry::new()), zoom_registry: Arc::new(ZoomRegistry::new()), resize_registry: Arc::new(ResizeRegistry::new()), - config: Arc::new(std::sync::RwLock::new(DriverConfig::default())), + // Load persisted config from ~/.cua-driver/config.json so that + // `cua-driver config set` changes carry over into MCP sessions. + config: Arc::new(std::sync::RwLock::new(load_driver_config())), } } } diff --git a/libs/cua-driver-rs/crates/platform-macos/src/tools/set_config.rs b/libs/cua-driver-rs/crates/platform-macos/src/tools/set_config.rs index 81eef9b992..6dd3a8e562 100644 --- a/libs/cua-driver-rs/crates/platform-macos/src/tools/set_config.rs +++ b/libs/cua-driver-rs/crates/platform-macos/src/tools/set_config.rs @@ -3,7 +3,7 @@ use mcp_server::{protocol::ToolResult, tool::{Tool, ToolDef}}; use serde_json::Value; use std::sync::Arc; -use super::ToolState; +use super::{write_driver_config_key, ToolState}; pub struct SetConfigTool { state: Arc, @@ -49,9 +49,19 @@ impl Tool for SetConfigTool { let mut cfg = self.state.config.write().unwrap(); if let Some(mode) = args.get("capture_mode").and_then(|v| v.as_str()) { cfg.capture_mode = mode.to_owned(); + if let Err(e) = write_driver_config_key("capture_mode", &Value::String(mode.to_owned())) { + tracing::warn!("set_config: failed to persist capture_mode: {e}"); + } } if let Some(dim) = args.get("max_image_dimension").and_then(|v| v.as_u64()) { - cfg.max_image_dimension = dim as u32; + if let Ok(dim32) = u32::try_from(dim) { + cfg.max_image_dimension = dim32; + if let Err(e) = write_driver_config_key("max_image_dimension", &Value::Number(dim.into())) { + tracing::warn!("set_config: failed to persist max_image_dimension: {e}"); + } + } else { + return ToolResult::error(format!("max_image_dimension {dim} exceeds u32::MAX")); + } } ToolResult::text(format!( "Config updated: capture_mode={}, max_image_dimension={}", diff --git a/libs/cua-driver-rs/scripts/install.sh b/libs/cua-driver-rs/scripts/install.sh index 4a35d2baec..55a050bf38 100644 --- a/libs/cua-driver-rs/scripts/install.sh +++ b/libs/cua-driver-rs/scripts/install.sh @@ -100,8 +100,19 @@ VERSION="${TAG#${TAG_PREFIX}}" # Prefer the bare-binary tarball (single `cua-driver` file at the root) — # the directory tarball would require unpacking and copying, but the bare -# form is curl-pipe-able. Both forms are published per release. -TARBALL="cua-driver-rs-${VERSION}-${LABEL}-binary.tar.gz" +# form is curl-pipe-able. +# +# macOS: the release workflow publishes one universal binary (arm64 + x86_64 +# lipo'd together) named `darwin-universal-binary`. There are NO per-arch +# bare-binary tarballs for macOS — only the directory tarballs are split +# by arch (darwin-arm64 / darwin-x86_64). The universal binary works on +# both Apple Silicon and Intel, so we always fetch it on macOS. +# +# Linux: per-arch bare-binary tarballs exist (e.g. linux-x86_64-binary). +case "$LABEL" in + darwin-*) TARBALL="cua-driver-rs-${VERSION}-darwin-universal-binary.tar.gz" ;; + *) TARBALL="cua-driver-rs-${VERSION}-${LABEL}-binary.tar.gz" ;; +esac URL="https://github.com/$REPO/releases/download/$TAG/$TARBALL" log "downloading $URL"