From 93697b7a3735fb58a4c22a5aa07bff2459da712f Mon Sep 17 00:00:00 2001 From: HackPoint <6758579+HackPoint@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:58:53 +0300 Subject: [PATCH] fix(setup): register MCP sidecars from a stable path off the DMG When Lumen runs directly from the mounted DMG (or a Gatekeeper App Translocation mount), find_binary() resolves the lumen-mcp/lumen-tok sidecars to their location *inside* that ephemeral mount and writes those absolute paths into ~/.claude.json. Once the DMG is ejected the paths vanish, so Claude Code fails to start the server with: ENOENT: no such file or directory, posix_spawn '/Volumes/dmg.XXXXX/Lumen.app/Contents/MacOS/lumen-mcp' Add stable_binary(): when the resolved sidecar lives on an ephemeral mount (/Volumes/... or .../AppTranslocation/...), copy it into ~/Library/Application Support/io.speedata.lumen/bin/ and register that stable path instead. Copying the standalone Mach-O preserves its embedded signature so it still launches. Non-DMG installs are unaffected. Bump to v1.0.1. --- CHANGELOG.md | 10 +++++++ Cargo.lock | 2 +- lumenator/package.json | 2 +- lumenator/src-tauri/Cargo.toml | 2 +- lumenator/src-tauri/src/setup.rs | 45 +++++++++++++++++++++++++++-- lumenator/src-tauri/tauri.conf.json | 2 +- 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d113df3..59629bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.0.1] — 2026-06-17 + +### Fixes +- fix(setup): register MCP sidecars from a stable path when the app runs from a + DMG or App Translocation mount. Previously the `lumen-mcp`/`lumen-tok` paths + written to `~/.claude.json` pointed inside the ejected `/Volumes/…` mount, so + Claude Code failed to launch the server with + `ENOENT … posix_spawn '/Volumes/…/lumen-mcp'`. The sidecars are now copied to + `~/Library/Application Support/io.speedata.lumen/bin/` and registered there. + ## [1.0.0] — 2026-06-09 ### Features diff --git a/Cargo.lock b/Cargo.lock index b8f8885..29d4f1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "Lumen" -version = "1.0.0" +version = "1.0.1" dependencies = [ "dirs 6.0.0", "futures-util", diff --git a/lumenator/package.json b/lumenator/package.json index 0d57eb8..ee0872a 100644 --- a/lumenator/package.json +++ b/lumenator/package.json @@ -1,6 +1,6 @@ { "name": "lumenator", - "version": "1.0.0", + "version": "1.0.1", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/lumenator/src-tauri/Cargo.toml b/lumenator/src-tauri/Cargo.toml index 30d1a40..36d2f04 100644 --- a/lumenator/src-tauri/Cargo.toml +++ b/lumenator/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Lumen" -version = "1.0.0" +version = "1.0.1" description = "macOS menu-bar app for Claude Code — live context gauge, cost, and optimizer" authors = ["HackPoint"] license = "MIT" diff --git a/lumenator/src-tauri/src/setup.rs b/lumenator/src-tauri/src/setup.rs index c021d86..c0f73ae 100644 --- a/lumenator/src-tauri/src/setup.rs +++ b/lumenator/src-tauri/src/setup.rs @@ -134,6 +134,47 @@ fn find_binary(name: &str) -> Option { None } +fn app_support_dir() -> PathBuf { + home().join("Library/Application Support/io.speedata.lumen") +} + +// macOS runs unsigned / quarantined apps from ephemeral, read-only locations: +// a mounted DMG (`/Volumes/…`) or a Gatekeeper App Translocation mount +// (`/private/var/folders/…/AppTranslocation/…`). Both disappear when the DMG is +// ejected or the app is moved, leaving the MCP `command` path we recorded in +// ~/.claude.json dangling — Claude Code then fails with +// `ENOENT … posix_spawn '/Volumes/…/lumen-mcp'`. +fn is_ephemeral_path(p: &std::path::Path) -> bool { + let s = p.to_string_lossy(); + s.starts_with("/Volumes/") || s.contains("/AppTranslocation/") +} + +// Resolve a sidecar binary to a path that survives the DMG being ejected. If the +// bundled binary lives on an ephemeral mount, copy it into a stable, user-writable +// location (`…/io.speedata.lumen/bin/`) and return that; otherwise return +// the resolved path unchanged. Copying the standalone Mach-O preserves its +// embedded code signature, so it still launches. +fn stable_binary(name: &str) -> Option { + let found = find_binary(name)?; + if !is_ephemeral_path(&found) { + return Some(found); + } + let bin_dir = app_support_dir().join("bin"); + if std::fs::create_dir_all(&bin_dir).is_err() { + return Some(found); // fall back rather than failing setup outright + } + let dest = bin_dir.join(name); + if std::fs::copy(&found, &dest).is_err() { + return Some(found); + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755)); + } + Some(dest) +} + fn db_path() -> String { std::env::var("LUMEN_DB").unwrap_or_else(|_| { home() @@ -537,8 +578,8 @@ fn run_setup() -> Vec { } // 2. Resolve binary paths - let mcp_bin = find_binary("lumen-mcp"); - let tok_bin = find_binary("lumen-tok"); + let mcp_bin = stable_binary("lumen-mcp"); + let tok_bin = stable_binary("lumen-tok"); let mcp_str = mcp_bin .as_ref() diff --git a/lumenator/src-tauri/tauri.conf.json b/lumenator/src-tauri/tauri.conf.json index b9e59ea..9f7b8ff 100644 --- a/lumenator/src-tauri/tauri.conf.json +++ b/lumenator/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Lumen", - "version": "1.0.0", + "version": "1.0.1", "identifier": "io.speedata.lumen", "build": { "frontendDist": "../dist/lumenator/browser",