Skip to main content

room_daemon/plugin/
schema.rs

1use room_protocol::plugin::{CommandInfo, ParamSchema, ParamType, Plugin};
2
3use super::{queue, stats, taskboard};
4
5// ── Built-in command schemas ───────────────────────────────────────────────
6
7/// Returns [`CommandInfo`] schemas for all built-in commands (those handled
8/// directly by the broker, not by plugins). Used by the TUI palette and
9/// `/help` to show a complete command list with typed parameter metadata.
10pub fn builtin_command_infos() -> Vec<CommandInfo> {
11    vec![
12        CommandInfo {
13            name: "dm".to_owned(),
14            description: "Send a private message".to_owned(),
15            usage: "/dm <user> <message>".to_owned(),
16            params: vec![
17                ParamSchema {
18                    name: "user".to_owned(),
19                    param_type: ParamType::Username,
20                    required: true,
21                    description: "Recipient username".to_owned(),
22                },
23                ParamSchema {
24                    name: "message".to_owned(),
25                    param_type: ParamType::Text,
26                    required: true,
27                    description: "Message content".to_owned(),
28                },
29            ],
30            subcommands: vec![],
31        },
32        CommandInfo {
33            name: "reply".to_owned(),
34            description: "Reply to a message".to_owned(),
35            usage: "/reply <id> <message>".to_owned(),
36            params: vec![
37                ParamSchema {
38                    name: "id".to_owned(),
39                    param_type: ParamType::Text,
40                    required: true,
41                    description: "Message ID to reply to".to_owned(),
42                },
43                ParamSchema {
44                    name: "message".to_owned(),
45                    param_type: ParamType::Text,
46                    required: true,
47                    description: "Reply content".to_owned(),
48                },
49            ],
50            subcommands: vec![],
51        },
52        CommandInfo {
53            name: "who".to_owned(),
54            description: "List users in the room".to_owned(),
55            usage: "/who [--verbose|-v]".to_owned(),
56            params: vec![ParamSchema {
57                name: "flag".to_owned(),
58                param_type: ParamType::Choice(vec!["--verbose".to_owned(), "-v".to_owned()]),
59                required: false,
60                description: "Show detailed status duration and last message time".to_owned(),
61            }],
62            subcommands: vec![],
63        },
64        CommandInfo {
65            name: "who_all".to_owned(),
66            description: "List all daemon users (cross-room)".to_owned(),
67            usage: "/who_all".to_owned(),
68            params: vec![],
69            subcommands: vec![],
70        },
71        CommandInfo {
72            name: "kick".to_owned(),
73            description: "Kick a user from the room".to_owned(),
74            usage: "/kick <user>".to_owned(),
75            params: vec![ParamSchema {
76                name: "user".to_owned(),
77                param_type: ParamType::Username,
78                required: true,
79                description: "User to kick (host only)".to_owned(),
80            }],
81            subcommands: vec![],
82        },
83        CommandInfo {
84            name: "reauth".to_owned(),
85            description: "Invalidate a user's token".to_owned(),
86            usage: "/reauth <user>".to_owned(),
87            params: vec![ParamSchema {
88                name: "user".to_owned(),
89                param_type: ParamType::Username,
90                required: true,
91                description: "User to reauth (host only)".to_owned(),
92            }],
93            subcommands: vec![],
94        },
95        CommandInfo {
96            name: "clear-tokens".to_owned(),
97            description: "Revoke all session tokens".to_owned(),
98            usage: "/clear-tokens".to_owned(),
99            params: vec![],
100            subcommands: vec![],
101        },
102        CommandInfo {
103            name: "exit".to_owned(),
104            description: "Shut down the broker".to_owned(),
105            usage: "/exit".to_owned(),
106            params: vec![],
107            subcommands: vec![],
108        },
109        CommandInfo {
110            name: "clear".to_owned(),
111            description: "Clear the room history".to_owned(),
112            usage: "/clear".to_owned(),
113            params: vec![],
114            subcommands: vec![],
115        },
116        CommandInfo {
117            name: "info".to_owned(),
118            description: "Show room metadata or user info".to_owned(),
119            usage: "/info [username]".to_owned(),
120            params: vec![ParamSchema {
121                name: "username".to_owned(),
122                param_type: ParamType::Username,
123                required: false,
124                description: "User to inspect (omit for room info)".to_owned(),
125            }],
126            subcommands: vec![],
127        },
128        CommandInfo {
129            name: "room-info".to_owned(),
130            description: "Alias for /info — show room visibility, config, and member count"
131                .to_owned(),
132            usage: "/room-info".to_owned(),
133            params: vec![],
134            subcommands: vec![],
135        },
136        CommandInfo {
137            name: "subscribe".to_owned(),
138            description: "Subscribe to this room".to_owned(),
139            usage: "/subscribe [tier]".to_owned(),
140            params: vec![ParamSchema {
141                name: "tier".to_owned(),
142                param_type: ParamType::Choice(vec!["full".to_owned(), "mentions_only".to_owned()]),
143                required: false,
144                description: "Subscription tier (default: full)".to_owned(),
145            }],
146            subcommands: vec![],
147        },
148        CommandInfo {
149            name: "set_subscription".to_owned(),
150            description: "Alias for /subscribe — set subscription tier for this room".to_owned(),
151            usage: "/set_subscription [tier]".to_owned(),
152            params: vec![ParamSchema {
153                name: "tier".to_owned(),
154                param_type: ParamType::Choice(vec!["full".to_owned(), "mentions_only".to_owned()]),
155                required: false,
156                description: "Subscription tier (default: full)".to_owned(),
157            }],
158            subcommands: vec![],
159        },
160        CommandInfo {
161            name: "unsubscribe".to_owned(),
162            description: "Unsubscribe from this room".to_owned(),
163            usage: "/unsubscribe".to_owned(),
164            params: vec![],
165            subcommands: vec![],
166        },
167        CommandInfo {
168            name: "subscribe_events".to_owned(),
169            description: "Set event type filter for this room".to_owned(),
170            usage: "/subscribe_events [filter]".to_owned(),
171            params: vec![ParamSchema {
172                name: "filter".to_owned(),
173                param_type: ParamType::Text,
174                required: false,
175                description: "all, none, or comma-separated event types (default: all)".to_owned(),
176            }],
177            subcommands: vec![],
178        },
179        CommandInfo {
180            name: "set_event_filter".to_owned(),
181            description: "Alias for /subscribe_events — set event type filter".to_owned(),
182            usage: "/set_event_filter [filter]".to_owned(),
183            params: vec![ParamSchema {
184                name: "filter".to_owned(),
185                param_type: ParamType::Text,
186                required: false,
187                description: "all, none, or comma-separated event types (default: all)".to_owned(),
188            }],
189            subcommands: vec![],
190        },
191        CommandInfo {
192            name: "set_status".to_owned(),
193            description: "Set your presence status".to_owned(),
194            usage: "/set_status <status>".to_owned(),
195            params: vec![ParamSchema {
196                name: "status".to_owned(),
197                param_type: ParamType::Text,
198                required: false,
199                description: "Status text (omit to clear)".to_owned(),
200            }],
201            subcommands: vec![],
202        },
203        CommandInfo {
204            name: "subscriptions".to_owned(),
205            description: "List subscription tiers and event filters for this room".to_owned(),
206            usage: "/subscriptions".to_owned(),
207            params: vec![],
208            subcommands: vec![],
209        },
210        CommandInfo {
211            name: "team".to_owned(),
212            description: "Manage teams — join, leave, list, show".to_owned(),
213            usage: "/team <action> [args...]".to_owned(),
214            params: vec![
215                ParamSchema {
216                    name: "action".to_owned(),
217                    param_type: ParamType::Choice(vec![
218                        "join".to_owned(),
219                        "leave".to_owned(),
220                        "list".to_owned(),
221                        "show".to_owned(),
222                    ]),
223                    required: true,
224                    description: "Subcommand".to_owned(),
225                },
226                ParamSchema {
227                    name: "args".to_owned(),
228                    param_type: ParamType::Text,
229                    required: false,
230                    description: "Team name and optional username".to_owned(),
231                },
232            ],
233            subcommands: vec![],
234        },
235        CommandInfo {
236            name: "help".to_owned(),
237            description: "List available commands or get help for a specific command".to_owned(),
238            usage: "/help [command]".to_owned(),
239            params: vec![ParamSchema {
240                name: "command".to_owned(),
241                param_type: ParamType::Text,
242                required: false,
243                description: "Command name to get help for".to_owned(),
244            }],
245            subcommands: vec![],
246        },
247    ]
248}
249
250/// Returns command schemas for all known commands: built-ins + default plugins.
251///
252/// Used by the TUI to build its command palette at startup without needing
253/// access to the broker's `PluginRegistry`.
254pub fn all_known_commands() -> Vec<CommandInfo> {
255    let mut cmds = builtin_command_infos();
256    cmds.extend(queue::QueuePlugin::default_commands());
257    cmds.extend(stats::StatsPlugin.commands());
258    cmds.extend(taskboard::TaskboardPlugin::default_commands());
259    // Agent plugin commands are no longer included statically — the agent
260    // plugin lives in knoxio/room-ralph and is loaded dynamically.
261    cmds
262}
263
264// ── Tests ───────────────────────────────────────────────────────────────────
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    // ── builtin_command_infos tests ───────────────────────────────────────
271
272    #[test]
273    fn builtin_command_infos_covers_all_expected_commands() {
274        let cmds = builtin_command_infos();
275        let names: Vec<&str> = cmds.iter().map(|c| c.name.as_str()).collect();
276        for expected in &[
277            "dm",
278            "reply",
279            "who",
280            "who_all",
281            "help",
282            "info",
283            "kick",
284            "reauth",
285            "clear-tokens",
286            "exit",
287            "clear",
288            "room-info",
289            "set_status",
290            "subscribe",
291            "set_subscription",
292            "unsubscribe",
293            "subscribe_events",
294            "set_event_filter",
295            "subscriptions",
296            "team",
297        ] {
298            assert!(
299                names.contains(expected),
300                "missing built-in command: {expected}"
301            );
302        }
303    }
304
305    #[test]
306    fn builtin_command_infos_dm_has_username_param() {
307        let cmds = builtin_command_infos();
308        let dm = cmds.iter().find(|c| c.name == "dm").unwrap();
309        assert_eq!(dm.params.len(), 2);
310        assert_eq!(dm.params[0].param_type, ParamType::Username);
311        assert!(dm.params[0].required);
312        assert_eq!(dm.params[1].param_type, ParamType::Text);
313    }
314
315    #[test]
316    fn builtin_command_infos_kick_has_username_param() {
317        let cmds = builtin_command_infos();
318        let kick = cmds.iter().find(|c| c.name == "kick").unwrap();
319        assert_eq!(kick.params.len(), 1);
320        assert_eq!(kick.params[0].param_type, ParamType::Username);
321        assert!(kick.params[0].required);
322    }
323
324    #[test]
325    fn builtin_command_infos_who_has_verbose_flag() {
326        let cmds = builtin_command_infos();
327        let who = cmds.iter().find(|c| c.name == "who").unwrap();
328        assert_eq!(who.params.len(), 1);
329        assert_eq!(who.params[0].name, "flag");
330        assert!(!who.params[0].required);
331    }
332
333    // ── all_known_commands tests ──────────────────────────────────────────
334
335    #[test]
336    fn all_known_commands_includes_builtins_and_plugins() {
337        let cmds = all_known_commands();
338        let names: Vec<&str> = cmds.iter().map(|c| c.name.as_str()).collect();
339        // Built-ins
340        assert!(names.contains(&"dm"));
341        assert!(names.contains(&"who"));
342        assert!(names.contains(&"kick"));
343        assert!(names.contains(&"help"));
344        // Plugins
345        assert!(names.contains(&"stats"));
346        assert!(names.contains(&"taskboard"));
347        assert!(names.contains(&"queue"));
348    }
349
350    #[test]
351    fn all_known_commands_no_duplicates() {
352        let cmds = all_known_commands();
353        let mut names: Vec<&str> = cmds.iter().map(|c| c.name.as_str()).collect();
354        let before = names.len();
355        names.sort();
356        names.dedup();
357        assert_eq!(before, names.len(), "duplicate command names found");
358    }
359}