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!(
135 "Following: {} relationships (up to {} hops)",
136 link, args.hops
137 );
138 println!();
139
140 if results.is_empty() {
141 println!("No {} relationships found.", link);
142 } else {
143 for result in &results {
144 println!(
145 " → {} ({}, confidence: {:.0}%)",
146 result.node,
147 result.kind.as_str(),
148 result.confidence * 100.0
149 );
150 if !result.frame_ids.is_empty() {
151 println!(" Frames: {:?}", result.frame_ids);
152 }
153 }
154 }
155 }
156
157 Ok(())
158}
159
160fn handle_follow_entities(args: FollowEntitiesArgs) -> Result<()> {
162 let mem = Memvid::open(&args.file)?;
163
164 if !mem.has_logic_mesh() {
165 bail!(
166 "No Logic-Mesh found in {}. Run `memvid put --logic-mesh` to build the graph.",
167 args.file.display()
168 );
169 }
170
171 let mesh = mem.logic_mesh();
172
173 let mut entities: Vec<_> = mesh.nodes.iter().collect();
175
176 if let Some(ref kind_filter) = args.kind {
177 let kind_lower = kind_filter.to_lowercase();
178 entities.retain(|node| node.kind.as_str() == kind_lower);
179 }
180
181 if let Some(ref query) = args.query {
183 let query_lower = query.to_lowercase();
184 entities.retain(|node| {
185 node.display_name.to_lowercase().contains(&query_lower)
186 || node.canonical_name.contains(&query_lower)
187 });
188 }
189
190 entities.sort_by(|a, b| a.display_name.cmp(&b.display_name));
192
193 let limited: Vec<_> = entities.into_iter().take(args.limit).collect();
195
196 if args.json {
197 #[derive(Serialize)]
198 struct EntityOutput {
199 name: String,
200 kind: String,
201 confidence: f32,
202 frame_count: usize,
203 frame_ids: Vec<u64>,
204 }
205 let output: Vec<EntityOutput> = limited
206 .iter()
207 .map(|node| EntityOutput {
208 name: node.display_name.clone(),
209 kind: node.kind.as_str().to_string(),
210 confidence: node.confidence_f32(),
211 frame_count: node.frame_ids.len(),
212 frame_ids: node.frame_ids.clone(),
213 })
214 .collect();
215 println!("{}", serde_json::to_string_pretty(&output)?);
216 } else {
217 println!(
218 "Entities in Logic-Mesh (showing {} of {})",
219 limited.len(),
220 mesh.nodes.len()
221 );
222 println!();
223 for node in &limited {
224 println!(
225 " {} ({}, confidence: {:.0}%, {} frames)",
226 node.display_name,
227 node.kind.as_str(),
228 node.confidence_f32() * 100.0,
229 node.frame_ids.len()
230 );
231 }
232 if limited.len() < mesh.nodes.len() {
233 println!();
234 println!("Use --limit to show more entities, or --query to filter.");
235 }
236 }
237
238 Ok(())
239}
240
241fn handle_follow_stats(args: FollowStatsArgs) -> Result<()> {
243 let mem = Memvid::open(&args.file)?;
244
245 if !mem.has_logic_mesh() {
246 if args.json {
247 println!(r#"{{"error": "No Logic-Mesh found"}}"#);
248 } else {
249 println!("No Logic-Mesh found in {}", args.file.display());
250 println!("Run `memvid put --logic-mesh` to build the graph.");
251 }
252 return Ok(());
253 }
254
255 let stats = mem.logic_mesh_stats();
256 let manifest = mem.logic_mesh_manifest();
257
258 if args.json {
259 #[derive(Serialize)]
260 struct MeshStatsOutput {
261 node_count: usize,
262 edge_count: usize,
263 entity_kinds: std::collections::HashMap<String, usize>,
264 link_types: std::collections::HashMap<String, usize>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 bytes_offset: Option<u64>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 bytes_length: Option<u64>,
269 }
270 let output = MeshStatsOutput {
271 node_count: stats.node_count,
272 edge_count: stats.edge_count,
273 entity_kinds: stats.entity_kinds,
274 link_types: stats.link_types,
275 bytes_offset: manifest.map(|m| m.bytes_offset),
276 bytes_length: manifest.map(|m| m.bytes_length),
277 };
278 println!("{}", serde_json::to_string_pretty(&output)?);
279 } else {
280 println!("Logic-Mesh Statistics");
281 println!("=====================");
282 println!(" Nodes (entities): {}", stats.node_count);
283 println!(" Edges (relations): {}", stats.edge_count);
284 if !stats.entity_kinds.is_empty() {
285 println!();
286 println!(" Entity Kinds:");
287 let mut kinds: Vec<_> = stats.entity_kinds.iter().collect();
288 kinds.sort_by(|a, b| b.1.cmp(a.1));
289 for (kind, count) in kinds {
290 println!(" {}: {}", kind, count);
291 }
292 }
293 if !stats.link_types.is_empty() {
294 println!();
295 println!(" Relationship Types:");
296 let mut links: Vec<_> = stats.link_types.iter().collect();
297 links.sort_by(|a, b| b.1.cmp(a.1));
298 for (link, count) in links {
299 println!(" {}: {}", link, count);
300 }
301 }
302 if let Some(m) = manifest {
303 println!();
304 println!(" Storage offset: {}", m.bytes_offset);
305 println!(" Storage size: {} bytes", m.bytes_length);
306 }
307 }
308
309 Ok(())
310}
311
312#[cfg(test)]
313mod tests {
314 #[test]
315 fn test_direction_parsing() {
316 let directions = vec!["outgoing", "incoming", "both"];
318 for d in directions {
319 assert!(!d.is_empty());
320 }
321 }
322}