1use std::fs;
4use std::path::PathBuf;
5
6use anyhow::{Context, Result};
7use clap::{ArgAction, Args, ValueEnum};
8use memvid_core::{Memvid, Ticket};
9
10use crate::commands::tickets::MemoryId;
11use crate::config::CliConfig;
12use crate::utils::{
13 format_bytes, open_read_only_mem, parse_size, yes_no, FREE_TIER_MAX_FILE_SIZE, MIN_FILE_SIZE,
14};
15
16#[derive(Clone, Copy, Debug, ValueEnum)]
18pub enum TierArg {
19 Free,
20 Dev,
21 Enterprise,
22}
23
24impl From<TierArg> for memvid_core::Tier {
25 fn from(value: TierArg) -> Self {
26 match value {
27 TierArg::Free => memvid_core::Tier::Free,
28 TierArg::Dev => memvid_core::Tier::Dev,
29 TierArg::Enterprise => memvid_core::Tier::Enterprise,
30 }
31 }
32}
33
34#[derive(Args)]
36pub struct CreateArgs {
37 #[arg(value_name = "FILE", value_parser = clap::value_parser!(PathBuf))]
39 pub file: PathBuf,
40 #[arg(long, value_enum)]
42 pub tier: Option<TierArg>,
43 #[arg(long = "size", alias = "capacity", value_name = "SIZE", value_parser = parse_size)]
45 pub size: Option<u64>,
46 #[arg(long = "memory-id", value_name = "ID")]
48 pub memory_id: Option<MemoryId>,
49 #[arg(long = "memory", value_name = "NAME")]
51 pub memory_name: Option<String>,
52 #[arg(long = "no-lex", action = ArgAction::SetTrue)]
54 pub no_lex: bool,
55 #[arg(long = "no-vector", aliases = ["no-vec"], action = ArgAction::SetTrue)]
57 pub no_vector: bool,
58}
59
60#[derive(Args)]
62pub struct OpenArgs {
63 #[arg(value_name = "FILE", value_parser = clap::value_parser!(PathBuf))]
65 pub file: PathBuf,
66 #[arg(long)]
68 pub json: bool,
69}
70
71pub fn handle_create(config: &CliConfig, args: CreateArgs) -> Result<()> {
73 if let Some(parent) = args.file.parent() {
74 if !parent.exists() {
75 fs::create_dir_all(parent)?;
76 }
77 }
78
79 let effective_memory_id: Option<MemoryId> = args.memory_id.clone().or_else(|| {
86 if let Some(ref name) = args.memory_name {
88 if let Ok(persistent_config) = crate::commands::config::PersistentConfig::load() {
89 if let Some(id) = persistent_config.get_memory(name) {
90 return id.parse::<MemoryId>().ok();
91 } else {
92 eprintln!(
93 "⚠️ Memory '{}' not found in config. Available memories:",
94 name
95 );
96 for (mem_name, _) in &persistent_config.memory {
97 eprintln!(" - {}", mem_name);
98 }
99 eprintln!(
100 " Set it with: memvid config set memory.{} <MEMORY_ID>",
101 name
102 );
103 }
104 }
105 return None;
106 }
107 config
109 .memory_id
110 .as_ref()
111 .and_then(|id| id.parse::<MemoryId>().ok())
112 });
113 let has_memory_id = effective_memory_id.is_some();
114
115 if let Some(requested) = args.size {
117 if requested < MIN_FILE_SIZE {
119 anyhow::bail!(
120 "Requested capacity {} is below minimum ({}).\n\
121 Minimum memory size is {} to ensure reasonable capacity for basic usage.",
122 format_bytes(requested),
123 format_bytes(MIN_FILE_SIZE),
124 format_bytes(MIN_FILE_SIZE)
125 );
126 }
127
128 if requested > FREE_TIER_MAX_FILE_SIZE && !has_memory_id {
130 anyhow::bail!(
131 "Requested capacity {} exceeds free tier limit ({}).\n\
132 To unlock more capacity, bind to a dashboard memory:\n\
133 memvid create {} --memory-id <MEMORY_ID>\n\
134 Or bind an existing file:\n\
135 memvid tickets sync {} --memory-id <MEMORY_ID>",
136 format_bytes(requested),
137 format_bytes(FREE_TIER_MAX_FILE_SIZE),
138 args.file.display(),
139 args.file.display()
140 );
141 }
142 }
143
144 let initial_capacity = args.size.unwrap_or(FREE_TIER_MAX_FILE_SIZE);
147 let capacity_bytes = initial_capacity.min(FREE_TIER_MAX_FILE_SIZE);
148
149 let lexical_enabled = !args.no_lex;
150 let vector_enabled = !args.no_vector;
151
152 let mut mem = Memvid::create(&args.file)?;
153 apply_capacity_override(&mut mem, capacity_bytes)?;
154 if lexical_enabled {
155 mem.enable_lex()?;
156 }
157
158 if vector_enabled {
159 mem.enable_vec()?;
160 }
161 mem.commit()?;
162
163 let binding_info = if let Some(memory_id) = &effective_memory_id {
165 match bind_to_dashboard_memory(config, &mut mem, &args.file, memory_id) {
166 Ok(info) => Some(info),
167 Err(e) => {
168 let root_cause = e.root_cause();
170 eprintln!("⚠️ Failed to bind to dashboard memory: {}", root_cause);
171 eprintln!(" File created with free tier capacity. You can bind later with:");
172 eprintln!(
173 " memvid tickets sync {} --memory-id {}",
174 args.file.display(),
175 memory_id
176 );
177 None
178 }
179 }
180 } else {
181 None
182 };
183
184 let stats = mem.stats()?;
185
186 let filename = args.file.display();
188 println!("✓ Created memory at {}", filename);
189 if let Some((bound_id, bound_capacity)) = &binding_info {
190 println!(" Bound to: {}", bound_id);
191 println!(
192 " Capacity: {} (from dashboard)",
193 format_bytes(*bound_capacity)
194 );
195 } else {
196 println!(" Tier: Free");
197 println!(
198 " Capacity: {} ({} bytes)",
199 format_bytes(stats.capacity_bytes),
200 stats.capacity_bytes
201 );
202 }
203 println!(" Size: {}", format_bytes(stats.size_bytes));
204 println!(
205 " Indexes: {} | {}",
206 if lexical_enabled { "lexical" } else { "no-lex" },
207 if vector_enabled { "vector" } else { "no-vec" }
208 );
209 println!();
210 println!("Next steps:");
211 println!(" memvid put {} --input <file> # Add content", filename);
212 println!(" memvid find {} --query <text> # Search", filename);
213 println!(" memvid stats {} # View stats", filename);
214 println!();
215 if binding_info.is_none() {
216 println!("Tip: Bind to a dashboard memory to unlock your plan's capacity:");
217 println!(
218 " memvid tickets sync {} --memory-id <MEMORY_ID>",
219 filename
220 );
221 }
222 println!("Documentation: https://docs.memvid.com/cli/tickets-and-capacity");
223 Ok(())
224}
225
226pub fn bind_to_dashboard_memory(
228 config: &CliConfig,
229 mem: &mut Memvid,
230 file_path: &PathBuf,
231 memory_id: &MemoryId,
232) -> Result<(String, u64)> {
233 use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
234 use chrono::Utc;
235 use memvid_core::types::MemoryBinding;
236 use memvid_core::verify_ticket_signature;
237
238 use crate::api::{fetch_ticket as api_fetch_ticket, register_file, RegisterFileRequest};
239 use crate::ticket_cache::{store as store_cached_ticket, CachedTicket};
240
241 if config.api_key.is_none() {
243 anyhow::bail!("API key required for dashboard binding. Set MEMVID_API_KEY.");
244 }
245
246 let pubkey = config
247 .ticket_pubkey
248 .as_ref()
249 .ok_or_else(|| anyhow::anyhow!("Ticket verification key not available"))?;
250
251 let response =
253 api_fetch_ticket(config, memory_id).context("failed to fetch ticket from dashboard")?;
254
255 let signature_bytes = BASE64_STANDARD
257 .decode(response.payload.signature.as_bytes())
258 .context("ticket signature is not valid base64")?;
259
260 verify_ticket_signature(
261 pubkey,
262 memory_id,
263 &response.payload.issuer,
264 response.payload.sequence,
265 response.payload.expires_in,
266 response.payload.capacity_bytes,
267 &signature_bytes,
268 )
269 .context("ticket signature verification failed")?;
270
271 let mut ticket = Ticket::new(&response.payload.issuer, response.payload.sequence)
273 .expires_in_secs(response.payload.expires_in);
274 if let Some(capacity) = response.payload.capacity_bytes {
275 ticket = ticket.capacity_bytes(capacity);
276 }
277
278 let binding = MemoryBinding {
280 memory_id: **memory_id,
281 memory_name: response.payload.issuer.clone(),
282 bound_at: Utc::now(),
283 api_url: config.api_url.clone(),
284 };
285
286 let api_key = config
288 .api_key
289 .as_deref()
290 .ok_or_else(|| anyhow::anyhow!("API key required for file registration"))?;
291 let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|_| file_path.clone());
292 let file_metadata = std::fs::metadata(file_path)?;
293 let file_name = file_path
294 .file_name()
295 .and_then(|n| n.to_str())
296 .unwrap_or("unknown.mv2");
297 let machine_id = hostname::get()
298 .map(|h| h.to_string_lossy().to_string())
299 .unwrap_or_else(|_| "unknown".to_string());
300
301 let abs_path_str = abs_path.to_string_lossy();
302 let register_request = RegisterFileRequest {
303 file_name,
304 file_path: &abs_path_str,
305 file_size: file_metadata.len() as i64,
306 machine_id: &machine_id,
307 };
308 register_file(config, memory_id, ®ister_request, api_key)
310 .context("failed to register file with dashboard")?;
311
312 mem.bind_memory(binding, ticket)
314 .context("failed to bind memory")?;
315 mem.commit()?;
316
317 let cache_entry = CachedTicket {
319 memory_id: **memory_id,
320 issuer: response.payload.issuer.clone(),
321 seq_no: response.payload.sequence,
322 expires_in: response.payload.expires_in,
323 capacity_bytes: response.payload.capacity_bytes,
324 signature: response.payload.signature.clone(),
325 };
326 store_cached_ticket(config, &cache_entry)?;
327
328 let capacity = mem.get_capacity();
329 Ok((memory_id.to_string(), capacity))
330}
331
332pub fn handle_open(_config: &CliConfig, args: OpenArgs) -> Result<()> {
334 let mem = open_read_only_mem(&args.file)?;
335 let stats = mem.stats()?;
336 if args.json {
337 println!("{}", serde_json::to_string_pretty(&stats)?);
338 } else {
339 println!("Memory: {}", args.file.display());
340 println!("Frames: {}", stats.frame_count);
341 println!("Size: {} bytes", stats.size_bytes);
342 println!("Tier: {:?}", stats.tier);
343 println!(
344 "Indices → lex: {}, vec: {}, time: {}",
345 yes_no(stats.has_lex_index),
346 yes_no(stats.has_vec_index),
347 yes_no(stats.has_time_index)
348 );
349 if let Some(seq) = stats.seq_no {
350 println!("Ticket sequence: {seq}");
351 }
352 }
353 Ok(())
354}
355
356pub fn apply_capacity_override(mem: &mut Memvid, capacity_bytes: u64) -> Result<()> {
359 let current = mem.current_ticket();
360 if current.capacity_bytes == capacity_bytes {
361 return Ok(());
362 }
363
364 let seq = current.seq_no.saturating_add(1).max(1);
365 let mut ticket = Ticket::new(current.issuer.clone(), seq).capacity_bytes(capacity_bytes);
366 if current.expires_in_secs != 0 {
367 ticket = ticket.expires_in_secs(current.expires_in_secs);
368 }
369 apply_ticket_with_warning(mem, ticket)?;
370 Ok(())
371}
372
373#[allow(deprecated)]
377pub fn apply_ticket_with_warning(mem: &mut Memvid, ticket: Ticket) -> Result<()> {
378 let before = mem.stats()?.capacity_bytes;
379 mem.apply_ticket(ticket)?;
380 let after = mem.stats()?.capacity_bytes;
381 if after < before {
382 println!(
383 "Warning: capacity reduced from {} to {}",
384 format_bytes(before),
385 format_bytes(after)
386 );
387 }
388 Ok(())
389}