engram_core/
init_handler.rs1use std::fs;
2use std::path::{Path, PathBuf};
3
4use engram_storage::Database;
5
6use crate::config::home_directory;
7use crate::error::CoreError;
8
9const ENGRAM_DIRECTORY: &str = ".engram";
10const CONFIG_FILENAME: &str = "engram.toml";
11const AGENT_MD_FILENAME: &str = "AGENT.md";
12const DATABASE_FILENAME: &str = "engram.db";
13const SOCKET_FILENAME: &str = "engram.sock";
14const MCP_JSON_FILENAME: &str = ".mcp.json";
15const GITIGNORE_FILENAME: &str = ".gitignore";
16const GITIGNORE_MARKER: &str = ".engram/";
17const AGENT_MD_CONTENT: &str = include_str!("../AGENT.md");
18const UNIX_SOCKET_PATH_MAX_BYTES: usize = 104;
19const MCP_NPX_PACKAGE: &str = "@engramm/engram-mcp-server";
20const DEFAULT_ENGRAM_BIN_NAME: &str = "engram";
21
22const DEFAULT_CONFIG_TEMPLATE: &str = r#"# Global engram config. Runtime always prefers per-project state under
23# <project>/.engram/{engram.db, engram.sock} (discovered by walking up from cwd
24# like .git). The database.path and server.socket_path values below are
25# fallbacks used only when no .engram/ marker is found and no ENGRAM_DB_PATH /
26# ENGRAM_SOCKET_PATH env override is set.
27
28[database]
29path = "~/.engram/memories.db"
30
31[embedding]
32provider = "voyage"
33model = "voyage-code-3"
34dimension = 1024
35# hyde_threshold: opt-in HyDE. 0 = disabled (default). N>0 = enable HyDE
36# when the query has fewer than N words. HyDE adds ~1.5s latency on cache
37# miss but improves recall on terse queries; cache is keyed by the
38# original query so repeated calls are instant.
39hyde_threshold = 0
40
41[llm]
42provider = "openai"
43model = "gpt-4o-mini"
44
45[server]
46socket_path = "~/.engram/engram.sock"
47reindex_interval_secs = 3600
48
49[hnsw]
50max_connections = 16
51ef_construction = 200
52ef_search = 40
53dimension = 1024
54
55[consolidation]
56stale_days = 90
57min_score = 0.3
58"#;
59
60#[derive(Debug)]
61pub enum McpCommand {
62 Node { script_path: PathBuf },
63 Npx,
64}
65
66pub fn execute() -> Result<(), CoreError> {
67 let cwd = std::env::current_dir()
68 .map_err(|error| CoreError::InitFailed(format!("cwd unavailable: {error}")))?;
69 let home = home_directory()
70 .ok_or_else(|| CoreError::InitFailed("HOME environment variable not set".into()))?;
71 try_interactive_wizard();
72 execute_with_dirs(&cwd, Path::new(&home))
73}
74
75pub fn execute_with_dirs(project_dir: &Path, home_dir: &Path) -> Result<(), CoreError> {
76 validate_socket_path(project_dir)?;
77
78 let home_config_path = home_dir.join(ENGRAM_DIRECTORY).join(CONFIG_FILENAME);
79 let mcp_command = resolve_mcp_server_command();
80
81 if home_config_path.exists() {
82 create_engram_directory(project_dir)?;
83 initialize_database(project_dir)?;
84 write_agent_instructions(project_dir)?;
85 write_mcp_json(project_dir, &mcp_command)?;
86 write_gitignore(project_dir)?;
87 return Ok(());
88 }
89
90 create_engram_directory(project_dir)?;
91 write_default_config_to_home(home_dir)?;
92 write_mcp_json(project_dir, &mcp_command)?;
93 initialize_database(project_dir)?;
94 write_agent_instructions(project_dir)?;
95 write_gitignore(project_dir)?;
96 print_completion_info(project_dir);
97 Ok(())
98}
99
100fn try_interactive_wizard() {
101 if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
102 return;
103 }
104 let _ = std::process::Command::new("engram-tui")
105 .arg("init")
106 .status();
107}
108
109fn validate_socket_path(project_dir: &Path) -> Result<(), CoreError> {
110 let socket_path = project_dir.join(ENGRAM_DIRECTORY).join(SOCKET_FILENAME);
111 let length = socket_path.as_os_str().as_encoded_bytes().len();
112 if length >= UNIX_SOCKET_PATH_MAX_BYTES {
113 return Err(CoreError::InitFailed(format!(
114 "project path too deep: socket path '{}' is {length} bytes, max {} (UNIX limit)",
115 socket_path.display(),
116 UNIX_SOCKET_PATH_MAX_BYTES - 1
117 )));
118 }
119 Ok(())
120}
121
122fn create_engram_directory(project_dir: &Path) -> Result<(), CoreError> {
123 let target = project_dir.join(ENGRAM_DIRECTORY);
124 fs::create_dir_all(&target).map_err(|error| {
125 CoreError::InitFailed(format!("failed to create {}: {error}", target.display()))
126 })
127}
128
129fn write_default_config_to_home(home_dir: &Path) -> Result<(), CoreError> {
130 let home_engram = home_dir.join(ENGRAM_DIRECTORY);
131 fs::create_dir_all(&home_engram).map_err(|error| {
132 CoreError::InitFailed(format!(
133 "failed to create {}: {error}",
134 home_engram.display()
135 ))
136 })?;
137 let config_path = home_engram.join(CONFIG_FILENAME);
138 if config_path.exists() {
139 return Ok(());
140 }
141 fs::write(&config_path, DEFAULT_CONFIG_TEMPLATE)
142 .map_err(|error| CoreError::InitFailed(format!("failed to write config: {error}")))
143}
144
145fn initialize_database(project_dir: &Path) -> Result<(), CoreError> {
146 let engram_dir = project_dir.join(ENGRAM_DIRECTORY);
147 fs::create_dir_all(&engram_dir).map_err(|error| {
148 CoreError::InitFailed(format!("failed to create database directory: {error}"))
149 })?;
150 let database_path = engram_dir.join(DATABASE_FILENAME);
151 let database_path_str = database_path
152 .to_str()
153 .ok_or_else(|| CoreError::InitFailed("database path is not valid utf-8".into()))?;
154 let _database = Database::open(database_path_str)?;
155 println!("Database initialized at {database_path_str}");
156 Ok(())
157}
158
159fn write_agent_instructions(project_dir: &Path) -> Result<(), CoreError> {
160 let agent_md_path = project_dir.join(ENGRAM_DIRECTORY).join(AGENT_MD_FILENAME);
161 fs::write(&agent_md_path, AGENT_MD_CONTENT)
162 .map_err(|error| CoreError::InitFailed(format!("failed to write AGENT.md: {error}")))?;
163 println!("Agent instructions written to {}", agent_md_path.display());
164 Ok(())
165}
166
167fn resolve_mcp_server_command() -> McpCommand {
168 if let Ok(env_path) = std::env::var("ENGRAM_MCP_SERVER_PATH") {
169 let path = PathBuf::from(&env_path);
170 if path.exists() {
171 return McpCommand::Node { script_path: path };
172 }
173 }
174 if let Some(bin_path) = resolve_engram_bin()
175 && let Some(bin_dir) = bin_path.parent()
176 {
177 let candidate = bin_dir
178 .join("..")
179 .join("mcp-server")
180 .join("dist")
181 .join("index.js");
182 if candidate.exists() {
183 if let Ok(canonical) = candidate.canonicalize() {
184 return McpCommand::Node {
185 script_path: canonical,
186 };
187 }
188 return McpCommand::Node {
189 script_path: candidate,
190 };
191 }
192 }
193 eprintln!(
194 "warning: could not locate bundled engram mcp-server script; falling back to 'npx {MCP_NPX_PACKAGE}'. \
195 Set ENGRAM_MCP_SERVER_PATH to an absolute path of mcp-server/dist/index.js to silence this warning."
196 );
197 McpCommand::Npx
198}
199
200fn resolve_engram_bin() -> Option<PathBuf> {
201 if let Ok(value) = std::env::var("ENGRAM_BIN") {
202 let candidate = PathBuf::from(value);
203 if candidate.exists() {
204 return Some(candidate);
205 }
206 }
207 let output = std::process::Command::new("which")
208 .arg(DEFAULT_ENGRAM_BIN_NAME)
209 .output()
210 .ok()?;
211 if !output.status.success() {
212 return None;
213 }
214 let stdout = String::from_utf8(output.stdout).ok()?;
215 let trimmed = stdout.trim();
216 if trimmed.is_empty() {
217 return None;
218 }
219 Some(PathBuf::from(trimmed))
220}
221
222fn write_mcp_json(project_dir: &Path, command: &McpCommand) -> Result<(), CoreError> {
223 let (command_name, args): (&str, Vec<String>) = match command {
224 McpCommand::Node { script_path } => {
225 let path_str = script_path
226 .to_str()
227 .ok_or_else(|| CoreError::InitFailed("mcp script path is not valid utf-8".into()))?
228 .to_string();
229 ("node", vec![path_str])
230 }
231 McpCommand::Npx => ("npx", vec![MCP_NPX_PACKAGE.to_string()]),
232 };
233 let engram_bin = resolve_engram_bin();
234 let engram_bin_string = engram_bin.as_ref().and_then(|path| {
235 let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
236 canonical.to_str().map(|value| value.to_string())
237 });
238 let mut server_entry = serde_json::json!({
239 "command": command_name,
240 "args": args,
241 });
242 if let Some(bin_string) = engram_bin_string.as_ref() {
243 server_entry["env"] = serde_json::json!({ "ENGRAM_BIN": bin_string });
244 } else {
245 eprintln!(
246 "warning: could not resolve engram binary; MCP config has no ENGRAM_BIN env — set it manually before starting Claude Code"
247 );
248 }
249 let document = serde_json::json!({
250 "mcpServers": {
251 "engram": server_entry,
252 }
253 });
254 let serialized = serde_json::to_string_pretty(&document)
255 .map_err(|error| CoreError::InitFailed(format!("failed to serialize mcp json: {error}")))?;
256 let mcp_path = project_dir.join(MCP_JSON_FILENAME);
257 fs::write(&mcp_path, serialized)
258 .map_err(|error| CoreError::InitFailed(format!("failed to write .mcp.json: {error}")))?;
259 println!("MCP config written to {}", mcp_path.display());
260 Ok(())
261}
262
263fn write_gitignore(project_dir: &Path) -> Result<(), CoreError> {
264 let gitignore_path = project_dir.join(GITIGNORE_FILENAME);
265 let existing = match fs::read_to_string(&gitignore_path) {
266 Ok(content) => content,
267 Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
268 Err(error) => {
269 return Err(CoreError::InitFailed(format!(
270 "failed to read .gitignore: {error}"
271 )));
272 }
273 };
274 if gitignore_contains_marker(&existing) {
275 return Ok(());
276 }
277 let mut updated = existing;
278 if !updated.is_empty() && !updated.ends_with('\n') {
279 updated.push('\n');
280 }
281 updated.push_str(GITIGNORE_MARKER);
282 updated.push('\n');
283 fs::write(&gitignore_path, updated)
284 .map_err(|error| CoreError::InitFailed(format!("failed to write .gitignore: {error}")))
285}
286
287fn gitignore_contains_marker(content: &str) -> bool {
288 content.lines().any(|line| {
289 line.trim() == GITIGNORE_MARKER.trim_end_matches('/') || line.trim() == GITIGNORE_MARKER
290 })
291}
292
293fn print_completion_info(project_dir: &Path) {
294 println!();
295 println!("Engram initialized at {}", project_dir.display());
296 println!();
297 println!("Set API keys via environment variables:");
298 println!(" export ENGRAM_VOYAGE_API_KEY=your-voyage-key");
299 println!(" export ENGRAM_OPENAI_API_KEY=your-openai-key");
300 println!();
301 println!("MCP config written to .mcp.json; Claude Code will pick it up automatically.");
302 println!("Add to your project's CLAUDE.md:");
303 println!(" Engram memory system: .engram/AGENT.md");
304}