ai_memory/cli/commands/
subscribe.rs1use crate::models::field_names;
19use 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 SubscribeArgs {
30 #[arg(long, value_name = "URL")]
32 pub url: String,
33
34 #[arg(long, value_name = "CSV")]
36 pub events: Option<String>,
37
38 #[arg(long, value_name = "SECRET")]
41 pub secret: Option<String>,
42
43 #[arg(long = "namespace-filter", value_name = "NS")]
45 pub namespace_filter: Option<String>,
46
47 #[arg(long = "agent-filter", value_name = "AGENT_ID")]
49 pub agent_filter: Option<String>,
50
51 #[arg(long = "event-types", value_name = "CSV", value_delimiter = ',')]
53 pub event_types: Vec<String>,
54
55 #[arg(long)]
57 pub json: bool,
58}
59
60pub fn cmd_subscribe(
69 db_path: &std::path::Path,
70 args: &SubscribeArgs,
71 out: &mut CliOutput<'_>,
72) -> Result<()> {
73 let conn = db::open(db_path)?;
74
75 let mut params = json!({"url": args.url});
76 if let Some(e) = &args.events {
77 params["events"] = json!(e);
78 }
79 if let Some(s) = &args.secret {
80 params["secret"] = json!(s);
81 }
82 if let Some(ns) = &args.namespace_filter {
83 params[field_names::NAMESPACE_FILTER] = json!(ns);
84 }
85 if let Some(a) = &args.agent_filter {
86 params[field_names::AGENT_FILTER] = json!(a);
87 }
88 if !args.event_types.is_empty() {
89 params[field_names::EVENT_TYPES] = json!(args.event_types);
90 }
91
92 let envelope = crate::mcp::handle_subscribe(&conn, ¶ms, None)
93 .map_err(|e| anyhow::anyhow!("subscribe: {e}"))?;
94
95 if args.json {
96 writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
97 return Ok(());
98 }
99
100 let id = envelope.get("id").and_then(Value::as_str).unwrap_or("?");
101 let url = envelope.get("url").and_then(Value::as_str).unwrap_or("?");
102 writeln!(out.stdout, "subscribe: id={id} url={url}")?;
103 Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::cli::test_utils::TestEnv;
110
111 #[test]
112 fn subscribe_cli_unregistered_agent_returns_err() {
113 let mut env = TestEnv::fresh();
114 let db = env.db_path.clone();
115 let args = SubscribeArgs {
116 url: "https://example.com/hook".into(),
117 events: None,
118 secret: Some("topsecret".into()),
119 namespace_filter: None,
120 agent_filter: None,
121 event_types: vec![],
122 json: true,
123 };
124 let mut out = env.output();
125 let err = cmd_subscribe(&db, &args, &mut out).expect_err("must fail");
128 assert!(
129 err.to_string().contains("subscribe") || err.to_string().contains("register"),
130 "got: {err}"
131 );
132 }
133
134 #[test]
138 fn subscribe_cli_success_json_with_all_params() {
139 crate::config::set_active_hooks_hmac_secret(None);
140 let mut env = TestEnv::fresh();
141 let db = env.db_path.clone();
142 {
143 let conn = db::open(&db).unwrap();
144 let agent_id = crate::identity::resolve_agent_id(None, None).unwrap();
145 db::register_agent(&conn, &agent_id, "test", &[]).expect("register");
146 }
147 let args = SubscribeArgs {
148 url: "https://example.com/hook".into(),
149 events: Some("memory_store,memory_link_created".into()),
150 secret: Some("topsecret".into()),
151 namespace_filter: Some("ns".into()),
152 agent_filter: Some("ai:other".into()),
153 event_types: vec!["memory_store".into()],
154 json: true,
155 };
156 {
157 let mut out = env.output();
158 cmd_subscribe(&db, &args, &mut out).expect("subscribe ok");
159 }
160 let envelope: Value = serde_json::from_str(env.stdout_str().trim()).expect("json");
161 assert!(envelope["id"].is_string());
162 assert_eq!(envelope["url"], "https://example.com/hook");
163 }
164
165 #[test]
166 fn subscribe_cli_success_text_output() {
167 crate::config::set_active_hooks_hmac_secret(None);
168 let mut env = TestEnv::fresh();
169 let db = env.db_path.clone();
170 {
171 let conn = db::open(&db).unwrap();
172 let agent_id = crate::identity::resolve_agent_id(None, None).unwrap();
173 db::register_agent(&conn, &agent_id, "test", &[]).expect("register");
174 }
175 let args = SubscribeArgs {
176 url: "https://example.com/hook2".into(),
177 events: None,
178 secret: Some("topsecret".into()),
179 namespace_filter: None,
180 agent_filter: None,
181 event_types: vec![],
182 json: false,
183 };
184 {
185 let mut out = env.output();
186 cmd_subscribe(&db, &args, &mut out).expect("subscribe ok");
187 }
188 let stdout = env.stdout_str();
189 assert!(stdout.contains("subscribe: id="), "got: {stdout}");
190 assert!(
191 stdout.contains("url=https://example.com/hook2"),
192 "got: {stdout}"
193 );
194 }
195}