use anyhow::{bail, Result};
use clap::{Args, Subcommand};
use memvid_core::Memvid;
use serde::Serialize;
use std::path::PathBuf;
use crate::config::CliConfig;
#[derive(Args)]
pub struct FollowArgs {
#[command(subcommand)]
pub command: FollowCommand,
}
#[derive(Subcommand)]
pub enum FollowCommand {
Traverse(FollowTraverseArgs),
Entities(FollowEntitiesArgs),
Stats(FollowStatsArgs),
}
#[derive(Args)]
pub struct FollowTraverseArgs {
pub file: PathBuf,
#[arg(short, long)]
pub start: String,
#[arg(short, long)]
pub link: Option<String>,
#[arg(long, default_value = "2")]
pub hops: usize,
#[arg(long, default_value = "both")]
pub direction: String,
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct FollowEntitiesArgs {
pub file: PathBuf,
#[arg(short = 't', long)]
pub kind: Option<String>,
#[arg(short, long)]
pub query: Option<String>,
#[arg(short, long, default_value = "50")]
pub limit: usize,
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct FollowStatsArgs {
pub file: PathBuf,
#[arg(long)]
pub json: bool,
}
pub fn handle_follow(_config: &CliConfig, args: FollowArgs) -> Result<()> {
match args.command {
FollowCommand::Traverse(traverse_args) => handle_follow_traverse(traverse_args),
FollowCommand::Entities(entities_args) => handle_follow_entities(entities_args),
FollowCommand::Stats(stats_args) => handle_follow_stats(stats_args),
}
}
fn handle_follow_traverse(args: FollowTraverseArgs) -> Result<()> {
let mem = Memvid::open(&args.file)?;
if !mem.has_logic_mesh() {
bail!(
"No Logic-Mesh found in {}. Run `memvid put --logic-mesh` to build the graph.",
args.file.display()
);
}
let start_node = mem.find_entity(&args.start);
if start_node.is_none() {
bail!(
"Entity '{}' not found in Logic-Mesh. Use `memvid follow entities` to list available entities.",
args.start
);
}
let link = args.link.as_deref().unwrap_or("related");
let results = mem.follow(&args.start, link, args.hops);
if args.json {
println!("{}", serde_json::to_string_pretty(&results)?);
} else {
let start_node = start_node.unwrap();
println!(
"Starting from: {} ({})",
start_node.display_name,
start_node.kind.as_str()
);
println!(
"Following: {} relationships (up to {} hops)",
link, args.hops
);
println!();
if results.is_empty() {
println!("No {} relationships found.", link);
} else {
for result in &results {
println!(
" → {} ({}, confidence: {:.0}%)",
result.node,
result.kind.as_str(),
result.confidence * 100.0
);
if !result.frame_ids.is_empty() {
println!(" Frames: {:?}", result.frame_ids);
}
}
}
}
Ok(())
}
fn handle_follow_entities(args: FollowEntitiesArgs) -> Result<()> {
let mem = Memvid::open(&args.file)?;
if !mem.has_logic_mesh() {
bail!(
"No Logic-Mesh found in {}. Run `memvid put --logic-mesh` to build the graph.",
args.file.display()
);
}
let mesh = mem.logic_mesh();
let mut entities: Vec<_> = mesh.nodes.iter().collect();
if let Some(ref kind_filter) = args.kind {
let kind_lower = kind_filter.to_lowercase();
entities.retain(|node| node.kind.as_str() == kind_lower);
}
if let Some(ref query) = args.query {
let query_lower = query.to_lowercase();
entities.retain(|node| {
node.display_name.to_lowercase().contains(&query_lower)
|| node.canonical_name.contains(&query_lower)
});
}
entities.sort_by(|a, b| a.display_name.cmp(&b.display_name));
let limited: Vec<_> = entities.into_iter().take(args.limit).collect();
if args.json {
#[derive(Serialize)]
struct EntityOutput {
name: String,
kind: String,
confidence: f32,
frame_count: usize,
frame_ids: Vec<u64>,
}
let output: Vec<EntityOutput> = limited
.iter()
.map(|node| EntityOutput {
name: node.display_name.clone(),
kind: node.kind.as_str().to_string(),
confidence: node.confidence_f32(),
frame_count: node.frame_ids.len(),
frame_ids: node.frame_ids.clone(),
})
.collect();
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!(
"Entities in Logic-Mesh (showing {} of {})",
limited.len(),
mesh.nodes.len()
);
println!();
for node in &limited {
println!(
" {} ({}, confidence: {:.0}%, {} frames)",
node.display_name,
node.kind.as_str(),
node.confidence_f32() * 100.0,
node.frame_ids.len()
);
}
if limited.len() < mesh.nodes.len() {
println!();
println!("Use --limit to show more entities, or --query to filter.");
}
}
Ok(())
}
fn handle_follow_stats(args: FollowStatsArgs) -> Result<()> {
let mem = Memvid::open(&args.file)?;
if !mem.has_logic_mesh() {
if args.json {
println!(r#"{{"error": "No Logic-Mesh found"}}"#);
} else {
println!("No Logic-Mesh found in {}", args.file.display());
println!("Run `memvid put --logic-mesh` to build the graph.");
}
return Ok(());
}
let stats = mem.logic_mesh_stats();
let manifest = mem.logic_mesh_manifest();
if args.json {
#[derive(Serialize)]
struct MeshStatsOutput {
node_count: usize,
edge_count: usize,
entity_kinds: std::collections::HashMap<String, usize>,
link_types: std::collections::HashMap<String, usize>,
#[serde(skip_serializing_if = "Option::is_none")]
bytes_offset: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
bytes_length: Option<u64>,
}
let output = MeshStatsOutput {
node_count: stats.node_count,
edge_count: stats.edge_count,
entity_kinds: stats.entity_kinds,
link_types: stats.link_types,
bytes_offset: manifest.map(|m| m.bytes_offset),
bytes_length: manifest.map(|m| m.bytes_length),
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("Logic-Mesh Statistics");
println!("=====================");
println!(" Nodes (entities): {}", stats.node_count);
println!(" Edges (relations): {}", stats.edge_count);
if !stats.entity_kinds.is_empty() {
println!();
println!(" Entity Kinds:");
let mut kinds: Vec<_> = stats.entity_kinds.iter().collect();
kinds.sort_by(|a, b| b.1.cmp(a.1));
for (kind, count) in kinds {
println!(" {}: {}", kind, count);
}
}
if !stats.link_types.is_empty() {
println!();
println!(" Relationship Types:");
let mut links: Vec<_> = stats.link_types.iter().collect();
links.sort_by(|a, b| b.1.cmp(a.1));
for (link, count) in links {
println!(" {}: {}", link, count);
}
}
if let Some(m) = manifest {
println!();
println!(" Storage offset: {}", m.bytes_offset);
println!(" Storage size: {} bytes", m.bytes_length);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_direction_parsing() {
let directions = vec!["outgoing", "incoming", "both"];
for d in directions {
assert!(!d.is_empty());
}
}
}