1use anyhow::{bail, Result};
7use clap::{Args, Subcommand};
8use memvid_core::Memvid;
9use serde::Serialize;
10use std::path::PathBuf;
11
12use crate::config::CliConfig;
13
14#[derive(Args)]
16pub struct FollowArgs {
17 #[command(subcommand)]
18 pub command: FollowCommand,
19}
20
21#[derive(Subcommand)]
22pub enum FollowCommand {
23 Traverse(FollowTraverseArgs),
25 Entities(FollowEntitiesArgs),
27 Stats(FollowStatsArgs),
29}
30
31#[derive(Args)]
33pub struct FollowTraverseArgs {
34 pub file: PathBuf,
36
37 #[arg(short, long)]
39 pub start: String,
40
41 #[arg(short, long)]
43 pub link: Option<String>,
44
45 #[arg(long, default_value = "2")]
47 pub hops: usize,
48
49 #[arg(long, default_value = "both")]
51 pub direction: String,
52
53 #[arg(long)]
55 pub json: bool,
56}
57
58#[derive(Args)]
60pub struct FollowEntitiesArgs {
61 pub file: PathBuf,
63
64 #[arg(short = 't', long)]
66 pub kind: Option<String>,
67
68 #[arg(short, long)]
70 pub query: Option<String>,
71
72 #[arg(short, long, default_value = "50")]
74 pub limit: usize,
75
76 #[arg(long)]
78 pub json: bool,
79}
80
81#[derive(Args)]
83pub struct FollowStatsArgs {
84 pub file: PathBuf,
86
87 #[arg(long)]
89 pub json: bool,
90}
91
92pub fn handle_follow(_config: &CliConfig, args: FollowArgs) -> Result<()> {
94 match args.command {
95 FollowCommand::Traverse(traverse_args) => handle_follow_traverse(traverse_args),
96 FollowCommand::Entities(entities_args) => handle_follow_entities(entities_args),
97 FollowCommand::Stats(stats_args) => handle_follow_stats(stats_args),
98 }
99}
100
101fn handle_follow_traverse(args: FollowTraverseArgs) -> Result<()> {
103 let mem = Memvid::open(&args.file)?;
104
105 if !mem.has_logic_mesh() {
107 bail!(
108 "No Logic-Mesh found in {}. Run `memvid put --logic-mesh` to build the graph.",
109 args.file.display()
110 );
111 }
112
113 let start_node = mem.find_entity(&args.start);
115 if start_node.is_none() {
116 bail!(
117 "Entity '{}' not found in Logic-Mesh. Use `memvid follow entities` to list available entities.",
118 args.start
119 );
120 }
121
122 let link = args.link.as_deref().unwrap_or("related");
123 let results = mem.follow(&args.start, link, args.hops);
124
125 if args.json {
126 println!("{}", serde_json::to_string_pretty(&results)?);
127 } else {
128 let start_node = start_node.unwrap();
129 println!(
130 "Starting from: {} ({})",
131 start_node.display_name,
132 start_node.kind.as_str()
133 );
134 println!("Following: {} relationships (up to {} hops)", link, args.hops);
135 println!();
136
137 if results.is_empty() {
138 println!("No {} relationships found.", link);
139 } else {
140 for result in &results {
141 println!(
142 " → {} ({}, confidence: {:.0}%)",
143 result.node,
144 result.kind.as_str(),
145 result.confidence * 100.0
146 );
147 if !result.frame_ids.is_empty() {
148 println!(" Frames: {:?}", result.frame_ids);
149 }
150 }
151 }
152 }
153
154 Ok(())
155}
156
157fn handle_follow_entities(args: FollowEntitiesArgs) -> Result<()> {
159 let mem = Memvid::open(&args.file)?;
160
161 if !mem.has_logic_mesh() {
162 bail!(
163 "No Logic-Mesh found in {}. Run `memvid put --logic-mesh` to build the graph.",
164 args.file.display()
165 );
166 }
167
168 let mesh = mem.logic_mesh();
169
170 let mut entities: Vec<_> = mesh.nodes.iter().collect();
172
173 if let Some(ref kind_filter) = args.kind {
174 let kind_lower = kind_filter.to_lowercase();
175 entities.retain(|node| node.kind.as_str() == kind_lower);
176 }
177
178 if let Some(ref query) = args.query {
180 let query_lower = query.to_lowercase();
181 entities.retain(|node| {
182 node.display_name.to_lowercase().contains(&query_lower)
183 || node.canonical_name.contains(&query_lower)
184 });
185 }
186
187 entities.sort_by(|a, b| a.display_name.cmp(&b.display_name));
189
190 let limited: Vec<_> = entities.into_iter().take(args.limit).collect();
192
193 if args.json {
194 #[derive(Serialize)]
195 struct EntityOutput {
196 name: String,
197 kind: String,
198 confidence: f32,
199 frame_count: usize,
200 frame_ids: Vec<u64>,
201 }
202 let output: Vec<EntityOutput> = limited
203 .iter()
204 .map(|node| EntityOutput {
205 name: node.display_name.clone(),
206 kind: node.kind.as_str().to_string(),
207 confidence: node.confidence_f32(),
208 frame_count: node.frame_ids.len(),
209 frame_ids: node.frame_ids.clone(),
210 })
211 .collect();
212 println!("{}", serde_json::to_string_pretty(&output)?);
213 } else {
214 println!(
215 "Entities in Logic-Mesh (showing {} of {})",
216 limited.len(),
217 mesh.nodes.len()
218 );
219 println!();
220 for node in &limited {
221 println!(
222 " {} ({}, confidence: {:.0}%, {} frames)",
223 node.display_name,
224 node.kind.as_str(),
225 node.confidence_f32() * 100.0,
226 node.frame_ids.len()
227 );
228 }
229 if limited.len() < mesh.nodes.len() {
230 println!();
231 println!("Use --limit to show more entities, or --query to filter.");
232 }
233 }
234
235 Ok(())
236}
237
238fn handle_follow_stats(args: FollowStatsArgs) -> Result<()> {
240 let mem = Memvid::open(&args.file)?;
241
242 if !mem.has_logic_mesh() {
243 if args.json {
244 println!(r#"{{"error": "No Logic-Mesh found"}}"#);
245 } else {
246 println!("No Logic-Mesh found in {}", args.file.display());
247 println!("Run `memvid put --logic-mesh` to build the graph.");
248 }
249 return Ok(());
250 }
251
252 let stats = mem.logic_mesh_stats();
253 let manifest = mem.logic_mesh_manifest();
254
255 if args.json {
256 #[derive(Serialize)]
257 struct MeshStatsOutput {
258 node_count: usize,
259 edge_count: usize,
260 entity_kinds: std::collections::HashMap<String, usize>,
261 link_types: std::collections::HashMap<String, usize>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 bytes_offset: Option<u64>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 bytes_length: Option<u64>,
266 }
267 let output = MeshStatsOutput {
268 node_count: stats.node_count,
269 edge_count: stats.edge_count,
270 entity_kinds: stats.entity_kinds,
271 link_types: stats.link_types,
272 bytes_offset: manifest.map(|m| m.bytes_offset),
273 bytes_length: manifest.map(|m| m.bytes_length),
274 };
275 println!("{}", serde_json::to_string_pretty(&output)?);
276 } else {
277 println!("Logic-Mesh Statistics");
278 println!("=====================");
279 println!(" Nodes (entities): {}", stats.node_count);
280 println!(" Edges (relations): {}", stats.edge_count);
281 if !stats.entity_kinds.is_empty() {
282 println!();
283 println!(" Entity Kinds:");
284 let mut kinds: Vec<_> = stats.entity_kinds.iter().collect();
285 kinds.sort_by(|a, b| b.1.cmp(a.1));
286 for (kind, count) in kinds {
287 println!(" {}: {}", kind, count);
288 }
289 }
290 if !stats.link_types.is_empty() {
291 println!();
292 println!(" Relationship Types:");
293 let mut links: Vec<_> = stats.link_types.iter().collect();
294 links.sort_by(|a, b| b.1.cmp(a.1));
295 for (link, count) in links {
296 println!(" {}: {}", link, count);
297 }
298 }
299 if let Some(m) = manifest {
300 println!();
301 println!(" Storage offset: {}", m.bytes_offset);
302 println!(" Storage size: {} bytes", m.bytes_length);
303 }
304 }
305
306 Ok(())
307}
308
309#[cfg(test)]
310mod tests {
311 #[test]
312 fn test_direction_parsing() {
313 let directions = vec!["outgoing", "incoming", "both"];
315 for d in directions {
316 assert!(!d.is_empty());
317 }
318 }
319}