From c77b05078ced73126585518a74575f875063e6a7 Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Fri, 1 May 2026 13:33:54 -0500 Subject: [PATCH] Add simple Vestige update flow --- README.md | 28 +- assets/vestige-icon.png | Bin 0 -> 40428 bytes crates/vestige-mcp/src/bin/cli.rs | 653 +++++++++++++++++++++++++- docs/COGNITIVE_SANDWICH.md | 21 +- docs/CONFIGURATION.md | 16 + docs/INSTALL-INTEL-MAC.md | 5 +- docs/blog/xcode-memory.md | 6 +- docs/integrations/xcode.md | 5 +- docs/launch/demo-script.md | 11 +- docs/launch/reddit-cross-reference.md | 4 +- docs/launch/show-hn.md | 3 +- packages/vestige-init/bin/init.js | 7 +- packages/vestige-mcp-npm/README.md | 11 + packages/vestige-mcp-npm/package.json | 3 +- scripts/install-sandwich.sh | 4 +- server.json | 21 + 16 files changed, 733 insertions(+), 65 deletions(-) create mode 100644 assets/vestige-icon.png create mode 100644 server.json diff --git a/README.md b/README.md index 334c3b6..84a2dcd 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ v2.1.1 focuses on the biggest post-launch ask: move memories between machines wi v2.1.0 adds an opt-in Claude Code hook harness around the existing Vestige MCP server. The MCP tool surface and database schema stay backward compatible, while preflight hooks can inject trusted memory context before Claude answers. The heavyweight Sanhedrin verifier is optional and can be enabled separately. - **Optional Sanhedrin Executioner.** The post-response verifier is off by default. Users can enable it with an OpenAI-compatible endpoint on x86/Linux/Intel Mac, or add `--with-launchd` on Apple Silicon to run the local MLX Qwen backend. -- **One-command Cognitive Sandwich installer.** `scripts/install-sandwich.sh` stages hook files and agents by default, removes old Vestige hook wiring, and leaves all Claude Code hook layers plus the 19 GB model path opt-in. +- **One-command Cognitive Sandwich installer.** `vestige sandwich install` stages hook files and agents by default, removes old Vestige hook wiring, and leaves all Claude Code hook layers plus the 19 GB model path opt-in. - **Pulse hook backed by `/api/changelog`.** Fresh dream and connection events can be injected into the next Claude Code prompt context without blocking the prompt. - **`VESTIGE_DATA_DIR` support.** `--data-dir` now has an env-var fallback, tilde expansion, secure directory creation, and clear precedence docs. - **NPM release wrapper fixed.** `vestige-mcp-server@2.1.0` now downloads binaries from the matching `v2.1.0` GitHub release tag instead of an old hardcoded release. @@ -103,15 +103,14 @@ Based on [Anderson et al. 2025](https://www.nature.com/articles/s41583-025-00929 ## Quick Start ```bash -# 1. Install (macOS Apple Silicon) -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +# 1. Install +npm install -g vestige-mcp-server@latest # 2. Connect to Claude Code claude mcp add vestige vestige-mcp -s user # Or connect to Codex -codex mcp add vestige -- /usr/local/bin/vestige-mcp +codex mcp add vestige -- vestige-mcp # 3. Test it # "Remember that I prefer TypeScript over JavaScript" @@ -123,18 +122,25 @@ codex mcp add vestige -- /usr/local/bin/vestige-mcp
Other platforms & install methods -**Linux (x86_64):** +**Updating an existing install:** ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-x86_64-unknown-linux-gnu.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +vestige update +``` + +`vestige update` updates the binaries and refreshes Cognitive Sandwich companion +files while keeping every hook layer disabled by default. Use +`vestige update --no-sandwich` if you only want the binaries. + +**macOS/Linux manual binary install:** +```bash +vestige update --install-dir /usr/local/bin ``` **macOS (Intel):** Microsoft is discontinuing x86_64 macOS prebuilts after ONNX Runtime v1.23.0, so Vestige's Intel Mac build links dynamically against a Homebrew-installed ONNX Runtime via the `ort-dynamic` feature. Install with: ```bash brew install onnxruntime -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-x86_64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server@latest echo 'export ORT_DYLIB_PATH="'"$(brew --prefix onnxruntime)"'/lib/libonnxruntime.dylib"' >> ~/.zshrc source ~/.zshrc claude mcp add vestige vestige-mcp -s user @@ -163,7 +169,7 @@ Open `%APPDATA%\Claude\claude_desktop_config.json` and point Claude Desktop at t } ``` -If Claude Desktop cannot find `vestige-mcp`, run `where vestige-mcp` in PowerShell and use the exact `.cmd` path it prints as `command`. Example: `"C:\\Users\\you\\AppData\\Roaming\\npm\\vestige-mcp.cmd"`. Reopen Claude Desktop after saving. Once v2.1.0 is installed, future binary updates can run with `vestige update`. +If Claude Desktop cannot find `vestige-mcp`, run `where vestige-mcp` in PowerShell and use the exact `.cmd` path it prints as `command`. Example: `"C:\\Users\\you\\AppData\\Roaming\\npm\\vestige-mcp.cmd"`. Reopen Claude Desktop after saving. Future binary and companion-file updates can run with `vestige update`. **Windows source build:** Prebuilt binaries ship but `usearch 2.24.0` hit an MSVC compile break ([usearch#746](https://github.com/unum-cloud/usearch/issues/746)); we've pinned `=2.23.0` until upstream fixes it. Source builds work with: diff --git a/assets/vestige-icon.png b/assets/vestige-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2f7deaa67430e2c3a3d09176b10c61eaf51f864f GIT binary patch literal 40428 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 = OnceLock::new(); +#[derive(Debug, Clone, Default, Args)] +struct SandwichInstallOptions { + /// Overwrite existing staged Vestige hook and agent files. + #[arg(long)] + force: bool, + + /// Wire optional UserPromptSubmit preflight hooks. + #[arg(long)] + enable_preflight: bool, + + /// Wire both optional preflight hooks and the optional Sanhedrin verifier. + #[arg(long)] + enable_sandwich: bool, + + /// Wire optional Sanhedrin Stop hook. + #[arg(long)] + enable_sanhedrin: bool, + + /// On Apple Silicon, auto-start the local MLX Sanhedrin backend. + #[arg(long)] + with_launchd: bool, + + /// Also stage the large memory-loader hook file. + #[arg(long)] + include_memory_loader: bool, + + /// OpenAI-compatible chat completions endpoint for optional Sanhedrin. + #[arg(long, value_name = "URL")] + sanhedrin_endpoint: Option, + + /// Model name passed to the optional Sanhedrin endpoint. + #[arg(long, value_name = "MODEL")] + sanhedrin_model: Option, + + /// Use a local checkout/release root containing hooks/ and agents/. + #[arg(long, value_name = "DIR", hide = true)] + src: Option, +} + +#[derive(Subcommand)] +enum SandwichCommands { + /// Install/update Cognitive Sandwich companion files without enabling hooks by default. + Install { + /// Install files from a specific release tag instead of latest. + #[arg(long)] + version: Option, + + #[command(flatten)] + options: SandwichInstallOptions, + }, +} + #[derive(Subcommand)] enum Commands { /// Show memory statistics @@ -68,6 +120,19 @@ enum Commands { /// Print what would be updated without changing files #[arg(long)] dry_run: bool, + + /// Skip Cognitive Sandwich companion file update and legacy hook cleanup. + #[arg(long)] + no_sandwich: bool, + + #[command(flatten)] + sandwich: SandwichInstallOptions, + }, + + /// Manage optional Claude Code Cognitive Sandwich companion files. + Sandwich { + #[command(subcommand)] + command: SandwichCommands, }, /// Restore memories from backup file @@ -191,7 +256,14 @@ fn main() -> anyhow::Result<()> { version, install_dir, dry_run, - } => run_update(version, install_dir, dry_run), + no_sandwich, + sandwich, + } => run_update(version, install_dir, dry_run, no_sandwich, sandwich), + Commands::Sandwich { command } => match command { + SandwichCommands::Install { version, options } => { + run_sandwich_install(version.as_deref(), &options) + } + }, Commands::Restore { file } => run_restore(file), Commands::Backup { output } => run_backup(output), Commands::Export { @@ -292,11 +364,7 @@ fn release_download_url(asset: ReleaseAsset, version: Option<&str>) -> String { let archive_name = format!("vestige-mcp-{}.{}", asset.target, asset.archive_ext); match version { Some(version) => { - let tag = if version.starts_with('v') { - version.to_string() - } else { - format!("v{}", version) - }; + let tag = normalize_release_tag(version); format!( "https://github.com/samvallad33/vestige/releases/download/{}/{}", tag, archive_name @@ -309,6 +377,506 @@ fn release_download_url(asset: ReleaseAsset, version: Option<&str>) -> String { } } +fn normalize_release_tag(version: &str) -> String { + if version.starts_with('v') { + version.to_string() + } else { + format!("v{}", version) + } +} + +fn source_archive_url(tag: &str) -> String { + format!( + "https://github.com/samvallad33/vestige/archive/refs/tags/{}.tar.gz", + tag + ) +} + +fn download_file(url: &str, output: &Path, action: &str) -> anyhow::Result<()> { + run_command( + Command::new("curl") + .arg("-fsSL") + .arg("-A") + .arg("vestige-cli") + .arg(url) + .arg("-o") + .arg(output), + action, + ) +} + +fn latest_release_tag() -> anyhow::Result { + let temp_dir = UpdateTempDir::create()?; + let metadata_path = temp_dir.path.join("latest-release.json"); + download_file( + "https://api.github.com/repos/samvallad33/vestige/releases/latest", + &metadata_path, + "checking latest Vestige release", + )?; + let file = fs::File::open(&metadata_path)?; + let metadata: serde_json::Value = + serde_json::from_reader(file).context("failed to parse latest Vestige release metadata")?; + metadata + .get("tag_name") + .and_then(|tag| tag.as_str()) + .map(|tag| tag.to_string()) + .ok_or_else(|| anyhow::anyhow!("latest Vestige release metadata did not include tag_name")) +} + +fn release_tag_for_source(version: Option<&str>) -> anyhow::Result { + match version { + Some(version) => Ok(normalize_release_tag(version)), + None => latest_release_tag(), + } +} + +fn find_sandwich_source_root(root: &Path) -> Option { + if root.join("hooks").is_dir() && root.join("agents").is_dir() { + return Some(root.to_path_buf()); + } + + let entries = fs::read_dir(root).ok()?; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("hooks").is_dir() && path.join("agents").is_dir() { + return Some(path); + } + } + + None +} + +fn download_sandwich_source(version: Option<&str>, output_dir: &Path) -> anyhow::Result { + let tag = release_tag_for_source(version)?; + let archive_path = output_dir.join(format!("vestige-source-{}.tar.gz", tag)); + let url = source_archive_url(&tag); + + println!("{}: {}", "Sandwich source".white().bold(), tag); + download_file(&url, &archive_path, "downloading Vestige source archive")?; + extract_archive(&archive_path, output_dir, "tar.gz")?; + find_sandwich_source_root(output_dir).ok_or_else(|| { + anyhow::anyhow!("Vestige source archive did not contain hooks/ and agents/ directories") + }) +} + +fn home_dir() -> anyhow::Result { + directories::BaseDirs::new() + .map(|dirs| dirs.home_dir().to_path_buf()) + .ok_or_else(|| anyhow::anyhow!("failed to locate home directory")) +} + +fn is_vestige_hook_command(command: &str) -> bool { + const NEEDLES: &[&str] = &[ + "synthesis-preflight.sh", + "cwd-state-injector.sh", + "vestige-pulse-daemon.sh", + "preflight-swarm.sh", + "load-all-memory.sh", + "veto-detector.sh", + "sanhedrin.sh", + "synthesis-stop-validator.sh", + "synthesis-gate.sh", + ]; + NEEDLES.iter().any(|needle| command.contains(needle)) +} + +fn scrub_vestige_hooks(settings: &mut serde_json::Value) { + let Some(hooks) = settings + .get_mut("hooks") + .and_then(|hooks| hooks.as_object_mut()) + else { + return; + }; + + for event_name in ["UserPromptSubmit", "Stop"] { + let Some(groups) = hooks + .get_mut(event_name) + .and_then(|groups| groups.as_array_mut()) + else { + continue; + }; + + for group in groups.iter_mut() { + if let Some(commands) = group + .get_mut("hooks") + .and_then(|hooks| hooks.as_array_mut()) + { + commands.retain(|hook| { + !hook + .get("command") + .and_then(|command| command.as_str()) + .is_some_and(is_vestige_hook_command) + }); + } + } + + groups.retain(|group| { + group + .get("hooks") + .and_then(|hooks| hooks.as_array()) + .is_some_and(|hooks| !hooks.is_empty()) + }); + } + + hooks.retain(|_, value| match value { + serde_json::Value::Array(items) => !items.is_empty(), + serde_json::Value::Object(items) => !items.is_empty(), + serde_json::Value::Null => false, + _ => true, + }); + + if hooks.is_empty() + && let Some(root) = settings.as_object_mut() + { + root.remove("hooks"); + } +} + +fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) { + match (base, overlay) { + (serde_json::Value::Object(base), serde_json::Value::Object(overlay)) => { + for (key, value) in overlay { + match base.get_mut(&key) { + Some(existing) => merge_json(existing, value), + None => { + base.insert(key, value); + } + } + } + } + (base, overlay) => *base = overlay, + } +} + +fn merge_settings_fragment( + settings: &mut serde_json::Value, + fragment_path: &Path, +) -> anyhow::Result<()> { + let file = fs::File::open(fragment_path) + .with_context(|| format!("failed to open {}", fragment_path.display()))?; + let fragment: serde_json::Value = serde_json::from_reader(file) + .with_context(|| format!("failed to parse {}", fragment_path.display()))?; + merge_json(settings, fragment); + Ok(()) +} + +fn copy_companion_files( + source_dir: &Path, + destination_dir: &Path, + allowed_extensions: &[&str], + _mode: u32, + options: &SandwichInstallOptions, +) -> anyhow::Result<(usize, usize)> { + fs::create_dir_all(destination_dir)?; + let mut copied = 0; + let mut skipped = 0; + + for entry in fs::read_dir(source_dir) + .with_context(|| format!("failed to read {}", source_dir.display()))? + { + let entry = entry?; + let source = entry.path(); + if !source.is_file() { + continue; + } + + let extension = source + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + if !allowed_extensions.contains(&extension) { + continue; + } + + let Some(file_name) = source.file_name() else { + continue; + }; + if file_name.to_string_lossy() == "load-all-memory.sh" && !options.include_memory_loader { + continue; + } + + let destination = destination_dir.join(file_name); + if destination.exists() && !options.force { + skipped += 1; + continue; + } + + fs::copy(&source, &destination).with_context(|| { + format!( + "failed to copy {} to {}", + source.display(), + destination.display() + ) + })?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&destination)?.permissions(); + perms.set_mode(_mode); + fs::set_permissions(&destination, perms)?; + } + + copied += 1; + } + + Ok((copied, skipped)) +} + +fn quote_shell_env(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\\''")) +} + +fn write_sanhedrin_env( + hooks_dir: &Path, + endpoint: &str, + model: &str, + dashboard_port: &str, +) -> anyhow::Result<()> { + let env_path = hooks_dir.join("vestige-sanhedrin.env"); + let contents = format!( + "VESTIGE_SANHEDRIN_ENABLED=1\nVESTIGE_SANHEDRIN_ENDPOINT={}\nVESTIGE_SANHEDRIN_MODEL={}\nVESTIGE_DASHBOARD_PORT={}\n", + quote_shell_env(endpoint), + quote_shell_env(model), + quote_shell_env(dashboard_port) + ); + fs::write(&env_path, contents)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&env_path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&env_path, perms)?; + } + + println!("{}: {}", "Sanhedrin env".white().bold(), env_path.display()); + Ok(()) +} + +fn install_launchd_job(source_root: &Path, home: &Path, model: &str) -> anyhow::Result<()> { + let launchd_dir = home.join("Library").join("LaunchAgents"); + fs::create_dir_all(&launchd_dir)?; + + let template_path = source_root + .join("launchd") + .join("com.vestige.mlx-server.plist.template"); + let template = fs::read_to_string(&template_path) + .with_context(|| format!("failed to read {}", template_path.display()))?; + let rendered = template + .replace("__HOME__", &home.display().to_string()) + .replace("__MODEL__", model); + + let plist = launchd_dir.join("com.vestige.mlx-server.plist"); + fs::write(&plist, rendered)?; + let _ = Command::new("launchctl").arg("unload").arg(&plist).status(); + run_command( + Command::new("launchctl").arg("load").arg(&plist), + "loading Vestige MLX launchd job", + )?; + println!("{}: {}", "launchd".white().bold(), plist.display()); + Ok(()) +} + +fn remove_legacy_launchd_job(home: &Path) { + if env::consts::OS != "macos" { + return; + } + + let plist = home + .join("Library") + .join("LaunchAgents") + .join("com.vestige.mlx-server.plist"); + if plist.exists() { + let _ = Command::new("launchctl").arg("unload").arg(&plist).status(); + if fs::remove_file(&plist).is_ok() { + println!( + "{}: removed old Sanhedrin launchd job", + "launchd".white().bold() + ); + } + } +} + +fn install_sandwich_from_source( + source_root: &Path, + options: &SandwichInstallOptions, +) -> anyhow::Result<()> { + let home = home_dir()?; + let claude_dir = home.join(".claude"); + let hooks_dir = claude_dir.join("hooks"); + let agents_dir = claude_dir.join("agents"); + let settings_path = claude_dir.join("settings.json"); + let source_root = + find_sandwich_source_root(source_root).unwrap_or_else(|| source_root.to_path_buf()); + + if !source_root.join("hooks").is_dir() || !source_root.join("agents").is_dir() { + anyhow::bail!( + "Cognitive Sandwich source missing hooks/ or agents/: {}", + source_root.display() + ); + } + + let enable_preflight = options.enable_preflight || options.enable_sandwich; + let mut enable_sanhedrin = + options.enable_sanhedrin || options.enable_sandwich || options.with_launchd; + let mut with_launchd = options.with_launchd; + + if with_launchd && (env::consts::OS != "macos" || env::consts::ARCH != "aarch64") { + println!( + "{}", + "--with-launchd is Apple Silicon only; using endpoint-backed Sanhedrin instead." + .yellow() + ); + with_launchd = false; + enable_sanhedrin = true; + } + + fs::create_dir_all(&claude_dir)?; + let (hooks_copied, hooks_skipped) = copy_companion_files( + &source_root.join("hooks"), + &hooks_dir, + &["sh", "py"], + 0o755, + options, + )?; + let (agents_copied, agents_skipped) = copy_companion_files( + &source_root.join("agents"), + &agents_dir, + &["md"], + 0o644, + options, + )?; + + println!( + "{}: {} installed, {} skipped", + "Hooks".white().bold(), + hooks_copied, + hooks_skipped + ); + println!( + "{}: {} installed, {} skipped", + "Agents".white().bold(), + agents_copied, + agents_skipped + ); + + if !with_launchd { + remove_legacy_launchd_job(&home); + } + + let dashboard_port = env::var("VESTIGE_DASHBOARD_PORT").unwrap_or_else(|_| "3927".to_string()); + let endpoint = options + .sanhedrin_endpoint + .clone() + .or_else(|| env::var("VESTIGE_SANHEDRIN_ENDPOINT").ok()) + .or_else(|| env::var("MLX_ENDPOINT").ok()) + .unwrap_or_else(|| "http://127.0.0.1:8080/v1/chat/completions".to_string()) + .trim_end_matches('/') + .to_string(); + let model = options + .sanhedrin_model + .clone() + .or_else(|| env::var("VESTIGE_SANHEDRIN_MODEL").ok()) + .or_else(|| env::var("VESTIGE_SANDWICH_MODEL").ok()) + .unwrap_or_else(|| "mlx-community/Qwen3.6-35B-A3B-4bit".to_string()); + + if enable_sanhedrin { + write_sanhedrin_env(&hooks_dir, &endpoint, &model, &dashboard_port)?; + } + if with_launchd { + install_launchd_job(&source_root, &home, &model)?; + } + + if !settings_path.exists() { + fs::write(&settings_path, "{}\n")?; + } + let backup_path = claude_dir.join("settings.json.bak.pre-sandwich"); + if !backup_path.exists() { + fs::copy(&settings_path, &backup_path)?; + } + + let settings_file = fs::File::open(&settings_path)?; + let mut settings: serde_json::Value = + serde_json::from_reader(settings_file).unwrap_or_else(|_| serde_json::json!({})); + scrub_vestige_hooks(&mut settings); + + if enable_preflight { + merge_settings_fragment( + &mut settings, + &source_root + .join("hooks") + .join("settings.preflight.fragment.json"), + )?; + } + if enable_sanhedrin { + merge_settings_fragment( + &mut settings, + &source_root + .join("hooks") + .join("settings.sanhedrin.fragment.json"), + )?; + } + + let mut settings_file = fs::File::create(&settings_path)?; + serde_json::to_writer_pretty(&mut settings_file, &settings)?; + writeln!(settings_file)?; + + if enable_preflight || enable_sanhedrin { + let mut layers = Vec::new(); + if enable_preflight { + layers.push("preflight"); + } + if enable_sanhedrin { + layers.push("sanhedrin"); + } + println!( + "{}: enabled optional layer(s): {}", + "Settings".white().bold(), + layers.join(", ") + ); + } else { + println!( + "{}: no Vestige Claude Code hooks enabled by default", + "Settings".white().bold() + ); + } + + Ok(()) +} + +fn run_sandwich_install( + version: Option<&str>, + options: &SandwichInstallOptions, +) -> anyhow::Result<()> { + println!( + "{}", + "=== Vestige Cognitive Sandwich Install ===".cyan().bold() + ); + println!(); + + if let Some(source_root) = &options.src { + install_sandwich_from_source(source_root, options)?; + } else { + let temp_dir = UpdateTempDir::create()?; + let source_root = download_sandwich_source(version, &temp_dir.path)?; + install_sandwich_from_source(&source_root, options)?; + } + + println!(); + let optional_layers_enabled = options.enable_preflight + || options.enable_sandwich + || options.enable_sanhedrin + || options.with_launchd; + let message = if optional_layers_enabled { + "Cognitive Sandwich files updated. Restart Claude Code to use enabled optional hooks." + } else { + "Cognitive Sandwich files updated. No hooks enabled; no automatic model calls." + }; + println!("{}", message.green().bold()); + Ok(()) +} + fn run_command(command: &mut Command, action: &str) -> anyhow::Result<()> { let status = command .status() @@ -400,6 +968,8 @@ fn run_update( version: Option, install_dir: Option, dry_run: bool, + no_sandwich: bool, + sandwich: SandwichInstallOptions, ) -> anyhow::Result<()> { println!("{}", "=== Vestige Update ===".cyan().bold()); println!(); @@ -453,14 +1023,7 @@ fn run_update( println!(); println!("{}", "Downloading release archive...".cyan()); - run_command( - Command::new("curl") - .arg("-fL") - .arg(&url) - .arg("-o") - .arg(&archive_path), - "downloading Vestige release archive with curl", - )?; + download_file(&url, &archive_path, "downloading Vestige release archive")?; println!("{}", "Extracting release archive...".cyan()); extract_archive(&archive_path, &temp_dir.path, asset.archive_ext)?; @@ -491,11 +1054,25 @@ fn run_update( println!( "{}", - "Update complete. Restart your MCP client to pick up the new binary." + "Binary update complete. Restart your MCP client to pick up the new binary." .green() .bold() ); + if no_sandwich { + println!( + "{}", + "Skipped Cognitive Sandwich companion update (--no-sandwich).".yellow() + ); + } else { + println!(); + println!( + "{}", + "Updating Cognitive Sandwich companion files...".cyan() + ); + run_sandwich_install(version.as_deref(), &sandwich)?; + } + Ok(()) } @@ -1800,4 +2377,48 @@ mod tests { "https://github.com/samvallad33/vestige/releases/download/v2.1.0/vestige-mcp-aarch64-apple-darwin.tar.gz" ); } + + #[test] + fn source_archive_url_uses_normalized_tag() { + assert_eq!(normalize_release_tag("2.1.1"), "v2.1.1"); + assert_eq!(normalize_release_tag("v2.1.1"), "v2.1.1"); + assert_eq!( + source_archive_url("v2.1.1"), + "https://github.com/samvallad33/vestige/archive/refs/tags/v2.1.1.tar.gz" + ); + } + + #[test] + fn scrub_vestige_hooks_removes_only_vestige_commands() { + let mut settings = serde_json::json!({ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { "type": "command", "command": "/tmp/synthesis-preflight.sh" }, + { "type": "command", "command": "/tmp/custom-user-hook.sh" } + ] + } + ], + "Stop": [ + { + "hooks": [ + { "type": "command", "command": "/tmp/sanhedrin.sh" } + ] + } + ] + }, + "other": true + }); + + scrub_vestige_hooks(&mut settings); + + let user_hooks = settings["hooks"]["UserPromptSubmit"][0]["hooks"] + .as_array() + .unwrap(); + assert_eq!(user_hooks.len(), 1); + assert_eq!(user_hooks[0]["command"], "/tmp/custom-user-hook.sh"); + assert!(settings["hooks"].get("Stop").is_none()); + assert_eq!(settings["other"], true); + } } diff --git a/docs/COGNITIVE_SANDWICH.md b/docs/COGNITIVE_SANDWICH.md index 6a395d4..4b4661e 100644 --- a/docs/COGNITIVE_SANDWICH.md +++ b/docs/COGNITIVE_SANDWICH.md @@ -69,12 +69,17 @@ False-positive guards (added v2.1.0 after dogfood): ## Installation -### One-liner +### From an installed Vestige CLI ```bash -curl -fsSL https://raw.githubusercontent.com/samvallad33/vestige/v2.1.1/scripts/install-sandwich.sh | sh +vestige sandwich install ``` +`vestige update` also refreshes these companion files by default after it updates +the binaries. The default command does not activate any Claude Code hook. It +removes old v2.1.0 Vestige hook wiring from `~/.claude/settings.json` while +preserving unrelated user hooks. + ### From a checkout ```bash @@ -84,15 +89,13 @@ cd vestige ./scripts/check-sandwich-prereqs.sh # verify no Vestige hooks are wired by default ``` -The default command does not activate any Claude Code hook. It removes old v2.1.0 Vestige hook wiring from `~/.claude/settings.json` while preserving unrelated user hooks. - ### Optional Preflight Preflight is a separate opt-in layer. It includes `preflight-swarm.sh`, which uses `claude -p --model claude-haiku-4-5-20251001`; it is not wired by default. ```bash -./scripts/install-sandwich.sh --enable-preflight -./scripts/check-sandwich-prereqs.sh --preflight +vestige sandwich install --enable-preflight +scripts/check-sandwich-prereqs.sh --preflight ``` ### Optional Sanhedrin @@ -101,13 +104,13 @@ Sanhedrin is a separate opt-in layer. ```bash # Wire the Sanhedrin Stop hook, using the default OpenAI-compatible endpoint. -./scripts/install-sandwich.sh --enable-sanhedrin +vestige sandwich install --enable-sanhedrin # Apple Silicon only, and only if the machine has enough memory: -./scripts/install-sandwich.sh --enable-sanhedrin --with-launchd +vestige sandwich install --enable-sanhedrin --with-launchd # x86 / Linux / Intel Mac: use any OpenAI-compatible endpoint. -./scripts/install-sandwich.sh \ +vestige sandwich install \ --enable-sanhedrin \ --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions \ --sanhedrin-model=qwen2.5:14b diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8b7c773..8f5a36d 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -175,11 +175,27 @@ See [Storage Modes](STORAGE.md) for more options. vestige update ``` +This updates `vestige`, `vestige-mcp`, `vestige-restore`, and the Cognitive +Sandwich companion files. The companion refresh keeps hooks disabled by default +and cleans up old mandatory v2.1.0 hook wiring. + +**Binaries only:** +```bash +vestige update --no-sandwich +``` + **Pin to specific version:** ```bash vestige update --version v2.1.1 ``` +**Manage the optional Cognitive Sandwich layer without updating binaries:** +```bash +vestige sandwich install +vestige sandwich install --enable-preflight +vestige sandwich install --enable-sanhedrin --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions +``` + **Check your version:** ```bash vestige-mcp --version diff --git a/docs/INSTALL-INTEL-MAC.md b/docs/INSTALL-INTEL-MAC.md index ee42975..3ec02e0 100644 --- a/docs/INSTALL-INTEL-MAC.md +++ b/docs/INSTALL-INTEL-MAC.md @@ -17,9 +17,8 @@ brew install onnxruntime ## Install ```bash -# 1. Download the binary -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-x86_64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +# 1. Install the binary +npm install -g vestige-mcp-server@latest # 2. Point the binary at Homebrew's libonnxruntime echo 'export ORT_DYLIB_PATH="'"$(brew --prefix onnxruntime)"'/lib/libonnxruntime.dylib"' >> ~/.zshrc diff --git a/docs/blog/xcode-memory.md b/docs/blog/xcode-memory.md index 4277f09..8036181 100644 --- a/docs/blog/xcode-memory.md +++ b/docs/blog/xcode-memory.md @@ -20,8 +20,7 @@ It speaks MCP (Model Context Protocol), the same protocol Xcode 26.3 uses for to **Step 1:** Install Vestige ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server ``` **Step 2:** Drop one file in your project root @@ -110,8 +109,7 @@ The full setup takes 30 seconds: ```bash # Install Vestige -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server # Add to your project (run from project root) cat > .mcp.json << 'EOF' diff --git a/docs/integrations/xcode.md b/docs/integrations/xcode.md index fb22dcf..0051146 100644 --- a/docs/integrations/xcode.md +++ b/docs/integrations/xcode.md @@ -13,8 +13,7 @@ Xcode 26.3 supports [agentic coding](https://developer.apple.com/documentation/x ### 1. Install Vestige ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server@latest ``` ### 2. Add to your Xcode project @@ -27,7 +26,7 @@ cat > /path/to/your/project/.mcp.json << 'EOF' "mcpServers": { "vestige": { "type": "stdio", - "command": "/usr/local/bin/vestige-mcp", + "command": "vestige-mcp", "args": [], "env": { "PATH": "/usr/local/bin:/usr/bin:/bin" diff --git a/docs/launch/demo-script.md b/docs/launch/demo-script.md index f5444b2..8907b27 100644 --- a/docs/launch/demo-script.md +++ b/docs/launch/demo-script.md @@ -194,10 +194,10 @@ wc -l $(find /path/to/vestige/crates -name "*.rs") | tail -1 # → 77,840 total ``` -> Seventy-eight thousand lines of Rust. Seven hundred thirty-four tests. Twenty-two megabyte binary. Ships with the dashboard embedded. Install is one curl command: +> Seventy-eight thousand lines of Rust. Seven hundred thirty-four tests. Twenty-two megabyte binary. Ships with the dashboard embedded. Install is one npm command: ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +npm install -g vestige-mcp-server claude mcp add vestige vestige-mcp -s user ``` @@ -241,8 +241,7 @@ claude mcp add vestige vestige-mcp -s user ```bash # Install (macOS Apple Silicon) -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server ``` > Three binaries. The MCP server, the CLI admin tool, and a restore utility. Twenty-two megabytes total. No Docker. No Python. No node_modules. No cloud API key. @@ -389,7 +388,7 @@ vestige-mcp --version # <300ns cosine similarity (benchmarked with Criterion) # Zero cloud dependencies # Zero API keys required -# One curl command to install +# One command to install ``` > This is what I've been building for the past three months. I'm one person, I'm twenty-one years old, and I believe this is how AI memory should work — grounded in real science, running locally, open source. @@ -479,7 +478,7 @@ vestige-mcp --version - **Start from the dashboard.** The 3D graph is the hook. It's visual, it's unusual, it makes people lean in. - **Don't rush the dream sequence.** The purple wash and sequential node pulses are the most visually impressive moment. Let it breathe for 3-4 seconds. - **Say the scientists' names.** "Ebbinghaus," "Bjork," "Frey and Morris" — this signals that you've done the reading. The MCP Dev Summit audience respects depth. -- **Make eye contact during the punchline.** "One curl command. Your AI now has a brain." Look at the audience, not the screen. +- **Make eye contact during the punchline.** "One command. Your AI now has a brain." Look at the audience, not the screen. - **Own your age.** Twenty-one, solo developer, zero funding. This is an asset, not a liability. You built something that the well-funded competitors haven't. - **The dashboard is your co-presenter.** Every time Claude does something, the dashboard should be showing the corresponding event. Practice the terminal-to-browser switch until it's seamless. - **Don't apologize.** Not for bugs, not for the AGPL, not for being solo. Confident but not arrogant. The work speaks. diff --git a/docs/launch/reddit-cross-reference.md b/docs/launch/reddit-cross-reference.md index e6e918a..7ab6b62 100644 --- a/docs/launch/reddit-cross-reference.md +++ b/docs/launch/reddit-cross-reference.md @@ -88,7 +88,7 @@ Memory systems need to be SMARTER, not just bigger. That's what Vestige does — ### Install (30 seconds): ```bash # macOS Apple Silicon -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +npm install -g vestige-mcp-server sudo mv vestige-mcp /usr/local/bin/ claude mcp add vestige vestige-mcp -s user ``` @@ -162,7 +162,7 @@ The AI sees the conflict. Picks the right one. Every time. **100% local. Your data never leaves your machine.** ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +npm install -g vestige-mcp-server sudo mv vestige-mcp /usr/local/bin/ claude mcp add vestige vestige-mcp -s user ``` diff --git a/docs/launch/show-hn.md b/docs/launch/show-hn.md index 03034e0..8cc5a95 100644 --- a/docs/launch/show-hn.md +++ b/docs/launch/show-hn.md @@ -401,8 +401,7 @@ locally on your machine. **Setup (2 minutes):** ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server claude mcp add vestige vestige-mcp -s user ``` diff --git a/packages/vestige-init/bin/init.js b/packages/vestige-init/bin/init.js index 6ef6da5..2c26b55 100755 --- a/packages/vestige-init/bin/init.js +++ b/packages/vestige-init/bin/init.js @@ -280,12 +280,7 @@ function main() { console.log(''); console.log('Install manually:'); console.log(''); - console.log(' # macOS (Apple Silicon)'); - console.log(' curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz'); - console.log(' sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/'); - console.log(''); - console.log(' # Or via npm'); - console.log(' npm install -g vestige-mcp-server'); + console.log(' npm install -g vestige-mcp-server@latest'); console.log(''); console.log('Then run: npx @vestige/init'); process.exit(1); diff --git a/packages/vestige-mcp-npm/README.md b/packages/vestige-mcp-npm/README.md index 6417198..25312d2 100644 --- a/packages/vestige-mcp-npm/README.md +++ b/packages/vestige-mcp-npm/README.md @@ -12,6 +12,15 @@ npm install -g vestige-mcp-server This automatically downloads the correct binary for your platform (macOS, Linux, Windows) from GitHub releases. +Already installed? Update without copying release URLs: + +```bash +vestige update +``` + +This refreshes the binaries and Cognitive Sandwich companion files while keeping +all hooks disabled by default. + ### What gets installed | Command | Description | @@ -57,6 +66,8 @@ vestige stats # Memory statistics vestige stats --states # Cognitive state distribution vestige health # System health check vestige consolidate # Run memory maintenance cycle +vestige update # Update binaries + companion files +vestige sandwich install # Refresh optional Claude Code hook files ``` ## Features diff --git a/packages/vestige-mcp-npm/package.json b/packages/vestige-mcp-npm/package.json index 56842dd..40f0a19 100644 --- a/packages/vestige-mcp-npm/package.json +++ b/packages/vestige-mcp-npm/package.json @@ -1,6 +1,7 @@ { "name": "vestige-mcp-server", - "version": "2.1.0", + "version": "2.1.1", + "mcpName": "io.github.samvallad33/vestige", "description": "Vestige MCP Server — Cognitive memory for AI with FSRS-6, 3D dashboard, and 29 brain modules", "bin": { "vestige-mcp": "bin/vestige-mcp.js", diff --git a/scripts/install-sandwich.sh b/scripts/install-sandwich.sh index fd2b4b9..280a4c3 100755 --- a/scripts/install-sandwich.sh +++ b/scripts/install-sandwich.sh @@ -2,8 +2,8 @@ # install-sandwich.sh — One-command installer for the Vestige Cognitive Sandwich. # # Usage: -# curl -fsSL https://raw.githubusercontent.com/samvallad33/vestige/v2.1.1/scripts/install-sandwich.sh | sh -# # or, from a checkout: +# vestige sandwich install +# # or, from a checkout / source archive: # ./scripts/install-sandwich.sh [--force] [--enable-preflight] [--enable-sanhedrin] [--with-launchd] [--include-memory-loader] # ./scripts/install-sandwich.sh --enable-sanhedrin --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions --sanhedrin-model=qwen2.5:14b # diff --git a/server.json b/server.json new file mode 100644 index 0000000..284ffb5 --- /dev/null +++ b/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.samvallad33/vestige", + "title": "Vestige", + "description": "Local-first cognitive memory for AI agents. Vestige gives Claude, Cursor, Codex, VS Code, Xcode, and other MCP clients durable memory with FSRS-6 scheduling, smart ingest, SQLite storage, portable sync, and an embedded dashboard.", + "repository": { + "url": "https://github.com/samvallad33/vestige", + "source": "github" + }, + "version": "2.1.1", + "packages": [ + { + "registryType": "npm", + "identifier": "vestige-mcp-server", + "version": "2.1.1", + "transport": { + "type": "stdio" + } + } + ] +}