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};
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 = "no-lex", action = ArgAction::SetTrue)]
49 pub no_lex: bool,
50 #[arg(long = "no-vector", aliases = ["no-vec"], action = ArgAction::SetTrue)]
52 pub no_vector: bool,
53}
54
55#[derive(Args)]
57pub struct OpenArgs {
58 #[arg(value_name = "FILE", value_parser = clap::value_parser!(PathBuf))]
60 pub file: PathBuf,
61 #[arg(long)]
63 pub json: bool,
64}
65
66pub fn handle_create(config: &CliConfig, args: CreateArgs) -> Result<()> {
68 if let Some(parent) = args.file.parent() {
69 if !parent.exists() {
70 fs::create_dir_all(parent)?;
71 }
72 }
73
74 let has_memory_id = args.memory_id.is_some();
79
80 let initial_capacity = args.size.unwrap_or(FREE_TIER_MAX_FILE_SIZE);
83 let mut capacity_bytes = initial_capacity.min(FREE_TIER_MAX_FILE_SIZE);
84
85 if let Some(requested) = args.size {
88 if requested > FREE_TIER_MAX_FILE_SIZE && !has_memory_id {
89 eprintln!(
90 "⚠️ Requested capacity {} exceeds free tier limit ({}).",
91 format_bytes(requested),
92 format_bytes(FREE_TIER_MAX_FILE_SIZE)
93 );
94 eprintln!(" To unlock more capacity, bind to a dashboard memory:");
95 eprintln!(" memvid create {} --memory-id <MEMORY_ID>", args.file.display());
96 eprintln!(" Or bind an existing file: memvid tickets sync {} --memory-id <MEMORY_ID>", args.file.display());
97 capacity_bytes = FREE_TIER_MAX_FILE_SIZE;
98 }
99 }
100
101 let lexical_enabled = !args.no_lex;
102 let vector_enabled = !args.no_vector;
103
104 let mut mem = Memvid::create(&args.file)?;
105 apply_capacity_override(&mut mem, capacity_bytes)?;
106 if lexical_enabled {
107 mem.enable_lex()?;
108 }
109
110 if vector_enabled {
111 mem.enable_vec()?;
112 }
113 mem.commit()?;
114
115 let binding_info = if let Some(memory_id) = &args.memory_id {
117 match bind_to_dashboard_memory(config, &mut mem, &args.file, memory_id) {
118 Ok(info) => Some(info),
119 Err(e) => {
120 eprintln!("⚠️ Failed to bind to dashboard memory: {}", e);
121 eprintln!(" File created with free tier capacity. You can bind later with:");
122 eprintln!(" memvid tickets sync {} --memory-id {}", args.file.display(), memory_id);
123 None
124 }
125 }
126 } else {
127 None
128 };
129
130 let stats = mem.stats()?;
131
132 let filename = args.file.display();
134 println!("✓ Created memory at {}", filename);
135 if let Some((bound_id, bound_capacity)) = &binding_info {
136 println!(" Bound to: {}", bound_id);
137 println!(" Capacity: {} (from dashboard)", format_bytes(*bound_capacity));
138 } else {
139 println!(" Tier: Free");
140 println!(
141 " Capacity: {} ({} bytes)",
142 format_bytes(stats.capacity_bytes),
143 stats.capacity_bytes
144 );
145 }
146 println!(" Size: {}", format_bytes(stats.size_bytes));
147 println!(
148 " Indexes: {} | {}",
149 if lexical_enabled { "lexical" } else { "no-lex" },
150 if vector_enabled { "vector" } else { "no-vec" }
151 );
152 println!();
153 println!("Next steps:");
154 println!(" memvid put {} --input <file> # Add content", filename);
155 println!(" memvid find {} --query <text> # Search", filename);
156 println!(" memvid stats {} # View stats", filename);
157 println!();
158 if binding_info.is_none() {
159 println!("Tip: Bind to a dashboard memory to unlock your plan's capacity:");
160 println!(" memvid tickets sync {} --memory-id <MEMORY_ID>", filename);
161 }
162 println!("Documentation: https://docs.memvid.com/cli/tickets-and-capacity");
163 Ok(())
164}
165
166pub fn bind_to_dashboard_memory(
168 config: &CliConfig,
169 mem: &mut Memvid,
170 file_path: &PathBuf,
171 memory_id: &MemoryId,
172) -> Result<(String, u64)> {
173 use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
174 use chrono::Utc;
175 use memvid_core::types::MemoryBinding;
176 use memvid_core::verify_ticket_signature;
177
178 use crate::api::{fetch_ticket as api_fetch_ticket, register_file, RegisterFileRequest};
179 use crate::ticket_cache::{store as store_cached_ticket, CachedTicket};
180
181 if config.api_key.is_none() {
183 anyhow::bail!("API key required for dashboard binding. Set MEMVID_API_KEY.");
184 }
185
186 let pubkey = config
187 .ticket_pubkey
188 .as_ref()
189 .ok_or_else(|| anyhow::anyhow!("Ticket verification key not available"))?;
190
191 let response = api_fetch_ticket(config, memory_id)
193 .context("failed to fetch ticket from dashboard")?;
194
195 let signature_bytes = BASE64_STANDARD
197 .decode(response.payload.signature.as_bytes())
198 .context("ticket signature is not valid base64")?;
199
200 verify_ticket_signature(
201 pubkey,
202 memory_id,
203 &response.payload.issuer,
204 response.payload.sequence,
205 response.payload.expires_in,
206 response.payload.capacity_bytes,
207 &signature_bytes,
208 )
209 .context("ticket signature verification failed")?;
210
211 let mut ticket = Ticket::new(&response.payload.issuer, response.payload.sequence)
213 .expires_in_secs(response.payload.expires_in);
214 if let Some(capacity) = response.payload.capacity_bytes {
215 ticket = ticket.capacity_bytes(capacity);
216 }
217
218 let binding = MemoryBinding {
220 memory_id: **memory_id,
221 memory_name: response.payload.issuer.clone(),
222 bound_at: Utc::now(),
223 api_url: config.api_url.clone(),
224 };
225
226 let api_key = config.api_key.as_deref()
228 .ok_or_else(|| anyhow::anyhow!("API key required for file registration"))?;
229 let abs_path = std::fs::canonicalize(file_path)
230 .unwrap_or_else(|_| file_path.clone());
231 let file_metadata = std::fs::metadata(file_path)?;
232 let file_name = file_path
233 .file_name()
234 .and_then(|n| n.to_str())
235 .unwrap_or("unknown.mv2");
236 let machine_id = hostname::get()
237 .map(|h| h.to_string_lossy().to_string())
238 .unwrap_or_else(|_| "unknown".to_string());
239
240 let abs_path_str = abs_path.to_string_lossy();
241 let register_request = RegisterFileRequest {
242 file_name,
243 file_path: &abs_path_str,
244 file_size: file_metadata.len() as i64,
245 machine_id: &machine_id,
246 };
247 register_file(config, memory_id, ®ister_request, api_key)
249 .context("failed to register file with dashboard")?;
250
251 mem.bind_memory(binding, ticket)
253 .context("failed to bind memory")?;
254 mem.commit()?;
255
256 let cache_entry = CachedTicket {
258 memory_id: **memory_id,
259 issuer: response.payload.issuer.clone(),
260 seq_no: response.payload.sequence,
261 expires_in: response.payload.expires_in,
262 capacity_bytes: response.payload.capacity_bytes,
263 signature: response.payload.signature.clone(),
264 };
265 store_cached_ticket(config, &cache_entry)?;
266
267 let capacity = mem.get_capacity();
268 Ok((memory_id.to_string(), capacity))
269}
270
271pub fn handle_open(_config: &CliConfig, args: OpenArgs) -> Result<()> {
273 let mem = open_read_only_mem(&args.file)?;
274 let stats = mem.stats()?;
275 if args.json {
276 println!("{}", serde_json::to_string_pretty(&stats)?);
277 } else {
278 println!("Memory: {}", args.file.display());
279 println!("Frames: {}", stats.frame_count);
280 println!("Size: {} bytes", stats.size_bytes);
281 println!("Tier: {:?}", stats.tier);
282 println!(
283 "Indices → lex: {}, vec: {}, time: {}",
284 yes_no(stats.has_lex_index),
285 yes_no(stats.has_vec_index),
286 yes_no(stats.has_time_index)
287 );
288 if let Some(seq) = stats.seq_no {
289 println!("Ticket sequence: {seq}");
290 }
291 }
292 Ok(())
293}
294
295pub fn apply_capacity_override(mem: &mut Memvid, capacity_bytes: u64) -> Result<()> {
298 let current = mem.current_ticket();
299 if current.capacity_bytes == capacity_bytes {
300 return Ok(());
301 }
302
303 let seq = current.seq_no.saturating_add(1).max(1);
304 let mut ticket = Ticket::new(current.issuer.clone(), seq).capacity_bytes(capacity_bytes);
305 if current.expires_in_secs != 0 {
306 ticket = ticket.expires_in_secs(current.expires_in_secs);
307 }
308 apply_ticket_with_warning(mem, ticket)?;
309 Ok(())
310}
311
312pub fn apply_ticket_with_warning(mem: &mut Memvid, ticket: Ticket) -> Result<()> {
313 let before = mem.stats()?.capacity_bytes;
314 mem.apply_ticket(ticket)?;
315 let after = mem.stats()?.capacity_bytes;
316 if after < before {
317 println!(
318 "Warning: capacity reduced from {} to {}",
319 format_bytes(before),
320 format_bytes(after)
321 );
322 }
323 Ok(())
324}