Skip to main content

ai_memory/cli/commands/
list_subscriptions.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4//! v0.7.0 ARCH-3 / FX-C3 (batch2) — `ai-memory list-subscriptions`
5//! CLI subcommand.
6//!
7//! Closes the three-surface-parity gap on `memory_list_subscriptions`.
8//! The MCP tool ([`crate::mcp::handle_list_subscriptions`]) and the
9//! HTTP route landed previously; this module wires the CLI surface so
10//! operators can inspect their webhook fleet from a terminal.
11
12use crate::models::field_names;
13use anyhow::Result;
14use clap::Args;
15use serde_json::Value;
16
17use crate::cli::CliOutput;
18use crate::storage as db;
19
20/// CLI args for `ai-memory list-subscriptions`.
21#[derive(Args, Debug, Clone)]
22pub struct ListSubscriptionsArgs {
23    /// Emit the raw JSON envelope.
24    #[arg(long)]
25    pub json: bool,
26}
27
28/// `ai-memory list-subscriptions` dispatch entry.
29///
30/// # Errors
31///
32/// - The DB at `db_path` cannot be opened.
33/// - The substrate refuses the listing.
34/// - `serde_json::to_string` cannot serialise the envelope.
35pub fn cmd_list_subscriptions(
36    db_path: &std::path::Path,
37    args: &ListSubscriptionsArgs,
38    out: &mut CliOutput<'_>,
39) -> Result<()> {
40    let conn = db::open(db_path)?;
41    let envelope = crate::mcp::handle_list_subscriptions(&conn, None)
42        .map_err(|e| anyhow::anyhow!("list-subscriptions: {e}"))?;
43
44    if args.json {
45        writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
46        return Ok(());
47    }
48
49    let count = envelope.get("count").and_then(Value::as_u64).unwrap_or(0);
50    writeln!(out.stdout, "list-subscriptions: {count} row(s)")?;
51    if let Some(arr) = envelope
52        .get(field_names::SUBSCRIPTIONS)
53        .and_then(Value::as_array)
54    {
55        for s in arr {
56            let id = s.get("id").and_then(Value::as_str).unwrap_or("?");
57            let url = s.get("url").and_then(Value::as_str).unwrap_or("?");
58            let events = s.get("events").and_then(Value::as_str).unwrap_or("*");
59            writeln!(out.stdout, "  {id}  events={events}  url={url}")?;
60        }
61    }
62    Ok(())
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::cli::test_utils::TestEnv;
69
70    #[test]
71    fn list_subscriptions_cli_empty_db_returns_zero() {
72        let mut env = TestEnv::fresh();
73        let db = env.db_path.clone();
74        let args = ListSubscriptionsArgs { json: true };
75        {
76            let mut out = env.output();
77            cmd_list_subscriptions(&db, &args, &mut out).expect("ok");
78        }
79        let stdout = env.stdout_str();
80        let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
81        assert_eq!(envelope["count"].as_u64(), Some(0));
82    }
83
84    #[test]
85    fn list_subscriptions_cli_text_mode_emits_count_line() {
86        let mut env = TestEnv::fresh();
87        let db = env.db_path.clone();
88        let args = ListSubscriptionsArgs { json: false };
89        {
90            let mut out = env.output();
91            cmd_list_subscriptions(&db, &args, &mut out).expect("ok");
92        }
93        let stdout = env.stdout_str();
94        assert!(
95            stdout.starts_with("list-subscriptions: 0 row(s)"),
96            "got: {stdout}"
97        );
98    }
99
100    #[test]
101    fn list_subscriptions_cli_text_mode_lists_rows() {
102        crate::config::set_active_hooks_hmac_secret(None);
103        let mut env = TestEnv::fresh();
104        let db = env.db_path.clone();
105        {
106            let conn = db::open(&db).unwrap();
107            let agent_id = crate::identity::resolve_agent_id(None, None).unwrap();
108            db::register_agent(&conn, &agent_id, "test", &[]).expect("register");
109            crate::mcp::handle_subscribe(
110                &conn,
111                &serde_json::json!({"url": "https://example.com/hook", "secret": "topsecret"}),
112                None,
113            )
114            .expect("subscribe");
115        }
116        let args = ListSubscriptionsArgs { json: false };
117        {
118            let mut out = env.output();
119            cmd_list_subscriptions(&db, &args, &mut out).expect("ok");
120        }
121        let stdout = env.stdout_str();
122        assert!(stdout.contains("1 row(s)"), "got: {stdout}");
123        assert!(
124            stdout.contains("url=https://example.com/hook"),
125            "got: {stdout}"
126        );
127    }
128}