1use crate::models::field_names;
32use anyhow::Result;
33use clap::Args;
34use serde_json::{Value, json};
35
36use crate::cli::CliOutput;
37use crate::storage as db;
38
39#[derive(Args, Debug, Clone)]
42pub struct ReflectArgs {
43 #[arg(long = "source-ids", value_name = "CSV", value_delimiter = ',')]
45 pub source_ids: Vec<String>,
46
47 #[arg(long, value_name = "TEXT")]
49 pub title: String,
50
51 #[arg(long, value_name = "TEXT")]
53 pub content: String,
54
55 #[arg(long, value_name = "TIER")]
57 pub tier: Option<String>,
58
59 #[arg(long, value_name = "NS")]
61 pub namespace: Option<String>,
62
63 #[arg(long, value_name = "N")]
65 pub priority: Option<i64>,
66
67 #[arg(long, value_name = "F32")]
69 pub confidence: Option<f64>,
70
71 #[arg(long, value_name = "CSV", value_delimiter = ',')]
73 pub tags: Vec<String>,
74
75 #[arg(long, value_name = "N")]
77 pub depth: Option<i64>,
78
79 #[arg(long = "agent-id", value_name = "AGENT_ID")]
81 pub agent_id: Option<String>,
82
83 #[arg(long)]
86 pub json: bool,
87}
88
89pub fn cmd_reflect(
98 db_path: &std::path::Path,
99 args: &ReflectArgs,
100 out: &mut CliOutput<'_>,
101) -> Result<()> {
102 if args.source_ids.is_empty() {
103 anyhow::bail!("reflect: --source-ids is required (comma-separated list)");
104 }
105 let conn = db::open(db_path)?;
106
107 let mut params = json!({
108 (field_names::SOURCE_IDS): args.source_ids,
109 "title": args.title,
110 "content": args.content,
111 });
112 if let Some(t) = &args.tier {
113 params["tier"] = json!(t);
114 }
115 if let Some(ns) = &args.namespace {
116 params["namespace"] = json!(ns);
117 }
118 if let Some(p) = args.priority {
119 params["priority"] = json!(p);
120 }
121 if let Some(c) = args.confidence {
122 params[field_names::CONFIDENCE] = json!(c);
123 }
124 if !args.tags.is_empty() {
125 params["tags"] = json!(args.tags);
126 }
127 if let Some(d) = args.depth {
128 params["depth"] = json!(d);
129 }
130 if let Some(a) = &args.agent_id {
131 params["agent_id"] = json!(a);
132 }
133
134 let envelope = crate::mcp::handle_reflect(&conn, db_path, ¶ms, None, None, None, None)
139 .map_err(|e| anyhow::anyhow!("reflect: {e}"))?;
140
141 if args.json {
142 writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
143 return Ok(());
144 }
145
146 let id = envelope.get("id").and_then(Value::as_str).unwrap_or("?");
147 let depth = envelope
148 .get(field_names::REFLECTION_DEPTH)
149 .and_then(Value::as_i64)
150 .unwrap_or(0);
151 writeln!(out.stdout, "reflect: id={id} depth={depth}")?;
152 Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::cli::test_utils::{TestEnv, seed_memory};
159
160 #[test]
161 fn reflect_cli_missing_source_ids_returns_err() {
162 let mut env = TestEnv::fresh();
163 let db = env.db_path.clone();
164 let args = ReflectArgs {
165 source_ids: vec![],
166 title: "t".into(),
167 content: "c".into(),
168 tier: None,
169 namespace: None,
170 priority: None,
171 confidence: None,
172 tags: vec![],
173 depth: None,
174 agent_id: None,
175 json: true,
176 };
177 let mut out = env.output();
178 let err = cmd_reflect(&db, &args, &mut out).expect_err("must fail");
179 assert!(err.to_string().contains("source-ids"), "got: {err}");
180 }
181
182 #[test]
183 fn reflect_cli_happy_path_writes_envelope() {
184 let mut env = TestEnv::fresh();
185 let db = env.db_path.clone();
186 let s = seed_memory(&db, "rns", "reflect-src", "source content");
187 let args = ReflectArgs {
188 source_ids: vec![s],
189 title: "synthesis".into(),
190 content: "reflection body".into(),
191 tier: Some("mid".into()),
192 namespace: None,
193 priority: None,
194 confidence: None,
195 tags: vec![],
196 depth: None,
197 agent_id: None,
198 json: true,
199 };
200 {
201 let mut out = env.output();
202 cmd_reflect(&db, &args, &mut out).expect("reflect ok");
203 }
204 let stdout = env.stdout_str();
205 let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
206 assert!(envelope.get("id").and_then(Value::as_str).is_some());
207 }
208
209 #[test]
210 fn reflect_cli_text_output_with_all_params() {
211 let mut env = TestEnv::fresh();
212 let db = env.db_path.clone();
213 let s = seed_memory(&db, "rns", "reflect-src2", "source content");
214 let args = ReflectArgs {
215 source_ids: vec![s],
216 title: "synthesis-text".into(),
217 content: "reflection body".into(),
218 tier: Some("mid".into()),
219 namespace: Some("rns".into()),
220 priority: Some(7),
221 confidence: Some(0.8),
222 tags: vec!["t1".into(), "t2".into()],
223 depth: Some(1),
224 agent_id: Some("ai:reflector".into()),
225 json: false,
226 };
227 {
228 let mut out = env.output();
229 cmd_reflect(&db, &args, &mut out).expect("reflect ok");
230 }
231 let stdout = env.stdout_str();
232 assert!(stdout.contains("reflect: id="), "got: {stdout}");
233 assert!(stdout.contains("depth="), "got: {stdout}");
234 }
235}