ai_memory/cli/commands/
find_paths.rs1use anyhow::Result;
20use clap::Args;
21use serde_json::{Value, json};
22
23use crate::cli::CliOutput;
24use crate::storage as db;
25
26#[derive(Args, Debug, Clone)]
29pub struct FindPathsArgs {
30 #[arg(long = "source-id", value_name = "ID")]
32 pub source_id: String,
33
34 #[arg(long = "target-id", value_name = "ID")]
36 pub target_id: String,
37
38 #[arg(long = "max-depth", value_name = "N")]
40 pub max_depth: Option<u32>,
41
42 #[arg(long = "max-results", value_name = "N")]
44 pub max_results: Option<u32>,
45
46 #[arg(long = "include-invalidated")]
48 pub include_invalidated: bool,
49
50 #[arg(long)]
53 pub json: bool,
54}
55
56pub fn cmd_find_paths(
68 db_path: &std::path::Path,
69 args: &FindPathsArgs,
70 out: &mut CliOutput<'_>,
71) -> Result<()> {
72 let conn = db::open(db_path)?;
73
74 let mut params = json!({
75 "source_id": args.source_id,
76 "target_id": args.target_id,
77 });
78 if let Some(d) = args.max_depth {
79 params["max_depth"] = json!(d);
80 }
81 if let Some(m) = args.max_results {
82 params["max_results"] = json!(m);
83 }
84 if args.include_invalidated {
85 params[crate::models::field_names::INCLUDE_INVALIDATED] = json!(true);
86 }
87
88 let envelope = crate::mcp::handle_find_paths(&conn, ¶ms)
89 .map_err(|e| anyhow::anyhow!("find-paths: {e}"))?;
90
91 if args.json {
92 writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
93 return Ok(());
94 }
95
96 let count = envelope.get("count").and_then(Value::as_u64).unwrap_or(0);
97 writeln!(out.stdout, "find-paths: {count} path(s)")?;
98 if let Some(arr) = envelope.get("paths").and_then(Value::as_array) {
99 for (idx, path) in arr.iter().enumerate() {
100 if let Some(ids) = path.as_array() {
101 let chain: Vec<&str> = ids.iter().filter_map(Value::as_str).collect();
102 writeln!(out.stdout, " [{}] {}", idx + 1, chain.join(" -> "))?;
103 }
104 }
105 }
106 Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::cli::test_utils::{TestEnv, seed_memory};
113
114 #[test]
115 fn find_paths_cli_empty_db_returns_zero() {
116 let mut env = TestEnv::fresh();
117 let db = env.db_path.clone();
118 let a = seed_memory(&db, "ns", "fp-a", "alpha");
119 let b = seed_memory(&db, "ns", "fp-b", "beta");
120 let args = FindPathsArgs {
121 source_id: a,
122 target_id: b,
123 max_depth: None,
124 max_results: None,
125 include_invalidated: false,
126 json: true,
127 };
128 {
129 let mut out = env.output();
130 cmd_find_paths(&db, &args, &mut out).expect("find-paths ok");
131 }
132 let stdout = env.stdout_str();
133 let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
134 assert_eq!(envelope["count"].as_u64(), Some(0));
135 }
136
137 #[test]
138 fn find_paths_cli_invalid_id_returns_err() {
139 let mut env = TestEnv::fresh();
140 let db = env.db_path.clone();
141 let args = FindPathsArgs {
142 source_id: "bogus id with spaces".to_string(),
143 target_id: "another bogus".to_string(),
144 max_depth: None,
145 max_results: None,
146 include_invalidated: false,
147 json: true,
148 };
149 let mut out = env.output();
150 let err = cmd_find_paths(&db, &args, &mut out).expect_err("must fail");
151 assert!(err.to_string().contains("find-paths"), "got: {err}");
152 }
153
154 #[test]
155 fn find_paths_cli_text_output_with_path_and_all_params() {
156 let mut env = TestEnv::fresh();
157 let db = env.db_path.clone();
158 let a = seed_memory(&db, "ns", "fp-src", "alpha");
159 let b = seed_memory(&db, "ns", "fp-tgt", "beta");
160 {
161 let conn = db::open(&db).unwrap();
162 let now = chrono::Utc::now().to_rfc3339();
163 conn.execute(
164 "INSERT INTO memory_links (source_id, target_id, relation, created_at, valid_from)
165 VALUES (?1, ?2, 'related_to', ?3, ?3)",
166 rusqlite::params![a, b, now],
167 )
168 .expect("insert link");
169 }
170 let args = FindPathsArgs {
171 source_id: a.clone(),
172 target_id: b.clone(),
173 max_depth: Some(4),
174 max_results: Some(10),
175 include_invalidated: true,
176 json: false,
177 };
178 {
179 let mut out = env.output();
180 cmd_find_paths(&db, &args, &mut out).expect("find-paths ok");
181 }
182 let stdout = env.stdout_str();
183 assert!(stdout.contains("path(s)"), "got: {stdout}");
184 assert!(stdout.contains("->"), "got: {stdout}");
185 }
186}