Skip to main content

zeroclaw_channels/
cli.rs

1use async_trait::async_trait;
2use tokio::io::{self, AsyncBufReadExt, BufReader};
3use uuid::Uuid;
4use zeroclaw_api::channel::{Channel, ChannelMessage, SendMessage};
5
6/// CLI channel — stdin/stdout, always available, zero deps
7pub struct CliChannel {
8    alias: String,
9}
10
11impl CliChannel {
12    pub fn new(alias: impl Into<String>) -> Self {
13        Self {
14            alias: alias.into(),
15        }
16    }
17}
18
19impl ::zeroclaw_api::attribution::Attributable for CliChannel {
20    fn role(&self) -> ::zeroclaw_api::attribution::Role {
21        ::zeroclaw_api::attribution::Role::Channel(::zeroclaw_api::attribution::ChannelKind::Cli)
22    }
23    fn alias(&self) -> &str {
24        &self.alias
25    }
26}
27
28#[async_trait]
29impl Channel for CliChannel {
30    fn name(&self) -> &str {
31        "cli"
32    }
33
34    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
35        println!("{}", message.content);
36        Ok(())
37    }
38
39    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
40        let stdin = io::stdin();
41        let reader = BufReader::new(stdin);
42        let mut lines = reader.lines();
43
44        while let Ok(Some(line)) = lines.next_line().await {
45            let line = line.trim().to_string();
46            if line.is_empty() {
47                continue;
48            }
49            if line == "/quit" || line == "/exit" {
50                break;
51            }
52
53            let msg = ChannelMessage {
54                id: Uuid::new_v4().to_string(),
55                sender: "user".to_string(),
56                reply_target: "user".to_string(),
57                content: line,
58                channel: "cli".to_string(),
59                channel_alias: None,
60                timestamp: std::time::SystemTime::now()
61                    .duration_since(std::time::UNIX_EPOCH)
62                    .unwrap_or_default()
63                    .as_secs(),
64                thread_ts: None,
65                interruption_scope_id: None,
66                attachments: vec![],
67                subject: None,
68            };
69
70            if tx.send(msg).await.is_err() {
71                break;
72            }
73        }
74        Ok(())
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn cli_channel_name() {
84        assert_eq!(CliChannel::new("cli").name(), "cli");
85    }
86
87    #[tokio::test]
88    async fn cli_channel_send_does_not_panic() {
89        let ch = CliChannel::new("cli");
90        let result = ch
91            .send(&SendMessage {
92                content: "hello".into(),
93                recipient: "user".into(),
94                subject: None,
95                thread_ts: None,
96                cancellation_token: None,
97                attachments: vec![],
98                in_reply_to: None,
99            })
100            .await;
101        assert!(result.is_ok());
102    }
103
104    #[tokio::test]
105    async fn cli_channel_send_empty_message() {
106        let ch = CliChannel::new("cli");
107        let result = ch
108            .send(&SendMessage {
109                content: String::new(),
110                recipient: String::new(),
111                subject: None,
112                thread_ts: None,
113                cancellation_token: None,
114                attachments: vec![],
115                in_reply_to: None,
116            })
117            .await;
118        assert!(result.is_ok());
119    }
120
121    #[tokio::test]
122    async fn cli_channel_health_check() {
123        let ch = CliChannel::new("cli");
124        assert!(ch.health_check().await);
125    }
126
127    #[test]
128    fn channel_message_struct() {
129        let msg = ChannelMessage {
130            id: "test-id".into(),
131            sender: "user".into(),
132            reply_target: "user".into(),
133            content: "hello".into(),
134            channel: "cli".into(),
135            channel_alias: None,
136            timestamp: 1_234_567_890,
137            thread_ts: None,
138            interruption_scope_id: None,
139            attachments: vec![],
140            subject: None,
141        };
142        assert_eq!(msg.id, "test-id");
143        assert_eq!(msg.sender, "user");
144        assert_eq!(msg.reply_target, "user");
145        assert_eq!(msg.content, "hello");
146        assert_eq!(msg.channel, "cli");
147        assert_eq!(msg.timestamp, 1_234_567_890);
148    }
149
150    #[test]
151    fn channel_message_clone() {
152        let msg = ChannelMessage {
153            id: "id".into(),
154            sender: "s".into(),
155            reply_target: "s".into(),
156            content: "c".into(),
157            channel: "ch".into(),
158            channel_alias: None,
159            timestamp: 0,
160            thread_ts: None,
161            interruption_scope_id: None,
162            attachments: vec![],
163            subject: None,
164        };
165        let cloned = msg.clone();
166        assert_eq!(cloned.id, msg.id);
167        assert_eq!(cloned.content, msg.content);
168    }
169}