1use room_protocol::plugin::{CommandInfo, ParamSchema, ParamType, Plugin};
2
3use super::{queue, stats, taskboard};
4
5pub 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
225pub 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#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[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 #[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 assert!(names.contains(&"dm"));
313 assert!(names.contains(&"who"));
314 assert!(names.contains(&"kick"));
315 assert!(names.contains(&"help"));
316 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}