diff --git a/crates/infigraph-cli/Cargo.toml b/crates/infigraph-cli/Cargo.toml index c463ce4..76ed356 100644 --- a/crates/infigraph-cli/Cargo.toml +++ b/crates/infigraph-cli/Cargo.toml @@ -68,3 +68,4 @@ libc = "0.2" [dev-dependencies] tempfile = "3" +toml = "0.8" diff --git a/crates/infigraph-cli/src/config_targets.rs b/crates/infigraph-cli/src/config_targets.rs index 5422776..1376bf2 100644 --- a/crates/infigraph-cli/src/config_targets.rs +++ b/crates/infigraph-cli/src/config_targets.rs @@ -117,15 +117,14 @@ pub(crate) fn install_toml_target(config_path: &std::path::Path, mcp_path_str: & String::new() }; - let mcp_block = format!( - "[mcp]\ninfigraph = {{ command = \"{}\", args = [\"--mcp\"] }}\n", - mcp_path_str - ); + let escaped_path = mcp_path_str.replace('\\', "\\\\").replace('"', "\\\""); + let section_header = "[mcp_servers.infigraph]"; + let mcp_block = format!("{section_header}\ncommand = \"{escaped_path}\"\nargs = [\"--mcp\"]\n"); let new_content = if existing.is_empty() { mcp_block - } else if let Some(start) = existing.find("[mcp]") { - let after_header = start + "[mcp]".len(); + } else if let Some(start) = existing.find(section_header) { + let after_header = start + section_header.len(); let section_end = existing[after_header..] .find("\n[") .map(|pos| after_header + pos + 1) @@ -194,35 +193,34 @@ pub(crate) fn uninstall_toml_target<'a>( let content = std::fs::read_to_string(config_path) .with_context(|| format!("Failed to read {}", config_path.display()))?; - if let Some(start) = content.find("[mcp]") { - let after_header = start + "[mcp]".len(); + let section_header = "[mcp_servers.infigraph]"; + if let Some(start) = content.find(section_header) { + let after_header = start + section_header.len(); let section_end = content[after_header..] .find("\n[") .map(|pos| after_header + pos + 1) .unwrap_or(content.len()); - let section_text = &content[start..section_end]; - if section_text.contains("infigraph") { - let new_content = format!("{}{}", &content[..start], &content[section_end..]); - let trimmed = new_content.trim_end().to_string(); - let final_content = if trimmed.is_empty() { - String::new() - } else { - format!("{}\n", trimmed) - }; - std::fs::write(config_path, final_content.as_bytes()) - .with_context(|| format!("Failed to write {}", config_path.display()))?; - println!( - " Removed infigraph from {} ({})", - label, - config_path.display() - ); - return Ok(Some(label)); + let new_content = format!("{}{}", &content[..start], &content[section_end..]); + let trimmed = new_content.trim_end().to_string(); + let final_content = if trimmed.is_empty() { + String::new() } else { - println!(" Skipping {} (infigraph entry not found in [mcp])", label); - } + format!("{}\n", trimmed) + }; + std::fs::write(config_path, final_content.as_bytes()) + .with_context(|| format!("Failed to write {}", config_path.display()))?; + println!( + " Removed infigraph from {} ({})", + label, + config_path.display() + ); + return Ok(Some(label)); } else { - println!(" Skipping {} (no [mcp] section in config)", label); + println!( + " Skipping {} (no [mcp_servers.infigraph] section in config)", + label + ); } Ok(None) @@ -287,6 +285,11 @@ mod tests { install_toml_target(&config, "/usr/bin/infigraph-mcp").unwrap(); let content = std::fs::read_to_string(&config).unwrap(); + assert!( + content.contains("[mcp_servers.infigraph]"), + "Codex expects [mcp_servers.infigraph], got: {}", + content + ); assert!( content.contains(r#"args = ["--mcp"]"#), "toml args must be [\"--mcp\"], got: {}", @@ -296,6 +299,34 @@ mod tests { assert!(!content.contains("--port"), "must not contain --port"); } + #[test] + fn install_toml_escapes_windows_path() { + let dir = tempfile::tempdir().unwrap(); + let config = dir.path().join("config.toml"); + install_toml_target(&config, r"C:\Users\foo\infigraph-mcp.exe").unwrap(); + + let content = std::fs::read_to_string(&config).unwrap(); + let parsed: toml::Value = toml::from_str(&content) + .unwrap_or_else(|e| panic!("generated TOML must parse, got error {e}: {content}")); + assert_eq!( + parsed["mcp_servers"]["infigraph"]["command"].as_str(), + Some(r"C:\Users\foo\infigraph-mcp.exe") + ); + } + + #[test] + fn install_toml_preserves_existing_sections() { + let dir = tempfile::tempdir().unwrap(); + let config = dir.path().join("config.toml"); + std::fs::write(&config, "[mcp_servers.other]\ncommand = \"other\"\n").unwrap(); + + install_toml_target(&config, "/usr/bin/infigraph-mcp").unwrap(); + + let content = std::fs::read_to_string(&config).unwrap(); + assert!(content.contains("[mcp_servers.other]")); + assert!(content.contains("[mcp_servers.infigraph]")); + } + #[test] fn uninstall_json_removes_infigraph() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/infigraph-mcp/Cargo.toml b/crates/infigraph-mcp/Cargo.toml index 00d1cda..202dd58 100644 --- a/crates/infigraph-mcp/Cargo.toml +++ b/crates/infigraph-mcp/Cargo.toml @@ -30,6 +30,7 @@ hmac = "0.12" hex = "0.4" fs2 = "0.4" toml = "0.8" +dirs-next = "2" ort = { version = "2.0.0-rc.12", features = ["download-binaries"], optional = true } ndarray = { version = "0.16", optional = true } tokenizers = { version = "0.21", default-features = false, features = ["onig"], optional = true } diff --git a/crates/infigraph-mcp/src/lib.rs b/crates/infigraph-mcp/src/lib.rs index e6d33e4..dc037b9 100644 --- a/crates/infigraph-mcp/src/lib.rs +++ b/crates/infigraph-mcp/src/lib.rs @@ -592,9 +592,10 @@ pub fn mcp_log(level: &str, msg: &str) { } fn mcp_log_file_path() -> std::path::PathBuf { - std::env::var("HOME") + std::env::var_os("HOME") .map(std::path::PathBuf::from) - .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .or_else(dirs_next::home_dir) + .unwrap_or_else(|| std::path::PathBuf::from(".")) .join(".infigraph") .join("mcp.log") } @@ -728,9 +729,10 @@ pub fn handle_tools_call(id: &Value, request: &Value) -> Value { .and_then(|p| p.as_str()) .map(|p| std::path::PathBuf::from(p).join(".infigraph")) .or_else(|| { - std::env::var("HOME") - .ok() - .map(|h| std::path::PathBuf::from(h).join(".infigraph")) + std::env::var_os("HOME") + .map(std::path::PathBuf::from) + .or_else(dirs_next::home_dir) + .map(|h| h.join(".infigraph")) }) { let _ = std::fs::create_dir_all(&dir); diff --git a/crates/infigraph-mcp/src/main.rs b/crates/infigraph-mcp/src/main.rs index 55b2047..c076444 100644 --- a/crates/infigraph-mcp/src/main.rs +++ b/crates/infigraph-mcp/src/main.rs @@ -96,13 +96,10 @@ fn auto_reindex_all() { } // Reindex group combined graphs - let groups_dir = std::env::var("HOME") - .map(|h| { - std::path::PathBuf::from(h) - .join(".infigraph") - .join("groups") - }) - .ok(); + let groups_dir = std::env::var_os("HOME") + .map(std::path::PathBuf::from) + .or_else(dirs_next::home_dir) + .map(|h| h.join(".infigraph").join("groups")); if let Some(ref gd) = groups_dir { if let Ok(entries) = std::fs::read_dir(gd) { for entry in entries.flatten() { @@ -149,7 +146,9 @@ fn find_infigraph_cli_for_reindex() -> Option { } } // Check common install locations - let home = std::env::var("HOME").ok().map(std::path::PathBuf::from); + let home = std::env::var_os("HOME") + .map(std::path::PathBuf::from) + .or_else(dirs_next::home_dir); if let Some(ref h) = home { let local_bin = h.join(".local").join("bin").join(bin_name); if local_bin.exists() { @@ -198,9 +197,10 @@ fn install_panic_hook() { } fn acquire_instance_lock() -> Option { - let lock_path = std::env::var("HOME") + let lock_path = std::env::var_os("HOME") .map(std::path::PathBuf::from) - .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .or_else(dirs_next::home_dir) + .unwrap_or_else(|| std::path::PathBuf::from(".")) .join(".infigraph") .join("mcp.lock"); if let Some(parent) = lock_path.parent() { diff --git a/crates/infigraph-mcp/src/tools/watch.rs b/crates/infigraph-mcp/src/tools/watch.rs index a611c12..b53ab9d 100644 --- a/crates/infigraph-mcp/src/tools/watch.rs +++ b/crates/infigraph-mcp/src/tools/watch.rs @@ -20,9 +20,10 @@ pub fn watchers_disabled() -> bool { fn watch_log(level: &str, msg: &str) { use std::io::Write; - let path = std::env::var("HOME") + let path = std::env::var_os("HOME") .map(std::path::PathBuf::from) - .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .or_else(dirs_next::home_dir) + .unwrap_or_else(|| std::path::PathBuf::from(".")) .join(".infigraph") .join("mcp.log"); if let Ok(mut f) = std::fs::OpenOptions::new() diff --git a/crates/infigraph-mcp/src/web/handlers_analysis.rs b/crates/infigraph-mcp/src/web/handlers_analysis.rs index 6ca9b13..013e56a 100644 --- a/crates/infigraph-mcp/src/web/handlers_analysis.rs +++ b/crates/infigraph-mcp/src/web/handlers_analysis.rs @@ -187,8 +187,10 @@ pub(crate) fn api_routes(params: &Value) -> Value { } pub(crate) fn api_groups(_params: &Value) -> Value { - let registry_path = std::env::var("HOME") - .map(|h| PathBuf::from(h).join(".infigraph").join("registry.json")) + let registry_path = std::env::var_os("HOME") + .map(PathBuf::from) + .or_else(dirs_next::home_dir) + .map(|h| h.join(".infigraph").join("registry.json")) .unwrap_or_default(); if !registry_path.exists() { return json!({"groups": [], "count": 0}); @@ -216,8 +218,10 @@ pub(crate) fn api_groups(_params: &Value) -> Value { pub(crate) fn api_contracts(params: &Value) -> Value { let group_name = params.get("group").and_then(|g| g.as_str()).unwrap_or(""); - let registry_path = std::env::var("HOME") - .map(|h| PathBuf::from(h).join(".infigraph").join("registry.json")) + let registry_path = std::env::var_os("HOME") + .map(PathBuf::from) + .or_else(dirs_next::home_dir) + .map(|h| h.join(".infigraph").join("registry.json")) .unwrap_or_default(); if !registry_path.exists() { return json!({"contracts": [], "count": 0});