roboticus_cli/cli/admin/misc/
channel_ops.rs1pub async fn cmd_circuit_status(url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
2 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
3 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
4 let c = RoboticusClient::new(url)?;
5 let data = c.get("/api/breaker/status").await.map_err(|e| {
6 RoboticusClient::check_connectivity_hint(&*e);
7 e
8 })?;
9 if json {
10 println!("{}", serde_json::to_string_pretty(&data)?);
11 return Ok(());
12 }
13
14 heading("Circuit Breaker Status");
15
16 if let Some(providers) = data["providers"].as_object() {
17 if providers.is_empty() {
18 empty_state("No providers registered yet");
19 } else {
20 for (name, status) in providers {
21 let state = status["state"].as_str().unwrap_or("unknown");
22 kv_accent(name, &status_badge(state));
23 }
24 }
25 } else {
26 empty_state("No providers registered yet");
27 }
28
29 if let Some(note) = data["note"].as_str() {
30 eprintln!();
31 eprintln!(" {DIM}\u{2139} {note}{RESET}");
32 }
33
34 eprintln!();
35 Ok(())
36}
37
38pub async fn cmd_circuit_reset(
39 url: &str,
40 provider: Option<&str>,
41) -> Result<(), Box<dyn std::error::Error>> {
42 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
43 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
44 let client = super::http_client()?;
45 heading("Circuit Breaker Reset");
46
47 let providers: Vec<String> = if let Some(single) = provider {
48 vec![single.to_string()]
49 } else {
50 let status = client
51 .get(format!("{url}/api/breaker/status"))
52 .send()
53 .await
54 .inspect_err(|_| {
55 eprintln!(" {ERR} Cannot reach gateway at {url}");
56 })?;
57
58 if !status.status().is_success() {
59 eprintln!(" {WARN} Status returned HTTP {}", status.status());
60 eprintln!();
61 return Ok(());
62 }
63
64 let body: serde_json::Value = status.json().await.unwrap_or_else(|e| {
65 tracing::warn!("failed to parse breaker status response: {e}");
66 serde_json::Value::default()
67 });
68 body.get("providers")
69 .and_then(|v| v.as_object())
70 .map(|m| m.keys().cloned().collect())
71 .unwrap_or_default()
72 };
73
74 if providers.is_empty() {
75 eprintln!(" {WARN} No providers reported by gateway");
76 eprintln!();
77 return Ok(());
78 }
79
80 let mut reset_ok = 0usize;
81 for provider in &providers {
82 let resp = client
83 .post(format!("{url}/api/breaker/reset/{provider}"))
84 .send()
85 .await;
86 match resp {
87 Ok(r) if r.status().is_success() => {
88 reset_ok += 1;
89 }
90 Ok(r) => {
91 eprintln!(" {WARN} reset {} returned HTTP {}", provider, r.status());
92 }
93 Err(e) => {
94 eprintln!(" {WARN} reset {} failed: {}", provider, e);
95 }
96 }
97 }
98
99 if reset_ok == providers.len() {
100 eprintln!(
101 " {OK} Reset {} providers to closed state",
102 providers.len()
103 );
104 } else {
105 eprintln!(
106 " {WARN} Reset {}/{} providers",
107 reset_ok,
108 providers.len()
109 );
110 }
111
112 eprintln!();
113 Ok(())
114}
115
116pub async fn cmd_agent_start(base_url: &str, id: &str) -> Result<(), Box<dyn std::error::Error>> {
119 let client = super::http_client()?;
120 let resp = client
121 .post(format!("{base_url}/api/agents/{id}/start"))
122 .send()
123 .await?;
124 if !resp.status().is_success() {
125 let status = resp.status();
126 let body = resp.text().await.unwrap_or_else(|e| {
127 tracing::warn!("failed to read agent start error body: {e}");
128 String::new()
129 });
130 return Err(format!("HTTP {status}: {body}").into());
131 }
132 eprintln!(" Agent {id} started");
133 Ok(())
134}
135
136pub async fn cmd_agent_stop(base_url: &str, id: &str) -> Result<(), Box<dyn std::error::Error>> {
137 let client = super::http_client()?;
138 let resp = client
139 .post(format!("{base_url}/api/agents/{id}/stop"))
140 .send()
141 .await?;
142 if !resp.status().is_success() {
143 let status = resp.status();
144 let body = resp.text().await.unwrap_or_else(|e| {
145 tracing::warn!("failed to read agent stop error body: {e}");
146 String::new()
147 });
148 return Err(format!("HTTP {status}: {body}").into());
149 }
150 eprintln!(" Agent {id} stopped");
151 Ok(())
152}
153
154pub async fn cmd_agents_list(base_url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
155 let client = super::http_client()?;
156 let resp = client.get(format!("{base_url}/api/agents")).send().await?;
157 let body: serde_json::Value = resp.json().await?;
158 if json {
159 println!("{}", serde_json::to_string_pretty(&body)?);
160 return Ok(());
161 }
162
163 let agents = body
164 .get("agents")
165 .and_then(|v| v.as_array())
166 .cloned()
167 .unwrap_or_default();
168
169 if agents.is_empty() {
170 println!("\n No agents registered.\n");
171 return Ok(());
172 }
173
174 println!(
175 "\n {:<15} {:<20} {:<10} {:<15}",
176 "ID", "Name", "State", "Model"
177 );
178 println!(" {}", "─".repeat(65));
179 for a in &agents {
180 let id = a.get("id").and_then(|v| v.as_str()).unwrap_or("?");
181 let name = a.get("name").and_then(|v| v.as_str()).unwrap_or("?");
182 let state = a.get("state").and_then(|v| v.as_str()).unwrap_or("?");
183 let model = a.get("model").and_then(|v| v.as_str()).unwrap_or("?");
184 println!(" {:<15} {:<20} {:<10} {:<15}", id, name, state, model);
185 }
186 println!();
187 Ok(())
188}
189
190pub async fn cmd_channels_status(
191 base_url: &str,
192 json: bool,
193) -> Result<(), Box<dyn std::error::Error>> {
194 let resp = super::http_client()?
195 .get(format!("{base_url}/api/channels/status"))
196 .send()
197 .await?;
198 let body: serde_json::Value = resp.json().await?;
199 if json {
200 println!("{}", serde_json::to_string_pretty(&body)?);
201 return Ok(());
202 }
203 let channels: Vec<serde_json::Value> =
204 serde_json::from_value(body).unwrap_or_default();
205
206 if channels.is_empty() {
207 println!(" No channels configured.");
208 return Ok(());
209 }
210
211 println!(
212 "\n {:<15} {:<10} {:<10} {:<10}",
213 "Channel", "Status", "Recv", "Sent"
214 );
215 println!(" {}", "─".repeat(50));
216 for ch in &channels {
217 let name = ch.get("name").and_then(|v| v.as_str()).unwrap_or("?");
218 let connected = ch
219 .get("connected")
220 .and_then(|v| v.as_bool())
221 .unwrap_or(false);
222 let status_str = if connected { "✓ up" } else { "✗ down" };
223 let recv = ch
224 .get("messages_received")
225 .and_then(|v| v.as_u64())
226 .unwrap_or(0);
227 let sent = ch
228 .get("messages_sent")
229 .and_then(|v| v.as_u64())
230 .unwrap_or(0);
231 println!(
232 " {:<15} {:<10} {:<10} {:<10}",
233 name, status_str, recv, sent
234 );
235 }
236 println!();
237 Ok(())
238}
239
240pub async fn cmd_integrations_test(
241 base_url: &str,
242 platform: &str,
243) -> Result<(), Box<dyn std::error::Error>> {
244 let resp = super::http_client()?
245 .post(format!("{base_url}/api/channels/{platform}/test"))
246 .send()
247 .await?;
248 let status = resp.status();
249 let body: serde_json::Value = resp.json().await?;
250 if status.is_success() {
251 let ok = body.get("ok").and_then(|v| v.as_bool()).unwrap_or(false);
252 let details = body
253 .pointer("/diagnostics/details")
254 .and_then(|v| v.as_str())
255 .unwrap_or("(no details)");
256 let icon = if ok { "✓" } else { "✗" };
257 println!("\n {icon} {platform}: {details}\n");
258 } else {
259 let err = body
260 .get("error")
261 .and_then(|v| v.as_str())
262 .unwrap_or("unknown error");
263 println!("\n ✗ {platform}: {err}\n");
264 }
265 Ok(())
266}
267
268pub async fn cmd_channels_dead_letter(
269 base_url: &str,
270 limit: usize,
271 json: bool,
272) -> Result<(), Box<dyn std::error::Error>> {
273 let resp = super::http_client()?
274 .get(format!("{base_url}/api/channels/dead-letter?limit={limit}"))
275 .send()
276 .await?;
277 let body: serde_json::Value = resp.json().await?;
278 if json {
279 println!("{}", serde_json::to_string_pretty(&body)?);
280 return Ok(());
281 }
282 let items = body
283 .get("items")
284 .and_then(|v| v.as_array())
285 .cloned()
286 .unwrap_or_default();
287
288 if items.is_empty() {
289 println!(" No dead-letter deliveries.");
290 return Ok(());
291 }
292
293 println!(
294 "\n {:<38} {:<12} {:<10} {:<40}",
295 "ID", "Channel", "Attempts", "Last error"
296 );
297 println!(" {}", "─".repeat(108));
298 for item in items {
299 let id = item.get("id").and_then(|v| v.as_str()).unwrap_or("?");
300 let channel = item.get("channel").and_then(|v| v.as_str()).unwrap_or("?");
301 let attempts = item
302 .get("attempts")
303 .and_then(|v| v.as_u64())
304 .unwrap_or_default();
305 let max_attempts = item
306 .get("max_attempts")
307 .and_then(|v| v.as_u64())
308 .unwrap_or_default();
309 let last_error = item
310 .get("last_error")
311 .and_then(|v| v.as_str())
312 .unwrap_or("-");
313 println!(
314 " {:<38} {:<12} {:<10} {:<40}",
315 truncate_id(id, 35),
316 channel,
317 format!("{attempts}/{max_attempts}"),
318 truncate_id(last_error, 37),
319 );
320 }
321 println!();
322 Ok(())
323}
324
325pub async fn cmd_channels_replay(
326 base_url: &str,
327 id: &str,
328) -> Result<(), Box<dyn std::error::Error>> {
329 let client = super::http_client()?;
330 let resp = client
331 .post(format!("{base_url}/api/channels/dead-letter/{id}/replay"))
332 .send()
333 .await?;
334 if resp.status().is_success() {
335 println!(" Replayed dead-letter item: {id}");
336 } else if resp.status() == reqwest::StatusCode::NOT_FOUND {
337 println!(" Dead-letter item not found: {id}");
338 } else {
339 println!(" Replay failed for {id}: HTTP {}", resp.status());
340 }
341 Ok(())
342}
343
344pub async fn cmd_integrations_connect(
347 _url: &str,
348 channel: &str,
349) -> Result<(), Box<dyn std::error::Error>> {
350 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
351 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
352 heading(&format!("Connect: {channel}"));
353
354 let snippet = match channel.to_ascii_lowercase().as_str() {
355 "telegram" => r#"[channels.telegram]
356enabled = true
357token_env = "TELEGRAM_BOT_TOKEN"
358# allowed_chat_ids = [123456789]
359# webhook_mode = false"#,
360 "discord" => r#"[channels.discord]
361enabled = true
362token_env = "DISCORD_BOT_TOKEN"
363application_id = "YOUR_APPLICATION_ID"
364# allowed_guild_ids = []"#,
365 "whatsapp" => r#"[channels.whatsapp]
366enabled = true
367token_env = "WHATSAPP_TOKEN"
368phone_number_id = "YOUR_PHONE_NUMBER_ID"
369verify_token = "YOUR_VERIFY_TOKEN"
370# app_secret_env = "WHATSAPP_APP_SECRET""#,
371 "signal" => r#"[channels.signal]
372enabled = true
373phone_number = "+15551234567"
374daemon_url = "http://localhost:8080"
375# allowed_numbers = []"#,
376 "email" => r#"[channels.email]
377enabled = true
378imap_host = "imap.example.com"
379smtp_host = "smtp.example.com"
380username = "bot@example.com"
381password_env = "EMAIL_PASSWORD"
382from_address = "bot@example.com"
383# allowed_senders = []"#,
384 "matrix" => r#"[channels.matrix]
385enabled = true
386homeserver_url = "https://matrix.example.com"
387access_token_env = "MATRIX_ACCESS_TOKEN"
388# allowed_rooms = []
389# auto_join = false"#,
390 other => {
391 eprintln!(" {ERR} Unknown platform: {other}");
392 eprintln!(" {DIM}Available: telegram, discord, whatsapp, signal, email, matrix{RESET}");
393 eprintln!();
394 return Ok(());
395 }
396 };
397
398 eprintln!(" {ACTION} Add the following to your {ACCENT}roboticus.toml{RESET}:\n");
399 eprintln!("{MONO}{snippet}{RESET}\n");
400 eprintln!(" {DETAIL} Then set the referenced environment variable(s) and restart.");
401 eprintln!(" {DIM}After restart, verify with: roboticus integrations test {channel}{RESET}");
402 eprintln!();
403 Ok(())
404}
405
406pub async fn cmd_integrations_disconnect(
407 _url: &str,
408 channel: &str,
409) -> Result<(), Box<dyn std::error::Error>> {
410 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
411 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
412 heading(&format!("Disconnect: {channel}"));
413
414 let section = format!("[channels.{}]", channel.to_ascii_lowercase());
415
416 eprintln!(" {ACTION} To disconnect {ACCENT}{channel}{RESET}, either:");
417 eprintln!();
418 eprintln!(" {DETAIL} 1. Set {MONO}enabled = false{RESET} in the {MONO}{section}{RESET} block");
419 eprintln!(" {DETAIL} 2. Or remove the entire {MONO}{section}{RESET} block from roboticus.toml");
420 eprintln!();
421 eprintln!(" {DIM}Then restart the server for changes to take effect.{RESET}");
422 eprintln!();
423 Ok(())
424}
425
426