use std::process::ExitCode;
use clap::{Parser, Subcommand, ValueEnum};
use mnemo::{
Memory, MemoryType, Metric, Mnemo, MnemoConfig, RecallRequest, RecallResult, Result, Ulid,
};
#[derive(Parser)]
#[command(name = "mnemo", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Init {
path: String,
#[arg(long, default_value_t = 768)]
dimensions: usize,
#[arg(long)]
no_manifest: bool,
#[arg(long)]
passphrase: Option<String>,
},
Info {
path: String,
#[arg(long)]
passphrase: Option<String>,
},
Rekey {
path: String,
#[arg(long)]
passphrase: Option<String>,
#[arg(long)]
new_passphrase: Option<String>,
},
Compact {
path: String,
#[arg(long)]
passphrase: Option<String>,
},
Verify {
path: String,
#[arg(long)]
passphrase: Option<String>,
},
Import {
path: String,
file: String,
#[arg(long)]
passphrase: Option<String>,
},
Remember {
path: String,
#[arg(long)]
text: String,
#[arg(long, default_value = "semantic")]
r#type: String,
#[arg(long)]
importance: Option<f32>,
#[arg(long)]
agent: Option<String>,
#[arg(long)]
metadata: Option<String>,
#[arg(long)]
embed_cmd: String,
#[arg(long)]
embed_name: String,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
embed_normalize: bool,
#[arg(long, default_value_t = 60)]
embed_timeout: u64,
#[arg(long)]
passphrase: Option<String>,
},
Index {
path: String,
#[arg(long)]
passphrase: Option<String>,
#[arg(long)]
drop: bool,
#[arg(long, default_value_t = 0)]
partitions: usize,
#[arg(long, default_value_t = 0)]
subspaces: usize,
#[arg(long, default_value_t = 8)]
n_probe: usize,
#[arg(long, default_value_t = 64)]
n_rerank: usize,
},
Search {
path: String,
#[arg(long, conflicts_with = "query_file")]
query: Option<String>,
#[arg(long, conflicts_with = "query")]
query_file: Option<String>,
#[arg(long, default_value_t = 5)]
top_k: usize,
#[arg(long)]
passphrase: Option<String>,
},
About {
path: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Table)]
format: OutputFormat,
#[arg(long)]
manifest_only: bool,
#[arg(long)]
passphrase: Option<String>,
},
Get {
path: String,
id: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Table)]
format: OutputFormat,
#[arg(long)]
verbose: bool,
#[arg(long)]
vector: bool,
#[arg(long)]
passphrase: Option<String>,
},
List {
path: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Table)]
format: OutputFormat,
#[arg(long, value_name = "T[,T...]")]
r#type: Option<String>,
#[arg(long)]
agent: Option<String>,
#[arg(long)]
limit: Option<usize>,
#[arg(long, default_value_t = 0)]
offset: usize,
#[arg(long, default_value = "created")]
sort: String,
#[arg(long)]
vector: bool,
#[arg(long)]
passphrase: Option<String>,
},
Recall {
path: String,
#[arg(long, conflicts_with_all = ["query_file", "text"])]
query: Option<String>,
#[arg(long, conflicts_with_all = ["query", "text"])]
query_file: Option<String>,
#[arg(long, conflicts_with_all = ["query", "query_file"])]
text: Option<String>,
#[arg(long, default_value_t = 10)]
top_k: usize,
#[arg(long, value_enum, default_value_t = OutputFormat::Table)]
format: OutputFormat,
#[arg(long, value_name = "T[,T...]")]
r#type: Option<String>,
#[arg(long)]
agent: Option<String>,
#[arg(long, default_value = "cosine")]
metric: String,
#[arg(long)]
n_probe: Option<usize>,
#[arg(long)]
n_rerank: Option<usize>,
#[arg(long)]
no_track: bool,
#[arg(long, requires = "text")]
embed_cmd: Option<String>,
#[arg(long, requires = "text")]
embed_name: Option<String>,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
embed_normalize: bool,
#[arg(long, default_value_t = 60)]
embed_timeout: u64,
#[arg(long)]
passphrase: Option<String>,
},
Demo {
#[arg(long, default_value = "demo.mnemo")]
path: String,
},
Snapshots {
path: String,
#[arg(long)]
passphrase: Option<String>,
},
Restore {
path: String,
#[arg(long, conflicts_with = "to_time")]
to_txn: Option<u64>,
#[arg(long)]
to_time: Option<i64>,
#[arg(long)]
passphrase: Option<String>,
},
Serve {
path: String,
#[arg(long)]
mcp: bool,
#[arg(long, requires = "embed_name")]
embed_cmd: Option<String>,
#[arg(long, requires = "embed_cmd")]
embed_name: Option<String>,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
embed_normalize: bool,
#[arg(long, default_value_t = 60)]
embed_timeout: u64,
},
Cache {
#[command(subcommand)]
op: CacheOp,
},
}
#[derive(Subcommand)]
enum CacheOp {
Get {
path: String,
#[arg(long)]
ns: String,
#[arg(long)]
key: String,
#[arg(long)]
passphrase: Option<String>,
},
Put {
path: String,
#[arg(long)]
ns: String,
#[arg(long)]
key: String,
#[arg(long, conflicts_with = "file")]
value: Option<String>,
#[arg(long, conflicts_with = "value")]
file: Option<String>,
#[arg(long, default_value = "text")]
content_type: String,
#[arg(long)]
ttl: Option<u64>,
#[arg(long)]
passphrase: Option<String>,
},
Delete {
path: String,
#[arg(long)]
ns: String,
#[arg(long)]
key: String,
#[arg(long)]
passphrase: Option<String>,
},
Stats {
path: String,
#[arg(long)]
ns: Option<String>,
#[arg(long)]
passphrase: Option<String>,
},
Purge {
path: String,
#[arg(long)]
ns: Option<String>,
#[arg(long)]
expired: bool,
#[arg(long)]
passphrase: Option<String>,
},
}
fn passphrase(arg: &Option<String>) -> std::result::Result<String, String> {
if let Some(p) = arg {
eprintln!(
"warning: passing the passphrase as `--passphrase` is insecure; \
it lands in shell history and process listings. Prefer the \
MNEMO_PASSPHRASE env var, or omit both to be prompted."
);
return Ok(p.clone());
}
if let Ok(p) = std::env::var("MNEMO_PASSPHRASE") {
return Ok(p);
}
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return Err("no passphrase: pass --passphrase or set MNEMO_PASSPHRASE".to_string());
}
rpassword::prompt_password("Passphrase: ")
.map_err(|e| format!("failed to read passphrase: {e}"))
}
fn new_passphrase(arg: &Option<String>, env_var: &str) -> std::result::Result<String, String> {
if let Some(p) = arg {
eprintln!(
"warning: passing the passphrase as a flag is insecure; it lands \
in shell history and process listings. Prefer the {env_var} env \
var, or omit both to be prompted twice."
);
return Ok(p.clone());
}
if let Ok(p) = std::env::var(env_var) {
return Ok(p);
}
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return Err(format!("no passphrase: pass the flag or set {env_var}"));
}
let first = rpassword::prompt_password("New passphrase: ")
.map_err(|e| format!("failed to read passphrase: {e}"))?;
let second = rpassword::prompt_password("Confirm passphrase: ")
.map_err(|e| format!("failed to read passphrase: {e}"))?;
if first != second {
return Err("passphrases do not match".to_string());
}
Ok(first)
}
fn resolve_query(
inline: Option<String>,
file: Option<String>,
) -> std::result::Result<Vec<f32>, String> {
let text = match (inline, file) {
(Some(s), None) => s,
(None, Some(p)) => {
if p == "-" {
use std::io::Read;
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.map_err(|e| format!("reading stdin: {e}"))?;
buf
} else {
std::fs::read_to_string(&p).map_err(|e| format!("reading {p}: {e}"))?
}
}
(Some(_), Some(_)) => {
return Err("pass only one of --query or --query-file".into());
}
(None, None) => {
return Err("missing query: pass --query <floats> or --query-file <path|->".into());
}
};
parse_query_text(&text)
}
fn parse_query_text(text: &str) -> std::result::Result<Vec<f32>, String> {
let trimmed = text.trim();
if trimmed.is_empty() {
return Err("query text was empty".into());
}
if trimmed.starts_with('[') {
return serde_json::from_str::<Vec<f32>>(trimmed)
.map_err(|e| format!("invalid JSON array of floats: {e}"));
}
trimmed
.split(|c: char| c == ',' || c.is_whitespace())
.filter(|s| !s.is_empty())
.map(|t| {
t.parse::<f32>()
.map_err(|e| format!("bad float '{t}': {e}"))
})
.collect()
}
fn parse_ulid(s: &str) -> std::result::Result<Ulid, String> {
Ulid::from_string(s.trim()).map_err(|e| format!("bad ULID '{s}': {e}"))
}
fn parse_memory_types(s: &str) -> std::result::Result<Vec<MemoryType>, String> {
s.split(',')
.map(|t| t.trim())
.filter(|t| !t.is_empty())
.map(|t| MemoryType::parse(t).ok_or_else(|| format!("unknown memory type '{t}'")))
.collect()
}
fn parse_metric(s: &str) -> std::result::Result<Metric, String> {
match s.trim().to_ascii_lowercase().as_str() {
"cosine" | "cos" => Ok(Metric::Cosine),
"l2" | "euclidean" => Ok(Metric::L2),
"dot" | "ip" => Ok(Metric::Dot),
other => Err(format!("unknown metric '{other}' (use cosine, l2, or dot)")),
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "lowercase")]
enum OutputFormat {
Table,
Json,
Jsonl,
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
OutputFormat::Table => "table",
OutputFormat::Json => "json",
OutputFormat::Jsonl => "jsonl",
})
}
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
out.push('…');
out
}
fn memory_to_json(m: &Memory, include_vector: bool) -> serde_json::Value {
let mut v = serde_json::to_value(m).unwrap_or_else(|_| serde_json::json!({}));
if !include_vector {
if let Some(obj) = v.as_object_mut() {
obj.remove("vector");
}
}
v
}
fn print_memory(m: &Memory, format: OutputFormat, verbose: bool, include_vector: bool) {
match format {
OutputFormat::Table => {
let id = m.id.to_string();
let imp = format!("imp={:.2}", m.importance);
if verbose {
println!(
"{} [{}] agent={} {}",
id,
m.memory_type.as_str(),
m.agent_id,
imp
);
println!(" content : {}", m.content);
if let Some(sid) = &m.session_id {
println!(" session : {sid}");
}
println!(
" scope : {}",
match m.scope {
mnemo::Scope::Private => "private",
mnemo::Scope::Shared => "shared",
}
);
println!(
" times : created={} accessed={} access_count={}",
m.created_at, m.accessed_at, m.access_count
);
if let Some(ttl) = m.ttl_secs {
println!(" ttl_secs: {ttl}");
}
if !m.metadata.is_empty() {
let meta = serde_json::Value::Object(m.metadata.clone());
println!(" meta : {meta}");
}
} else {
println!(
"{} [{}] agent={} {} {}",
id,
m.memory_type.as_str(),
m.agent_id,
imp,
truncate(&m.content, 80),
);
}
}
OutputFormat::Json => {
let v = memory_to_json(m, include_vector);
println!(
"{}",
serde_json::to_string_pretty(&v).unwrap_or_else(|_| "{}".into())
);
}
OutputFormat::Jsonl => {
let v = memory_to_json(m, include_vector);
println!(
"{}",
serde_json::to_string(&v).unwrap_or_else(|_| "{}".into())
);
}
}
}
fn print_memories(items: &[Memory], total: usize, format: OutputFormat, include_vector: bool) {
match format {
OutputFormat::Table => {
if items.is_empty() {
println!("no memories");
return;
}
println!("showing {} of {} memories", items.len(), total);
for m in items {
print_memory(m, OutputFormat::Table, false, false);
}
}
OutputFormat::Json => {
let arr: Vec<_> = items
.iter()
.map(|m| memory_to_json(m, include_vector))
.collect();
let doc = serde_json::json!({ "total": total, "count": arr.len(), "memories": arr });
println!(
"{}",
serde_json::to_string_pretty(&doc).unwrap_or_else(|_| "{}".into())
);
}
OutputFormat::Jsonl => {
for m in items {
println!(
"{}",
serde_json::to_string(&memory_to_json(m, include_vector))
.unwrap_or_else(|_| "{}".into())
);
}
}
}
}
fn print_recall_hits(hits: &[RecallResult], format: OutputFormat) {
match format {
OutputFormat::Table => {
if hits.is_empty() {
println!("no results");
return;
}
for h in hits {
println!(
"score={:.3} sim={:.3} [{}] {} {}",
h.score,
h.similarity,
h.memory.memory_type.as_str(),
h.memory.id,
truncate(&h.memory.content, 80),
);
}
}
OutputFormat::Json => {
let arr: Vec<_> = hits
.iter()
.map(|h| {
serde_json::json!({
"score": h.score,
"similarity": h.similarity,
"memory": memory_to_json(&h.memory, false),
})
})
.collect();
let doc = serde_json::json!({ "count": arr.len(), "hits": arr });
println!(
"{}",
serde_json::to_string_pretty(&doc).unwrap_or_else(|_| "{}".into())
);
}
OutputFormat::Jsonl => {
for h in hits {
let line = serde_json::json!({
"score": h.score,
"similarity": h.similarity,
"memory": memory_to_json(&h.memory, false),
});
println!(
"{}",
serde_json::to_string(&line).unwrap_or_else(|_| "{}".into())
);
}
}
}
}
fn run() -> std::result::Result<(), String> {
let cli = Cli::parse();
match cli.command {
Command::Init {
path,
dimensions,
no_manifest,
passphrase: pp,
} => {
let pp = new_passphrase(&pp, "MNEMO_PASSPHRASE")?;
let cfg = MnemoConfig {
dimensions,
..Default::default()
};
let mut db = Mnemo::create(&path, &pp, cfg).map_err(fmt)?;
if !no_manifest {
let manifest = Memory::scaffold_manifest(dimensions);
db.remember(manifest).map_err(fmt)?;
db.flush().map_err(fmt)?;
}
db.close().map_err(fmt)?;
if no_manifest {
println!("created {path} ({dimensions} dimensions, encrypted, no manifest)");
} else {
println!(
"created {path} ({dimensions} dimensions, encrypted) with scaffold manifest"
);
println!(" → run `mnemo about {path}` to view it");
println!(" → replace it with one that records your embedder and conventions");
}
}
Command::Info {
path,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let s = db.stats().map_err(fmt)?;
println!("path: {path}");
println!("memories: {}", s.memories);
println!("tombstoned: {}", s.deleted);
println!("dimensions: {}", s.dimensions);
println!("file bytes: {}", s.file_bytes);
println!("encrypted: {}", s.encrypted);
println!(
"wal region: {} pages ({} KiB)",
s.wal_pages,
s.wal_pages * 8
);
println!("created at: {}", s.created_at);
println!("agents: {}", s.agents.join(", "));
println!("snapshots: {}", db.snapshots().len());
{
let cs = db.cache_stats(None);
println!(
"cache: {} entries, {} bytes (hits {}, misses {}, evict {})",
cs.entries, cs.bytes, cs.hits, cs.misses, cs.evictions
);
}
match s.index {
Some(ix) => {
println!(
"ann index: {} vectors, {} partitions, {} PQ subspaces",
ix.vectors, ix.partitions, ix.subspaces
);
println!(
" defaults: n_probe={}, n_rerank={}",
ix.n_probe, ix.n_rerank
);
}
None => println!("ann index: none (recall uses exact scan)"),
}
}
Command::Rekey {
path,
passphrase: pp,
new_passphrase: new,
} => {
let pp = passphrase(&pp)?;
let new = new_passphrase(&new, "MNEMO_NEW_PASSPHRASE")?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
db.rekey(&new, mnemo::KdfParams::secure()).map_err(fmt)?;
db.close().map_err(fmt)?;
println!("rekeyed {path}");
}
Command::Compact {
path,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let report = Mnemo::compact_file(&path, &pp).map_err(fmt)?;
println!(
"compacted {path}: {} -> {} live memories",
report.before, report.after
);
}
Command::Verify {
path,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let n = db.verify().map_err(fmt)?;
println!("verified {n} records — all pages decrypt and decode");
}
Command::Import {
path,
file,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
let text =
std::fs::read_to_string(&file).map_err(|e| format!("cannot read {file}: {e}"))?;
let mut count = 0usize;
for (lineno, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let m = memory_from_json(line).map_err(|e| format!("line {}: {e}", lineno + 1))?;
db.remember(m).map_err(fmt)?;
count += 1;
}
db.flush().map_err(fmt)?;
println!("imported {count} memories into {path}");
}
Command::Remember {
path,
text,
r#type,
importance,
agent,
metadata,
embed_cmd,
embed_name,
embed_normalize,
embed_timeout,
passphrase: pp,
} => {
use mnemo::{EmbedCmd, EmbedderIdentity};
let pp = passphrase(&pp)?;
let mt = parse_memory_types(&r#type)?
.pop()
.ok_or_else(|| format!("unknown memory type: {type:?}", r#type = r#type))?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
let identity = EmbedderIdentity {
name: embed_name,
dimensions: db.dimensions(),
normalize: embed_normalize,
};
let embedder = EmbedCmd::new(&embed_cmd, identity)
.map_err(fmt)?
.with_timeout(std::time::Duration::from_secs(embed_timeout));
db.set_embedder(embedder).map_err(fmt)?;
let vector = {
let embedder_ref = db
.embedder()
.ok_or_else(|| "no embedder registered".to_string())?;
let mut vectors = embedder_ref.embed(&[text.as_str()]).map_err(fmt)?;
vectors
.pop()
.ok_or_else(|| "embedder returned no vector".to_string())?
};
let mut memory = Memory::new(text, mt, vector);
if let Some(i) = importance {
memory = memory.with_importance(i);
}
if let Some(a) = agent {
memory = memory.with_agent(a);
}
if let Some(meta_str) = metadata {
let parsed: serde_json::Value = serde_json::from_str(&meta_str)
.map_err(|e| format!("invalid --metadata JSON: {e}"))?;
let obj = parsed
.as_object()
.ok_or_else(|| "--metadata must be a JSON object".to_string())?;
for (k, v) in obj {
memory.metadata.insert(k.clone(), v.clone());
}
}
let id = db.remember(memory).map_err(fmt)?;
db.flush().map_err(fmt)?;
println!("{id}");
}
Command::Index {
path,
passphrase: pp,
drop,
partitions,
subspaces,
n_probe,
n_rerank,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
if drop {
db.drop_index();
db.flush().map_err(fmt)?;
println!("dropped ANN index on {path}");
} else {
let cfg = mnemo::IndexConfig {
n_partitions: partitions,
pq_subspaces: subspaces,
n_probe,
n_rerank,
..Default::default()
};
let info = db.build_index_with(cfg).map_err(fmt)?;
db.flush().map_err(fmt)?;
println!(
"built ANN index on {path}: {} vectors, {} partitions, {} PQ subspaces",
info.vectors, info.partitions, info.subspaces
);
}
}
Command::Search {
path,
query,
query_file,
top_k,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let q = resolve_query(query, query_file)?;
let mut db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let hits = db.search(&q, top_k, mnemo::Metric::Cosine).map_err(fmt)?;
if hits.is_empty() {
println!("no results");
}
for (m, sim) in hits {
println!("{sim:.4} [{}] {}", m.memory_type.as_str(), m.content);
}
}
Command::About {
path,
format,
manifest_only,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let stats = db.stats().map_err(fmt)?;
let snapshots = db.snapshots().len();
let mut entries = db.about().map_err(fmt)?;
if manifest_only {
entries.retain(|m| {
m.metadata
.get("topic")
.and_then(|v| v.as_str())
.map(|s| s.eq_ignore_ascii_case("manifest"))
.unwrap_or(false)
});
}
match format {
OutputFormat::Json | OutputFormat::Jsonl => {
let manifest = entries.iter().find(|m| {
m.metadata
.get("topic")
.and_then(|v| v.as_str())
.map(|s| s.eq_ignore_ascii_case("manifest"))
.unwrap_or(false)
});
let doc = serde_json::json!({
"path": path,
"stats": {
"memories": stats.memories,
"dimensions": stats.dimensions,
"file_bytes": stats.file_bytes,
"encrypted": stats.encrypted,
"snapshots": snapshots,
"has_index": stats.index.is_some(),
},
"manifest": manifest.map(|m| memory_to_json(m, false)),
"onboarding": entries
.iter()
.map(|m| memory_to_json(m, false))
.collect::<Vec<_>>(),
});
println!(
"{}",
serde_json::to_string_pretty(&doc).unwrap_or_else(|_| "{}".into())
);
}
OutputFormat::Table => {
println!(
"# {} ({} memories · {}-dim · {} · {} snapshots{})",
path,
stats.memories,
stats.dimensions,
if stats.encrypted {
"encrypted"
} else {
"plaintext"
},
snapshots,
if stats.index.is_some() {
" · ANN index built"
} else {
""
},
);
if entries.is_empty() {
println!();
println!(
"(no onboarding memories — this database has no self-description.)"
);
println!("To make a database self-describing, store a memory with");
println!(
" metadata = {{\"area\": \"onboarding\", \"topic\": \"manifest\"}}"
);
println!("that introduces the project, embedder, and conventions.");
} else {
println!();
println!(
"## Onboarding briefing ({} entries, most important first)",
entries.len()
);
for m in &entries {
let topic = m
.metadata
.get("topic")
.and_then(|v| v.as_str())
.unwrap_or("");
let is_scaffold = m
.metadata
.get("scaffold")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let head = if topic.eq_ignore_ascii_case("manifest") {
if is_scaffold {
format!(
"MANIFEST (scaffold — please replace) (importance={:.2})",
m.importance
)
} else {
format!("MANIFEST (importance={:.2})", m.importance)
}
} else if topic.is_empty() {
format!("(importance={:.2})", m.importance)
} else {
format!("[{}] (importance={:.2})", topic, m.importance)
};
println!();
println!("### {head}");
println!("{}", m.content);
}
println!();
println!("## Quick start");
println!(
" mnemo list {path} # browse all live memories"
);
println!(" mnemo recall {path} --query VEC # multi-signal recall");
println!(" mnemo get {path} <ulid> --verbose # fetch one memory");
}
}
}
}
Command::Get {
path,
id,
format,
verbose,
vector,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let ulid = parse_ulid(&id)?;
let mut db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let m = db.get(&ulid).map_err(fmt)?;
print_memory(&m, format, verbose, vector);
}
Command::List {
path,
format,
r#type,
agent,
limit,
offset,
sort,
vector,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let types = match r#type {
Some(s) => Some(parse_memory_types(&s)?),
None => None,
};
let sort = sort.trim().to_ascii_lowercase();
if !matches!(sort.as_str(), "created" | "importance" | "id") {
return Err(format!(
"unknown sort '{sort}' (use created, importance, or id)"
));
}
let mut db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let mut all = db.memories().map_err(fmt)?;
if let Some(ts) = &types {
all.retain(|m| ts.contains(&m.memory_type));
}
if let Some(a) = &agent {
all.retain(|m| &m.agent_id == a);
}
let total_after_filter = all.len();
if limit.is_none() && total_after_filter > 10_000 {
eprintln!(
"warning: {} memories — consider --limit; printing all anyway",
total_after_filter
);
}
match sort.as_str() {
"created" => all.sort_by_key(|m| m.created_at),
"importance" => {
all.sort_by(|a, b| b.importance.total_cmp(&a.importance));
}
"id" => all.sort_by_key(|m| m.id),
_ => unreachable!(),
}
let start = offset.min(all.len());
let end = match limit {
Some(n) => (start + n).min(all.len()),
None => all.len(),
};
let page: Vec<Memory> = all[start..end].to_vec();
print_memories(&page, total_after_filter, format, vector);
}
Command::Recall {
path,
query,
query_file,
text,
top_k,
format,
r#type,
agent,
metric,
n_probe,
n_rerank,
no_track,
embed_cmd,
embed_name,
embed_normalize,
embed_timeout,
passphrase: pp,
} => {
use mnemo::{EmbedCmd, EmbedderIdentity};
let pp = passphrase(&pp)?;
if query.is_none() && query_file.is_none() && text.is_none() {
return Err("recall requires one of --query / --query-file / --text".into());
}
let (q, mut db_opt) = match text {
Some(text) => {
let ecmd = embed_cmd.ok_or_else(|| {
"--text requires --embed-cmd and --embed-name".to_string()
})?;
let ename = embed_name.ok_or_else(|| {
"--text requires --embed-cmd and --embed-name".to_string()
})?;
let mut db = if no_track {
Mnemo::open_read_only(&path, &pp).map_err(fmt)?
} else {
Mnemo::open(&path, &pp).map_err(fmt)?
};
let identity = EmbedderIdentity {
name: ename,
dimensions: db.dimensions(),
normalize: embed_normalize,
};
let embedder = EmbedCmd::new(&ecmd, identity)
.map_err(fmt)?
.with_timeout(std::time::Duration::from_secs(embed_timeout));
db.set_embedder(embedder).map_err(fmt)?;
let vector = {
let embedder_ref = db
.embedder()
.ok_or_else(|| "embedder vanished after set_embedder".to_string())?;
let mut vs = embedder_ref.embed(&[text.as_str()]).map_err(fmt)?;
vs.pop()
.ok_or_else(|| "embedder returned no vector".to_string())?
};
(vector, Some(db))
}
None => (resolve_query(query, query_file)?, None),
};
let metric = parse_metric(&metric)?;
let mut req = RecallRequest::new(q).top_k(top_k).metric(metric);
if let Some(s) = r#type {
req = req.types(parse_memory_types(&s)?);
}
if let Some(a) = agent {
req = req.agent(a);
}
if let Some(n) = n_probe {
req = req.n_probe(n);
}
if let Some(n) = n_rerank {
req = req.n_rerank(n);
}
let hits = match (db_opt.take(), no_track) {
(Some(mut db), _) => {
if no_track {
req = req.track_access(false);
}
db.recall(&req).map_err(fmt)?
}
(None, true) => {
req = req.track_access(false);
let mut db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
db.recall(&req).map_err(fmt)?
}
(None, false) => {
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
db.recall(&req).map_err(fmt)?
}
};
print_recall_hits(&hits, format);
}
Command::Demo { path } => demo(&path).map_err(fmt)?,
Command::Snapshots {
path,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let snaps = db.snapshots();
if snaps.is_empty() {
println!("no snapshots yet (nothing has been flushed)");
} else {
println!("{:<8} {:<12} committed (unix)", "txn", "memories");
for s in snaps {
println!("{:<8} {:<12} {}", s.txn_id, s.memory_count, s.created_at);
}
}
}
Command::Restore {
path,
to_txn,
to_time,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
let info = match (to_txn, to_time) {
(Some(txn), _) => db.restore_to(txn).map_err(fmt)?,
(None, Some(t)) => db.restore_to_time(t).map_err(fmt)?,
(None, None) => return Err("restore needs --to-txn or --to-time".to_string()),
};
println!(
"restored {path} to snapshot txn {} ({} memories)",
info.txn_id, info.memory_count
);
}
Command::Serve {
path,
mcp,
embed_cmd,
embed_name,
embed_normalize,
embed_timeout,
} => {
if !mcp {
return Err("serve requires --mcp today (other transports may come later)".into());
}
let p = std::path::Path::new(&path);
let embed_spec = match (embed_cmd, embed_name) {
(Some(c), Some(n)) => Some((
c,
n,
embed_normalize,
std::time::Duration::from_secs(embed_timeout),
)),
_ => None,
};
mnemo::mcp::serve_stdio_with_embedder(p, embed_spec).map_err(fmt)?;
}
Command::Cache { op } => run_cache_op(op)?,
}
Ok(())
}
fn run_cache_op(op: CacheOp) -> std::result::Result<(), String> {
use mnemo::CachePutOpts;
match op {
CacheOp::Get {
path,
ns,
key,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
match db.cache_get(&ns, &key).map_err(fmt)? {
Some(v) => {
use std::io::Write;
std::io::stdout()
.write_all(&v.value)
.map_err(|e| e.to_string())?;
if !v.value.ends_with(b"\n") {
println!();
}
db.flush().map_err(fmt)?;
}
None => {
return Err(format!("cache miss on ({ns}, {key})"));
}
}
}
CacheOp::Put {
path,
ns,
key,
value,
file,
content_type,
ttl,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let bytes = match (value, file) {
(Some(v), None) => v.into_bytes(),
(None, Some(f)) => std::fs::read(&f).map_err(|e| format!("read {f}: {e}"))?,
(Some(_), Some(_)) => {
return Err("--value and --file are mutually exclusive".into())
}
(None, None) => {
return Err("cache put needs --value <string> or --file <path>".into())
}
};
let opts = CachePutOpts {
content_type,
ttl_secs: ttl,
cost_hint_ms: None,
};
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
db.cache_put(&ns, &key, &bytes, opts).map_err(fmt)?;
db.flush().map_err(fmt)?;
println!("cached {} bytes at ({ns}, {key})", bytes.len());
}
CacheOp::Delete {
path,
ns,
key,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
db.cache_delete(&ns, &key).map_err(fmt)?;
db.flush().map_err(fmt)?;
println!("deleted ({ns}, {key}) (space reclaimed on next compact)");
}
CacheOp::Stats {
path,
ns,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let db = Mnemo::open_read_only(&path, &pp).map_err(fmt)?;
let s = db.cache_stats(ns.as_deref());
let obj = serde_json::json!({
"namespace": ns,
"entries": s.entries,
"bytes": s.bytes,
"hits": s.hits,
"misses": s.misses,
"hit_rate": s.hit_rate,
"evictions": s.evictions,
});
println!("{}", serde_json::to_string_pretty(&obj).unwrap());
}
CacheOp::Purge {
path,
ns,
expired,
passphrase: pp,
} => {
let pp = passphrase(&pp)?;
let mut db = Mnemo::open(&path, &pp).map_err(fmt)?;
let n = db.cache_purge(ns.as_deref(), expired).map_err(fmt)?;
db.flush().map_err(fmt)?;
println!("purged {n} entr{}", if n == 1 { "y" } else { "ies" });
}
}
Ok(())
}
fn memory_from_json(line: &str) -> std::result::Result<Memory, String> {
use serde_json::Value;
let v: Value = serde_json::from_str(line).map_err(|e| e.to_string())?;
let content = v
.get("content")
.and_then(Value::as_str)
.ok_or("missing string field 'content'")?;
let vec_json = v
.get("vector")
.and_then(Value::as_array)
.ok_or("missing array field 'vector'")?;
let vector: Vec<f32> = vec_json
.iter()
.map(|x| {
x.as_f64()
.map(|f| f as f32)
.ok_or("vector element not a number")
})
.collect::<std::result::Result<_, _>>()?;
let mt = v
.get("memory_type")
.and_then(Value::as_str)
.and_then(MemoryType::parse)
.unwrap_or(MemoryType::Semantic);
let mut m = Memory::new(content, mt, vector);
if let Some(a) = v.get("agent_id").and_then(Value::as_str) {
m = m.with_agent(a);
}
if let Some(i) = v.get("importance").and_then(Value::as_f64) {
m = m.with_importance(i as f32);
}
Ok(m)
}
fn fmt(e: mnemo::MnemoError) -> String {
e.to_string()
}
fn demo(path: &str) -> Result<()> {
let _ = std::fs::remove_file(path);
let pp = "demo-passphrase";
let cfg = MnemoConfig {
dimensions: 4,
..Default::default()
};
println!("creating encrypted database at {path} ...");
let mut db = Mnemo::create(path, pp, cfg)?;
let seeds = [
(
"user prefers concise answers",
MemoryType::Semantic,
[0.9, 0.1, 0.0, 0.1],
0.9,
),
(
"user is based in Berlin",
MemoryType::Semantic,
[0.8, 0.2, 0.1, 0.0],
0.7,
),
(
"ran the deploy script at 14:00",
MemoryType::Episodic,
[0.1, 0.9, 0.1, 0.0],
0.4,
),
(
"to reset state, call clear() then reload",
MemoryType::Procedural,
[0.0, 0.1, 0.9, 0.2],
0.6,
),
(
"scratch: temp calculation result",
MemoryType::Working,
[0.2, 0.2, 0.2, 0.9],
0.2,
),
];
for (content, mt, v, imp) in seeds {
db.remember(
Memory::new(content, mt, v.to_vec())
.with_agent("assistant")
.with_importance(imp),
)?;
}
db.flush()?;
println!("stored {} memories, flushed to disk\n", db.len());
let info = db.build_index()?;
db.flush()?;
println!(
"built ANN index: {} vectors, {} partitions, {} PQ subspaces",
info.vectors, info.partitions, info.subspaces
);
println!("\nrecall for a query close to 'user preferences' (index-accelerated):");
let req = RecallRequest::new(vec![0.85, 0.15, 0.05, 0.05]).top_k(3);
for h in db.recall(&req)? {
println!(
" score={:.3} sim={:.3} [{}] {}",
h.score,
h.similarity,
h.memory.memory_type.as_str(),
h.memory.content
);
}
println!("\nreopening to confirm persistence and encryption ...");
db.close()?;
let db = Mnemo::open(path, pp)?;
println!("reopened: {} memories survive the round-trip", db.len());
let raw = std::fs::read(path).unwrap_or_default();
let needle = b"user prefers concise answers";
let leaked = raw.windows(needle.len()).any(|w| w == needle);
println!("plaintext 'user prefers concise answers' present in file bytes: {leaked}");
println!("\ndemo complete.");
Ok(())
}
fn main() -> ExitCode {
const RUN_STACK: usize = 8 * 1024 * 1024;
let handle = match std::thread::Builder::new()
.name("mnemo-cli".into())
.stack_size(RUN_STACK)
.spawn(run)
{
Ok(h) => h,
Err(e) => {
eprintln!("error: could not start cli worker thread: {e}");
return ExitCode::FAILURE;
}
};
let result = match handle.join() {
Ok(r) => r,
Err(_) => {
eprintln!("error: cli worker thread panicked");
return ExitCode::FAILURE;
}
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}