diff --git a/crates/neuromesh-cli/src/commands/mod.rs b/crates/neuromesh-cli/src/commands/mod.rs index 59e592a..bb7e557 100644 --- a/crates/neuromesh-cli/src/commands/mod.rs +++ b/crates/neuromesh-cli/src/commands/mod.rs @@ -128,13 +128,15 @@ pub fn print_file_cap(report: &neuromesh_index::ScanReport, indent: &str) { } } +/// Starts the background index of `dir`; returns whether it will be indexed +/// (`false` when the path is rejected — the server then answers from nothing). pub fn spawn_live_sync( graph: std::sync::Arc, dir: std::path::PathBuf, pid: ProjectId, cap: FileCapArg, explicit: bool, -) { +) -> bool { // The one choke point where the CLI starts a scan. A path the user gave us // only has to be safe; a path we guessed also has to look like a project, // which is what stops a server launched with no workspace from indexing @@ -150,7 +152,7 @@ pub fn spawn_live_sync( "NeuroMesh: pass a project path (`neuromesh mcp `) or set NEUROMESH_WORKSPACE" ); graph.mark_index_ready(); - return; + return false; } // Marked before the scan thread is scheduled: until then the gate still // said Ready over an empty graph, and a first question got "no seed". @@ -174,6 +176,7 @@ pub fn spawn_live_sync( let _ = graph.save_persisted(&dir); } }); + true } #[cfg(test)] diff --git a/crates/neuromesh-cli/src/commands/monitor.rs b/crates/neuromesh-cli/src/commands/monitor.rs index d8ac861..10f8fda 100644 --- a/crates/neuromesh-cli/src/commands/monitor.rs +++ b/crates/neuromesh-cli/src/commands/monitor.rs @@ -42,7 +42,7 @@ pub async fn execute(port_override: Option, cap: FileCapArg) -> Result<()> let bg_pid = project_id.clone(); // `monitor` has no workspace argument; the root is the current directory, // so it is a guess and has to look like a project. - super::spawn_live_sync(bg_graph, bg_dir, bg_pid, cap, false); + let _ = super::spawn_live_sync(bg_graph, bg_dir, bg_pid, cap, false); let state = AppState::new(config, graph, memory_db, provider); state.attach_graph_proxy_if_configured().await; diff --git a/crates/neuromesh-cli/src/main.rs b/crates/neuromesh-cli/src/main.rs index 8bc2c14..e663d3c 100644 --- a/crates/neuromesh-cli/src/main.rs +++ b/crates/neuromesh-cli/src/main.rs @@ -287,7 +287,7 @@ async fn async_main(command: &str, args: &[String]) -> Result<()> { { graph.mark_index_loading(); } - commands::spawn_live_sync( + let indexable = commands::spawn_live_sync( graph.clone(), current_dir.clone(), project_id.clone(), @@ -316,16 +316,27 @@ async fn async_main(command: &str, args: &[String]) -> Result<()> { }); // Separate task: only open a browser once we know the real // bound port, but this never delays the stdio server below. + // A workspace that will not be indexed gets no tab: its + // dashboard would show an empty graph ("no modules"). let workspace_for_ui = current_dir.clone(); tokio::spawn(async move { if let Ok(actual_port) = port_rx.await { - maybe_open_ui_first_run(&workspace_for_ui, actual_port); + if indexable { + maybe_open_ui_first_run(&workspace_for_ui, actual_port); + } } }); } let server = neuromesh_mcp::McpServer::new(handler); server.run_stdio().await?; + // The client closed stdin: the session is over. Dropping the + // runtime would wait on the file watcher's blocking task forever, + // leaving an orphan process per closed client (a probe that closed + // stdin found the server still alive after 10 minutes). + use std::io::Write; + let _ = std::io::stdout().flush(); + std::process::exit(0); } "--help" | "-h" | "help" => { print_help();