zeph_commands/handlers/
loop_cmd.rs1use std::future::Future;
15use std::pin::Pin;
16
17use crate::context::CommandContext;
18use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
19
20pub struct LoopCommand;
22
23impl CommandHandler<CommandContext<'_>> for LoopCommand {
24 fn name(&self) -> &'static str {
25 "/loop"
26 }
27
28 fn description(&self) -> &'static str {
29 "Repeat a prompt on a fixed interval, or stop the active loop"
30 }
31
32 fn args_hint(&self) -> &'static str {
33 "<prompt> every <N> <unit> | stop | status"
34 }
35
36 fn category(&self) -> SlashCategory {
37 SlashCategory::Advanced
38 }
39
40 fn requires_auth(&self) -> bool {
41 true
42 }
43
44 fn handle<'a>(
45 &'a self,
46 ctx: &'a mut CommandContext<'_>,
47 args: &'a str,
48 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
49 use tracing::Instrument as _;
50 let span = tracing::info_span!("commands.loop.handle");
51 Box::pin(
52 async move {
53 let msg = ctx.agent.handle_loop(args).await?;
54 Ok(CommandOutput::message_or_silent(msg))
55 }
56 .instrument(span),
57 )
58 }
59}
60
61pub fn parse_loop_args(args: &str) -> Result<(String, u64), CommandError> {
69 let sep = " every ";
72 let sep_pos = args.rfind(sep).ok_or_else(|| {
73 CommandError::new(
74 "Usage: /loop <prompt> every <N> <unit> (e.g. /loop check logs every 10 minutes)",
75 )
76 })?;
77
78 let prompt = args[..sep_pos].trim().to_owned();
79 if prompt.is_empty() {
80 return Err(CommandError::new("Prompt must not be empty."));
81 }
82
83 let interval_str = args[sep_pos + sep.len()..].trim();
84 let (n_str, unit) = interval_str.split_once(' ').ok_or_else(|| {
85 CommandError::new("Expected format: every <N> <unit> (e.g. every 5 minutes)")
86 })?;
87
88 let n: u64 = n_str
89 .parse()
90 .map_err(|_| CommandError::new(format!("Expected a positive integer, got '{n_str}'")))?;
91 if n == 0 {
92 return Err(CommandError::new("Interval must be greater than zero."));
93 }
94
95 let multiplier: u64 = match unit.trim() {
96 "s" | "sec" | "secs" | "second" | "seconds" => 1,
97 "m" | "min" | "mins" | "minute" | "minutes" => 60,
98 "h" | "hr" | "hrs" | "hour" | "hours" => 3600,
99 other => {
100 return Err(CommandError::new(format!(
101 "Unknown time unit '{other}'. Use: s/sec/seconds, m/min/minutes, h/hr/hours"
102 )));
103 }
104 };
105
106 Ok((prompt, n * multiplier))
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn parse_seconds() {
115 let (prompt, secs) = parse_loop_args("check logs every 10 seconds").unwrap();
116 assert_eq!(prompt, "check logs");
117 assert_eq!(secs, 10);
118 }
119
120 #[test]
121 fn parse_minutes() {
122 let (prompt, secs) = parse_loop_args("summarize recent activity every 5 minutes").unwrap();
123 assert_eq!(prompt, "summarize recent activity");
124 assert_eq!(secs, 300);
125 }
126
127 #[test]
128 fn parse_hours() {
129 let (prompt, secs) = parse_loop_args("daily report every 1 hour").unwrap();
130 assert_eq!(prompt, "daily report");
131 assert_eq!(secs, 3600);
132 }
133
134 #[test]
135 fn parse_short_units() {
136 let (_, s) = parse_loop_args("ping every 30 s").unwrap();
137 assert_eq!(s, 30);
138 let (_, m) = parse_loop_args("ping every 2 m").unwrap();
139 assert_eq!(m, 120);
140 let (_, h) = parse_loop_args("ping every 1 h").unwrap();
141 assert_eq!(h, 3600);
142 }
143
144 #[test]
145 fn parse_prompt_with_every_word() {
146 let (prompt, secs) = parse_loop_args("check every file in dir every 15 sec").unwrap();
148 assert_eq!(prompt, "check every file in dir");
149 assert_eq!(secs, 15);
150 }
151
152 #[test]
153 fn parse_missing_every() {
154 assert!(parse_loop_args("check logs 10 seconds").is_err());
155 }
156
157 #[test]
158 fn parse_empty_prompt() {
159 assert!(parse_loop_args("every 5 seconds").is_err());
160 }
161
162 #[test]
163 fn parse_zero_n() {
164 assert!(parse_loop_args("ping every 0 seconds").is_err());
165 }
166
167 #[test]
168 fn parse_bad_unit() {
169 assert!(parse_loop_args("ping every 5 fortnights").is_err());
170 }
171
172 #[test]
173 fn parse_bad_n() {
174 assert!(parse_loop_args("ping every abc seconds").is_err());
175 }
176}