use std::fs;
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{ArgAction, Args, ValueEnum};
use memvid_core::{Memvid, Ticket};
use crate::commands::tickets::MemoryId;
use crate::config::CliConfig;
use crate::utils::{
format_bytes, open_read_only_mem, parse_size, yes_no, FREE_TIER_MAX_FILE_SIZE, MIN_FILE_SIZE,
};
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum TierArg {
Free,
Dev,
Enterprise,
}
impl From<TierArg> for memvid_core::Tier {
fn from(value: TierArg) -> Self {
match value {
TierArg::Free => memvid_core::Tier::Free,
TierArg::Dev => memvid_core::Tier::Dev,
TierArg::Enterprise => memvid_core::Tier::Enterprise,
}
}
}
#[derive(Args)]
pub struct CreateArgs {
#[arg(value_name = "FILE", value_parser = clap::value_parser!(PathBuf))]
pub file: PathBuf,
#[arg(long, value_enum)]
pub tier: Option<TierArg>,
#[arg(long = "size", alias = "capacity", value_name = "SIZE", value_parser = parse_size)]
pub size: Option<u64>,
#[arg(long = "memory-id", value_name = "ID")]
pub memory_id: Option<MemoryId>,
#[arg(long = "memory", value_name = "NAME")]
pub memory_name: Option<String>,
#[arg(long = "no-lex", action = ArgAction::SetTrue)]
pub no_lex: bool,
#[arg(long = "no-vector", aliases = ["no-vec"], action = ArgAction::SetTrue)]
pub no_vector: bool,
}
#[derive(Args)]
pub struct OpenArgs {
#[arg(value_name = "FILE", value_parser = clap::value_parser!(PathBuf))]
pub file: PathBuf,
#[arg(long)]
pub json: bool,
}
pub fn handle_create(config: &CliConfig, args: CreateArgs) -> Result<()> {
if let Some(parent) = args.file.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let effective_memory_id: Option<MemoryId> = args.memory_id.clone().or_else(|| {
if let Some(ref name) = args.memory_name {
if let Ok(persistent_config) = crate::commands::config::PersistentConfig::load() {
if let Some(id) = persistent_config.get_memory(name) {
return id.parse::<MemoryId>().ok();
} else {
eprintln!(
"⚠️ Memory '{}' not found in config. Available memories:",
name
);
for (mem_name, _) in &persistent_config.memory {
eprintln!(" - {}", mem_name);
}
eprintln!(
" Set it with: memvid config set memory.{} <MEMORY_ID>",
name
);
}
}
return None;
}
config
.memory_id
.as_ref()
.and_then(|id| id.parse::<MemoryId>().ok())
});
let has_memory_id = effective_memory_id.is_some();
if let Some(requested) = args.size {
if requested < MIN_FILE_SIZE {
anyhow::bail!(
"Requested capacity {} is below minimum ({}).\n\
Minimum memory size is {} to ensure reasonable capacity for basic usage.",
format_bytes(requested),
format_bytes(MIN_FILE_SIZE),
format_bytes(MIN_FILE_SIZE)
);
}
if requested > FREE_TIER_MAX_FILE_SIZE && !has_memory_id {
anyhow::bail!(
"Requested capacity {} exceeds free tier limit ({}).\n\
To unlock more capacity, bind to a dashboard memory:\n\
memvid create {} --memory-id <MEMORY_ID>\n\
Or bind an existing file:\n\
memvid tickets sync {} --memory-id <MEMORY_ID>",
format_bytes(requested),
format_bytes(FREE_TIER_MAX_FILE_SIZE),
args.file.display(),
args.file.display()
);
}
}
let initial_capacity = args.size.unwrap_or(FREE_TIER_MAX_FILE_SIZE);
let capacity_bytes = initial_capacity.min(FREE_TIER_MAX_FILE_SIZE);
let lexical_enabled = !args.no_lex;
let vector_enabled = !args.no_vector;
let mut mem = Memvid::create(&args.file)?;
apply_capacity_override(&mut mem, capacity_bytes)?;
if lexical_enabled {
mem.enable_lex()?;
}
if vector_enabled {
mem.enable_vec()?;
}
mem.commit()?;
let binding_info = if let Some(memory_id) = &effective_memory_id {
match bind_to_dashboard_memory(config, &mut mem, &args.file, memory_id) {
Ok(info) => Some(info),
Err(e) => {
let root_cause = e.root_cause();
eprintln!("⚠️ Failed to bind to dashboard memory: {}", root_cause);
eprintln!(" File created with free tier capacity. You can bind later with:");
eprintln!(
" memvid tickets sync {} --memory-id {}",
args.file.display(),
memory_id
);
None
}
}
} else {
None
};
let stats = mem.stats()?;
let filename = args.file.display();
println!("✓ Created memory at {}", filename);
if let Some((bound_id, bound_capacity)) = &binding_info {
println!(" Bound to: {}", bound_id);
println!(
" Capacity: {} (from dashboard)",
format_bytes(*bound_capacity)
);
} else {
println!(" Tier: Free");
println!(
" Capacity: {} ({} bytes)",
format_bytes(stats.capacity_bytes),
stats.capacity_bytes
);
}
println!(" Size: {}", format_bytes(stats.size_bytes));
println!(
" Indexes: {} | {}",
if lexical_enabled { "lexical" } else { "no-lex" },
if vector_enabled { "vector" } else { "no-vec" }
);
println!();
println!("Next steps:");
println!(" memvid put {} --input <file> # Add content", filename);
println!(" memvid find {} --query <text> # Search", filename);
println!(" memvid stats {} # View stats", filename);
println!();
if binding_info.is_none() {
println!("Tip: Bind to a dashboard memory to unlock your plan's capacity:");
println!(
" memvid tickets sync {} --memory-id <MEMORY_ID>",
filename
);
}
println!("Documentation: https://docs.memvid.com/cli/tickets-and-capacity");
Ok(())
}
pub fn bind_to_dashboard_memory(
config: &CliConfig,
mem: &mut Memvid,
file_path: &PathBuf,
memory_id: &MemoryId,
) -> Result<(String, u64)> {
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use chrono::Utc;
use memvid_core::types::MemoryBinding;
use memvid_core::verify_ticket_signature;
use crate::api::{fetch_ticket as api_fetch_ticket, register_file, RegisterFileRequest};
use crate::ticket_cache::{store as store_cached_ticket, CachedTicket};
if config.api_key.is_none() {
anyhow::bail!("API key required for dashboard binding. Set MEMVID_API_KEY.");
}
let pubkey = config
.ticket_pubkey
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Ticket verification key not available"))?;
let response =
api_fetch_ticket(config, memory_id).context("failed to fetch ticket from dashboard")?;
let signature_bytes = BASE64_STANDARD
.decode(response.payload.signature.as_bytes())
.context("ticket signature is not valid base64")?;
verify_ticket_signature(
pubkey,
memory_id,
&response.payload.issuer,
response.payload.sequence,
response.payload.expires_in,
response.payload.capacity_bytes,
&signature_bytes,
)
.context("ticket signature verification failed")?;
let mut ticket = Ticket::new(&response.payload.issuer, response.payload.sequence)
.expires_in_secs(response.payload.expires_in);
if let Some(capacity) = response.payload.capacity_bytes {
ticket = ticket.capacity_bytes(capacity);
}
let binding = MemoryBinding {
memory_id: **memory_id,
memory_name: response.payload.issuer.clone(),
bound_at: Utc::now(),
api_url: config.api_url.clone(),
};
let api_key = config
.api_key
.as_deref()
.ok_or_else(|| anyhow::anyhow!("API key required for file registration"))?;
let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|_| file_path.clone());
let file_metadata = std::fs::metadata(file_path)?;
let file_name = file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.mv2");
let machine_id = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string());
let abs_path_str = abs_path.to_string_lossy();
let register_request = RegisterFileRequest {
file_name,
file_path: &abs_path_str,
file_size: file_metadata.len() as i64,
machine_id: &machine_id,
};
register_file(config, memory_id, ®ister_request, api_key)
.context("failed to register file with dashboard")?;
mem.bind_memory(binding, ticket)
.context("failed to bind memory")?;
mem.commit()?;
let cache_entry = CachedTicket {
memory_id: **memory_id,
issuer: response.payload.issuer.clone(),
seq_no: response.payload.sequence,
expires_in: response.payload.expires_in,
capacity_bytes: response.payload.capacity_bytes,
signature: response.payload.signature.clone(),
};
store_cached_ticket(config, &cache_entry)?;
let capacity = mem.get_capacity();
Ok((memory_id.to_string(), capacity))
}
pub fn handle_open(_config: &CliConfig, args: OpenArgs) -> Result<()> {
let mem = open_read_only_mem(&args.file)?;
let stats = mem.stats()?;
if args.json {
println!("{}", serde_json::to_string_pretty(&stats)?);
} else {
println!("Memory: {}", args.file.display());
println!("Frames: {}", stats.frame_count);
println!("Size: {} bytes", stats.size_bytes);
println!("Tier: {:?}", stats.tier);
println!(
"Indices → lex: {}, vec: {}, time: {}",
yes_no(stats.has_lex_index),
yes_no(stats.has_vec_index),
yes_no(stats.has_time_index)
);
if let Some(seq) = stats.seq_no {
println!("Ticket sequence: {seq}");
}
}
Ok(())
}
pub fn apply_capacity_override(mem: &mut Memvid, capacity_bytes: u64) -> Result<()> {
let current = mem.current_ticket();
if current.capacity_bytes == capacity_bytes {
return Ok(());
}
let seq = current.seq_no.saturating_add(1).max(1);
let mut ticket = Ticket::new(current.issuer.clone(), seq).capacity_bytes(capacity_bytes);
if current.expires_in_secs != 0 {
ticket = ticket.expires_in_secs(current.expires_in_secs);
}
apply_ticket_with_warning(mem, ticket)?;
Ok(())
}
#[allow(deprecated)]
pub fn apply_ticket_with_warning(mem: &mut Memvid, ticket: Ticket) -> Result<()> {
let before = mem.stats()?.capacity_bytes;
mem.apply_ticket(ticket)?;
let after = mem.stats()?.capacity_bytes;
if after < before {
println!(
"Warning: capacity reduced from {} to {}",
format_bytes(before),
format_bytes(after)
);
}
Ok(())
}