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