use crate::errors::Error;
use crate::memory::MemoryStore;
use crate::memory_types::{AddResult, IngestPolicy};
use crate::output::*;
use crate::{config, temporal};
use std::process::ExitCode;
struct SearchContext {
query: String,
limit: usize,
recency: Option<f64>,
hybrid: bool,
}
#[derive(clap::Subcommand)]
pub enum Commands {
Add {
text: String,
#[arg(short = 'm', long)]
metadata: Option<String>,
#[arg(long)]
force: bool,
},
Search {
query: String,
#[arg(short = 'l', long, default_value = "5")]
limit: usize,
#[arg(long)]
recency: Option<f64>,
#[arg(long)]
hybrid: bool,
},
Get {
id: String,
},
List {
#[arg(short = 'l', long, default_value = "10")]
limit: usize,
},
Delete {
id: String,
},
Update {
id: String,
text: String,
},
Version,
#[cfg(feature = "mcp")]
Mcp,
}
pub fn execute(
command: &Commands,
store: &mut MemoryStore,
project_id: String,
config: &config::Config,
json: bool,
) -> Result<ExitCode, Error> {
match command {
Commands::Add {
text,
metadata,
force,
} => handle_add(store, &project_id, text, metadata.as_deref(), *force, json),
Commands::Search {
query,
limit,
recency,
hybrid,
} => handle_search(
store,
&project_id,
&SearchContext {
query: query.clone(),
limit: *limit,
recency: *recency,
hybrid: *hybrid,
},
config,
json,
),
Commands::Get { id } => handle_get(store, id, json),
Commands::List { limit } => handle_list(store, &project_id, *limit, json),
Commands::Delete { id } => handle_delete(store, id, json),
Commands::Update { id, text } => handle_update(store, id, text, json),
Commands::Version => handle_version(json),
#[cfg(feature = "mcp")]
Commands::Mcp => unreachable!("Mcp is handled before execute"),
}
}
fn handle_add(
store: &mut MemoryStore,
project_id: &str,
text: &str,
metadata: Option<&str>,
force: bool,
json: bool,
) -> Result<ExitCode, Error> {
let policy = if force {
IngestPolicy::Force
} else {
IngestPolicy::ConflictAware
};
match store.ingest(project_id, text, metadata, policy)? {
AddResult::Added { id } => {
if json {
print_json(&AddResponse {
status: "added".to_string(),
id,
});
} else {
println!("Added memory: {}", id);
}
Ok(ExitCode::SUCCESS)
}
AddResult::Conflicts {
proposed,
conflicts,
} => {
if json {
let conflict_responses: Vec<ConflictMemoryResponse> = conflicts
.into_iter()
.map(|c| ConflictMemoryResponse {
id: c.id,
content: c.content,
similarity: c.similarity,
})
.collect();
print_json(&ConflictsResponse {
status: "conflicts".to_string(),
proposed,
conflicts: conflict_responses,
});
} else {
println!(
"Conflicts detected: {} similar memory/memories found",
conflicts.len()
);
println!("Proposed: {}", proposed);
println!("Use --force to add anyway");
for conflict in conflicts {
println!(" {} (similarity: {:.3})", conflict.id, conflict.similarity);
println!(" {}", conflict.content);
}
}
Ok(ExitCode::from(2))
}
}
}
fn handle_search(
store: &mut MemoryStore,
project_id: &str,
opts: &SearchContext,
config: &config::Config,
json: bool,
) -> Result<ExitCode, Error> {
let recency_weight = opts.recency.unwrap_or(config.recency_weight);
temporal::validate_recency_weight(recency_weight)?;
let memories = if opts.hybrid {
store.search_hybrid(project_id, &opts.query, opts.limit, recency_weight)?
} else {
store.search(project_id, &opts.query, opts.limit, recency_weight)?
};
if json {
let results: Vec<SearchResultItem> = memories
.into_iter()
.map(|m| SearchResultItem {
id: m.id,
content: m.content,
similarity: m.similarity.unwrap_or(0.0),
created_at: m.created_at,
})
.collect();
print_json(&SearchResponse { results });
} else {
for memory in memories {
let score = memory.similarity.unwrap_or(0.0);
println!(
"{} [score: {:.2}]\n {}\n",
memory.id, score, memory.content
);
}
}
Ok(ExitCode::SUCCESS)
}
fn handle_get(store: &mut MemoryStore, id: &str, json: bool) -> Result<ExitCode, Error> {
let memory = store
.get(id)?
.ok_or_else(|| Error::NotFound("memory not found".to_string()))?;
if json {
print_json(&GetResponse {
id: memory.id.clone(),
content: memory.content.clone(),
project_id: memory.project_id,
metadata: memory.metadata,
created_at: memory.created_at,
updated_at: memory.updated_at,
});
} else {
println!("ID: {}", memory.id);
println!("Content: {}", memory.content);
println!("Project: {}", memory.project_id);
if let Some(meta) = &memory.metadata {
println!("Metadata: {}", meta);
}
println!("Created: {}", memory.created_at);
println!("Updated: {}", memory.updated_at);
}
Ok(ExitCode::SUCCESS)
}
fn handle_list(
store: &mut MemoryStore,
project_id: &str,
limit: usize,
json: bool,
) -> Result<ExitCode, Error> {
let memories = store.list(project_id, limit)?;
if json {
let items: Vec<ListItem> = memories
.into_iter()
.map(|m| ListItem {
id: m.id,
content: m.content,
created_at: m.created_at,
})
.collect();
print_json(&ListResponse { memories: items });
} else {
for memory in memories {
println!("{}: {}", memory.id, memory.content);
}
}
Ok(ExitCode::SUCCESS)
}
fn handle_delete(store: &mut MemoryStore, id: &str, json: bool) -> Result<ExitCode, Error> {
let deleted = store.delete(id)?;
if deleted {
if json {
print_json(&DeleteResponse {
status: "deleted".to_string(),
id: id.to_string(),
});
} else {
println!("Deleted memory: {}", id);
}
Ok(ExitCode::SUCCESS)
} else {
Err(Error::NotFound("memory not found".to_string()))
}
}
fn handle_update(
store: &mut MemoryStore,
id: &str,
text: &str,
json: bool,
) -> Result<ExitCode, Error> {
store.update(id, text)?;
if json {
print_json(&UpdateResponse {
status: "updated".to_string(),
id: id.to_string(),
});
} else {
println!("Updated memory: {}", id);
}
Ok(ExitCode::SUCCESS)
}
fn handle_version(json: bool) -> Result<ExitCode, Error> {
if json {
print_json(&serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"name": env!("CARGO_PKG_NAME")
}));
} else {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
Ok(ExitCode::SUCCESS)
}