use mcp_host::protocol::types::{JsonRpcError, JsonRpcResponse};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
pub fn get_cache_dir() -> std::path::PathBuf {
if let Ok(cwd) = std::env::current_dir() {
let config_file = cwd.join(".dictate.toml");
if config_file.exists() {
let cache = cwd.join(".dictator").join("cache");
let _ = std::fs::create_dir_all(&cache);
let _ = std::fs::set_permissions(&cache, std::fs::Permissions::from_mode(0o700));
return cache;
}
}
let cache_dir = std::env::var("XDG_CACHE_HOME")
.ok()
.or_else(|| std::env::var("HOME").ok().map(|h| format!("{h}/.cache")))
.unwrap_or_else(|| "/tmp".to_string());
let dictator_cache = std::path::Path::new(&cache_dir).join("dictator");
let _ = std::fs::create_dir_all(&dictator_cache);
dictator_cache
}
pub fn get_log_path(filename: &str) -> std::path::PathBuf {
get_cache_dir().join(filename)
}
pub fn log_to_file(msg: &str) {
let log_file = get_log_path("mcp.log");
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_file) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let _ = writeln!(
file,
"[{}.{:03}] {}",
now.as_secs(),
now.subsec_millis(),
msg
);
}
}
#[must_use]
pub fn current_dir_or_default() -> std::path::PathBuf {
std::env::current_dir().unwrap_or_default()
}
#[must_use]
pub fn invalid_arguments_response(id: serde_json::Value) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(JsonRpcError {
code: -32602,
message: "Missing or invalid arguments".to_string(),
data: None,
}),
}
}
pub fn parse_arguments<T: DeserializeOwned>(
id: &serde_json::Value,
arguments: Option<serde_json::Value>,
) -> Result<T, Box<JsonRpcResponse>> {
arguments
.and_then(|a| serde_json::from_value(a).ok())
.ok_or_else(|| Box::new(invalid_arguments_response(id.clone())))
}
#[allow(dead_code)]
pub fn is_git_repo() -> bool {
let cwd = current_dir_or_default();
cwd.join(".git").exists()
}
pub fn command_available(cmd: &str) -> bool {
std::process::Command::new("which")
.arg(cmd)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub use mcp_host::utils::{base64_decode, base64_encode, byte_to_line_col, collect_files};
pub fn is_within_cwd(path: &std::path::Path, cwd: &std::path::Path) -> bool {
mcp_host::utils::is_safe_path(path, cwd)
}
#[must_use]
pub fn partition_paths_within_cwd(
paths: &[String],
cwd: &std::path::Path,
) -> (Vec<String>, Vec<String>) {
let mut allowed = Vec::new();
let mut rejected = Vec::new();
for path in paths {
let candidate = std::path::Path::new(path);
if is_within_cwd(candidate, cwd) {
allowed.push(path.clone());
} else {
rejected.push(path.clone());
}
}
(allowed, rejected)
}
#[must_use]
pub fn to_json_string<T: Serialize>(value: &T) -> String {
serde_json::to_string(value).unwrap_or_default()
}
#[must_use]
pub fn to_json_string_pretty<T: Serialize>(value: &T) -> String {
serde_json::to_string_pretty(value).unwrap_or_default()
}
pub fn make_snippet(source: &str, span: &dictator_decree_abi::Span, max_len: usize) -> String {
if source.is_empty() {
return String::new();
}
let start = span.start.min(source.len());
let line_start = source[..start].rfind('\n').map_or(0, |idx| idx + 1);
let line_end = source[start..]
.find('\n')
.map_or_else(|| source.len(), |off| start + off);
let line = &source[line_start..line_end];
let mut cleaned: String = line
.chars()
.map(|c| if c.is_control() && c != '\t' { ' ' } else { c })
.collect();
cleaned.truncate(cleaned.trim_end().len());
if cleaned.len() > max_len {
let mut out = cleaned
.chars()
.take(max_len.saturating_sub(1))
.collect::<String>();
out.push('…');
out
} else {
cleaned
}
}