1use std::path::{Path, PathBuf};
5
6pub fn merged_desktop_config(existing: &str, exe: &Path, space: &str) -> Result<String, String> {
9 let mut root: serde_json::Value = if existing.trim().is_empty() {
10 serde_json::json!({})
11 } else {
12 serde_json::from_str(existing).map_err(|e| format!("existing config is not JSON: {e}"))?
13 };
14 if !root.is_object() {
15 return Err("existing config is not a JSON object".into());
16 }
17 let servers = root
18 .as_object_mut()
19 .expect("checked object")
20 .entry("mcpServers")
21 .or_insert_with(|| serde_json::json!({}));
22 if !servers.is_object() {
23 return Err("mcpServers is not an object".into());
24 }
25 servers.as_object_mut().expect("checked object").insert(
26 "scone".to_owned(),
27 serde_json::json!({
28 "command": exe.display().to_string(),
29 "args": ["--space", space, "mcp"],
30 }),
31 );
32 serde_json::to_string_pretty(&root).map_err(|e| e.to_string())
33}
34
35pub fn desktop_config_path() -> Result<PathBuf, String> {
36 let home = std::env::var_os("HOME").ok_or("cannot resolve HOME")?;
37 let base = if cfg!(target_os = "macos") {
38 PathBuf::from(&home).join("Library/Application Support/Claude")
39 } else {
40 PathBuf::from(&home).join(".config/Claude")
41 };
42 Ok(base.join("claude_desktop_config.json"))
43}
44
45pub fn setup_claude_desktop(space: &str) -> Result<String, String> {
46 let exe = std::env::current_exe().map_err(|e| e.to_string())?;
47 let path = desktop_config_path()?;
48 if let Some(parent) = path.parent() {
49 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
50 }
51 let existing = std::fs::read_to_string(&path).unwrap_or_default();
52 let merged = merged_desktop_config(&existing, &exe, space)?;
53 std::fs::write(&path, merged).map_err(|e| e.to_string())?;
54 Ok(format!(
55 "wrote {}\nrestart Claude Desktop to pick up the scone memory server",
56 path.display()
57 ))
58}
59
60pub fn setup_claude_code(space: &str) -> Result<String, String> {
61 let exe = std::env::current_exe().map_err(|e| e.to_string())?;
62 let output = std::process::Command::new("claude")
63 .args([
64 "mcp",
65 "add",
66 "scone",
67 "--",
68 &exe.display().to_string(),
69 "--space",
70 space,
71 "mcp",
72 ])
73 .output()
74 .map_err(|_| "the `claude` CLI is not on PATH; install Claude Code first".to_owned())?;
75 if !output.status.success() {
76 return Err(format!(
77 "claude mcp add failed: {}",
78 String::from_utf8_lossy(&output.stderr)
79 ));
80 }
81 Ok(format!(
82 "registered the scone memory server with Claude Code (space: {space})"
83 ))
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn merge_into_empty_and_invalid() {
92 let merged =
93 merged_desktop_config("", Path::new("/usr/local/bin/scone"), "default").unwrap();
94 let v: serde_json::Value = serde_json::from_str(&merged).unwrap();
95 assert_eq!(v["mcpServers"]["scone"]["args"][2], "mcp");
96 assert!(merged_desktop_config("[1,2]", Path::new("/x"), "d").is_err());
97 assert!(merged_desktop_config("not json", Path::new("/x"), "d").is_err());
98 }
99}