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