use std::path::PathBuf;
use clap::{Parser, Subcommand};
use crate::cli::onboarding::run_onboarding;
use crate::cli::output::{
print_blank_line, print_error, print_field, print_heading, print_info, print_line,
print_success,
};
use crate::config::{resolve_local_config_path, Config};
use crate::error::Result;
use crate::index::{build_index, query_index, read_index_stats, QueryResult};
use crate::store::Store;
use crate::types::{Key, KeyPrefix, Namespace, Value};
pub fn run() -> i32 {
match run_inner() {
Ok(()) => 0,
Err(error) => {
print_error(&error);
1
}
}
}
fn run_inner() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Init => {
let _ = run_onboarding()?;
}
Command::Info => {
let store = open_local_store()?;
render_info(&store);
}
Command::Get { key } => {
let store = open_local_store()?;
let key = Key::new(key)?;
if let Some(value) = store.get(&key) {
print_line(value.as_str());
} else {
print_info("key not found");
}
}
Command::Set { key, value } => {
let mut store = open_local_store_locked()?;
let key = Key::new(key)?;
let value = Value::new(value)?;
let _ = store.set(key, value)?;
store.flush()?;
print_success("value stored");
}
Command::Delete { key } => {
let mut store = open_local_store_locked()?;
let key = Key::new(key)?;
match store.delete(&key) {
Some(_) => {
store.flush()?;
print_success("key removed");
}
None => print_info("key not found"),
}
}
Command::List { prefix } => {
let store = open_local_store()?;
match prefix {
Some(prefix) => {
let prefix = KeyPrefix::new(prefix)?;
for entry in store.list_prefix(&prefix) {
print_line(format!("{} = {}", entry.key, entry.value));
}
}
None => {
for entry in store.entries() {
print_line(format!("{} = {}", entry.key, entry.value));
}
}
}
}
Command::Namespace { namespace } => {
let store = open_local_store()?;
let namespace = Namespace::new(namespace)?;
for entry in store.list_namespace(&namespace) {
print_line(format!("{} = {}", entry.key, entry.value));
}
}
Command::Clear => {
let confirmed = crate::cli::prompts::prompt_confirm("Delete all entries?", false)?;
if confirmed {
let mut store = open_local_store_locked()?;
store.clear();
store.flush()?;
print_success("store cleared");
} else {
print_info("cancelled");
}
}
Command::Index { command } => match command {
IndexCommand::Build { root } => {
let mut store = open_local_store_locked()?;
let root = root.unwrap_or(std::env::current_dir()?);
let report = build_index(&mut store, &root)?;
print_success("index built");
print_field("Root", report.root);
print_field("Files", report.file_count.to_string());
print_field("Skipped", report.skipped_files.to_string());
print_field("Chunks", report.chunk_count.to_string());
print_field("Tokens", report.token_count.to_string());
}
IndexCommand::Query {
query,
top_k,
token_budget,
} => {
let store = open_local_store()?;
let result = query_index(&store, &query, top_k, token_budget)?;
render_index_query(&result);
}
IndexCommand::Stats => {
let store = open_local_store()?;
let stats = read_index_stats(&store);
if !stats.built {
print_info("index not built");
} else {
print_heading("Index Stats");
print_field("Root", stats.root.unwrap_or_default());
print_field("Files", stats.file_count.to_string());
print_field("Chunks", stats.chunk_count.to_string());
print_field("Tokens", stats.token_count.to_string());
print_field(
"Built (unix)",
stats
.built_unix_seconds
.map_or_else(|| "unknown".to_owned(), |value| value.to_string()),
);
print_blank_line();
}
}
},
}
Ok(())
}
#[derive(Debug, Parser)]
#[command(
name = "agentmem",
version,
about = "Secure local memory for AI agents"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Init,
Info,
Get {
key: String,
},
Set {
key: String,
value: String,
},
Delete {
key: String,
},
List {
#[arg(long)]
prefix: Option<String>,
},
Namespace {
namespace: String,
},
Clear,
Index {
#[command(subcommand)]
command: IndexCommand,
},
}
#[derive(Debug, Subcommand)]
enum IndexCommand {
Build {
#[arg(long)]
root: Option<PathBuf>,
},
Query {
query: String,
#[arg(long, default_value_t = 8)]
top_k: usize,
#[arg(long, default_value_t = 4000)]
token_budget: usize,
},
Stats,
}
fn open_local_store() -> Result<Store> {
let config_path = resolve_local_config_path()?;
let config = Config::load(config_path)?;
Store::open(config)
}
fn open_local_store_locked() -> Result<Store> {
let config_path = resolve_local_config_path()?;
let config = Config::load(config_path)?;
Store::open_locked(config)
}
fn render_info(store: &Store) {
let info = store.info();
let stats = store.stats();
print_heading("Store Info");
print_field("Project", info.project_name.as_str());
print_field("Path", &info.path.to_string());
print_field("Entries", &stats.entry_count.to_string());
print_field("Locked", if stats.locked { "yes" } else { "no" });
print_field("Format", &info.format_version.to_string());
print_blank_line();
}
fn render_index_query(result: &QueryResult) {
print_heading("Index Query");
print_field("Query", &result.query);
print_field("Top K", result.top_k.to_string());
print_field("Token budget", result.token_budget.to_string());
print_field("Used tokens", result.used_tokens.to_string());
print_field("Confidence", format!("{:.2}", result.confidence));
print_field(
"Fallback required",
if result.fallback_required {
"yes"
} else {
"no"
},
);
print_field(
"Matched tokens",
if result.matched_tokens.is_empty() {
"(none)".to_owned()
} else {
result.matched_tokens.join(", ")
},
);
print_field(
"Missing tokens",
if result.missing_tokens.is_empty() {
"(none)".to_owned()
} else {
result.missing_tokens.join(", ")
},
);
print_blank_line();
if result.chunks.is_empty() {
print_info("no matching chunks");
return;
}
for chunk in &result.chunks {
print_line(format!(
"{}:{}-{} score={} est_tokens={}",
chunk.path, chunk.line_start, chunk.line_end, chunk.score, chunk.estimated_tokens
));
print_line(truncate_for_terminal(&chunk.content, 700));
print_blank_line();
}
}
fn truncate_for_terminal(input: &str, max_len: usize) -> String {
if input.len() <= max_len {
return input.to_owned();
}
let mut clipped = input.chars().take(max_len).collect::<String>();
clipped.push_str("...");
clipped
}