Add simple Vestige update flow

This commit is contained in:
Sam Valladares 2026-05-01 13:33:54 -05:00
parent fb250207a3
commit c77b05078c
16 changed files with 733 additions and 65 deletions

View file

@ -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
<details>
<summary>Other platforms & install methods</summary>
**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:

BIN
assets/vestige-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -12,7 +12,7 @@ use std::sync::{Arc, OnceLock};
use anyhow::Context;
use chrono::{NaiveDate, Utc};
use clap::{Parser, Subcommand};
use clap::{Args, Parser, Subcommand};
use colored::Colorize;
use vestige_core::{IngestInput, PortableImportMode, Storage};
@ -36,6 +36,58 @@ struct Cli {
static CLI_DB_PATH: OnceLock<PathBuf> = 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<String>,
/// Model name passed to the optional Sanhedrin endpoint.
#[arg(long, value_name = "MODEL")]
sanhedrin_model: Option<String>,
/// Use a local checkout/release root containing hooks/ and agents/.
#[arg(long, value_name = "DIR", hide = true)]
src: Option<PathBuf>,
}
#[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<String>,
#[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<String> {
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<String> {
match version {
Some(version) => Ok(normalize_release_tag(version)),
None => latest_release_tag(),
}
}
fn find_sandwich_source_root(root: &Path) -> Option<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<String>,
install_dir: Option<PathBuf>,
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);
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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"

View file

@ -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.

View file

@ -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
```

View file

@ -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
```

View file

@ -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);

View file

@ -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

View file

@ -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",

View file

@ -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
#

21
server.json Normal file
View file

@ -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"
}
}
]
}