use anyhow::Result;
use clap::{Parser, Subcommand};
use std::net::SocketAddr;
use trusty_memory::commands::inbox_check::handle_inbox_check;
use trusty_memory::commands::migrate::{handle_migrate, MigrateTarget};
use trusty_memory::commands::note::handle_note;
use trusty_memory::commands::prompt_context::handle_prompt_context;
use trusty_memory::commands::send_message::handle_send_message;
use trusty_memory::commands::service::{handle_service, ServiceAction};
use trusty_memory::commands::setup::handle_setup;
use trusty_memory::commands::start::handle_start;
use trusty_memory::commands::stop::handle_stop;
use trusty_memory::{resolve_palace_registry_dir, run_http, run_http_dynamic, AppState};
#[derive(Debug, Parser)]
#[command(
name = "trusty-memory",
version,
about = "Memory palace MCP server + migration utility",
long_about = "MCP server (stdio + HTTP/SSE) for trusty-memory, plus a \
`migrate kuzu-memory` subcommand that rewrites Claude \
settings files referencing the legacy kuzu-memory server."
)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Start,
Stop,
Serve {
#[arg(long, value_name = "ADDR")]
http: Option<SocketAddr>,
#[arg(long)]
foreground: bool,
#[arg(long, value_name = "NAME")]
palace: Option<String>,
},
Migrate {
#[arg(value_enum)]
target: MigrateTarget,
#[arg(long)]
dry_run: bool,
#[arg(long)]
config_only: bool,
#[arg(long, value_name = "PATH")]
from: Option<std::path::PathBuf>,
#[arg(long, value_name = "NAME")]
palace: Option<String>,
#[arg(long, value_name = "N")]
limit: Option<usize>,
},
Setup,
#[command(name = "prompt-context")]
PromptContext,
Doctor {
#[arg(long)]
fix_palaces: bool,
#[arg(long, requires = "fix_palaces")]
fix: bool,
},
Service {
#[command(subcommand)]
action: ServiceAction,
},
#[command(subcommand_required = true)]
Monitor {
#[command(subcommand)]
target: MonitorTarget,
},
#[command(name = "send-message")]
SendMessage {
#[arg(long, value_name = "PALACE")]
to: String,
#[arg(long, value_name = "PURPOSE")]
purpose: String,
#[arg(long, value_name = "TEXT")]
content: String,
#[arg(long, value_name = "PALACE")]
from: Option<String>,
},
#[command(name = "inbox-check")]
InboxCheck {
#[arg(long, value_name = "PALACE")]
palace: Option<String>,
},
Note {
#[arg(value_name = "CONTENT")]
content: String,
#[arg(long, value_name = "NAME")]
palace: Option<String>,
#[arg(long = "tag", value_name = "TAG")]
tags: Vec<String>,
},
#[command(name = "kg-rebuild")]
KgRebuild {
#[arg(long, value_name = "ID")]
palace: Option<String>,
},
}
#[derive(Debug, Subcommand)]
enum MonitorTarget {
Web,
Tui,
Status {
#[arg(long)]
json: bool,
},
Palaces {
id: Option<String>,
#[arg(long)]
json: bool,
},
}
static HELP: std::sync::LazyLock<trusty_common::help::HelpConfig> =
std::sync::LazyLock::new(|| {
trusty_common::help::load_help(include_str!("../help.yaml"))
.expect("trusty-memory help.yaml is bundled and valid")
});
#[tokio::main]
async fn main() -> Result<()> {
let argv: Vec<String> = std::env::args().collect();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
e.print().ok();
if matches!(
e.kind(),
clap::error::ErrorKind::InvalidSubcommand | clap::error::ErrorKind::UnknownArgument
) {
trusty_common::help::print_suggestion_hint(&argv, &HELP);
}
std::process::exit(e.exit_code());
}
};
let log_buffer = trusty_common::init_tracing_with_buffer(
cli.verbose,
trusty_common::log_buffer::DEFAULT_LOG_CAPACITY,
);
match cli.command {
Command::Start => handle_start().await,
Command::Stop => handle_stop().await,
Command::Serve {
http,
foreground,
palace,
} => run_serve(http, foreground, palace, log_buffer).await,
Command::Migrate {
target,
dry_run,
config_only,
from,
palace,
limit,
} => handle_migrate(target, dry_run, config_only, from, palace, limit),
Command::Setup => handle_setup(),
Command::PromptContext => handle_prompt_context().await,
Command::Service { action } => handle_service(&action),
Command::Doctor { fix_palaces, fix } => {
if fix_palaces {
trusty_memory::commands::doctor::handle_doctor_fix_palaces(fix).await?;
}
trusty_memory::commands::doctor::handle_doctor().await
}
Command::Monitor { target } => run_monitor(target).await,
Command::SendMessage {
to,
purpose,
content,
from,
} => handle_send_message(to, purpose, content, from).await,
Command::InboxCheck { palace } => handle_inbox_check(palace).await,
Command::Note {
content,
palace,
tags,
} => handle_note(content, palace, tags).await,
Command::KgRebuild { palace } => {
trusty_memory::commands::kg_rebuild::handle_kg_rebuild(palace).await
}
}
}
async fn run_monitor(target: MonitorTarget) -> Result<()> {
use trusty_memory::commands::monitor;
match target {
MonitorTarget::Web => match trusty_common::read_daemon_addr("trusty-memory")? {
Some(addr) => {
println!("{addr}/ui");
Ok(())
}
None => {
eprintln!("trusty-memory daemon not running (no address found)");
std::process::exit(1);
}
},
MonitorTarget::Tui => trusty_common::monitor::memory_tui::run().await,
MonitorTarget::Status { json } => monitor::handle_status(json).await,
MonitorTarget::Palaces { id, json } => monitor::handle_palaces(id, json).await,
}
}
async fn run_serve(
http: Option<SocketAddr>,
foreground: bool,
palace: Option<String>,
log_buffer: trusty_common::log_buffer::LogBuffer,
) -> Result<()> {
if !foreground && http.is_none() {
return trusty_memory::commands::start::handle_start().await;
}
let data_dir = trusty_common::resolve_data_dir("trusty-memory")?;
let data_root = resolve_palace_registry_dir(data_dir);
if let Err(e) = trusty_memory::commands::migrations::migrate_default_palace_name(&data_root) {
tracing::warn!("default-palace name migration skipped: {e:#}");
}
if let Some(addr) = http {
let state = AppState::new(data_root)
.with_default_palace(palace)
.with_log_buffer(log_buffer)
.with_bm25_client_from_env();
spawn_startup_tasks(&state);
run_http(state, addr).await
} else {
let state = AppState::new(data_root)
.with_default_palace(palace)
.with_log_buffer(log_buffer)
.with_bm25_client_from_env();
spawn_startup_tasks(&state);
run_http_dynamic(state).await
}
}
fn spawn_startup_tasks(state: &AppState) {
let bg_state = state.clone();
tokio::spawn(async move {
let started = std::time::Instant::now();
tracing::info!("starting background palace hydration");
match bg_state.load_palaces_from_disk().await {
Ok(count) => tracing::info!(
elapsed_ms = started.elapsed().as_millis() as u64,
"background palace hydration complete: {count} palaces loaded"
),
Err(e) => tracing::error!(
elapsed_ms = started.elapsed().as_millis() as u64,
"background palace hydration failed: {e:#}"
),
}
if let Some(palace) = bg_state.default_palace.clone() {
if let Ok(cwd) = std::env::current_dir() {
bg_state.spawn_alias_discovery(palace, cwd);
}
}
});
}