use crate::client::TrustyCommand;
use teloxide::utils::command::BotCommands;
#[derive(BotCommands, Clone, Debug, PartialEq, Eq)]
#[command(rename_rule = "lowercase", description = "trusty-mpm commands:")]
pub enum TelegramCommand {
#[command(description = "List managed sessions")]
Sessions,
#[command(description = "Session status")]
Status(String),
#[command(description = "Approve a permission request")]
Approve(String),
#[command(description = "Deny a permission request")]
Deny(String),
#[command(description = "Show overseer status")]
Overseer,
#[command(description = "List all tmux sessions")]
Tmux,
#[command(description = "Discover projects from Claude Code config")]
Projects,
#[command(description = "Auto-discover tmux sessions running Claude Code")]
Discover,
#[command(description = "Adopt an external tmux session")]
Adopt(String),
#[command(description = "Analyze Claude Code config")]
Config(String),
#[command(description = "Capture tmux pane output")]
Snapshot(String),
#[command(description = "Kill a session")]
Kill(String),
#[command(description = "Send a prompt to a session")]
Send(String),
#[command(description = "Show alert subscriptions")]
Alerts,
#[command(description = "Pair with daemon")]
Pair(String),
#[command(description = "Start and pair")]
Start(String),
#[command(description = "Connect to or start a session (no deployment)")]
Connect(String),
#[command(description = "Run full system diagnostic")]
Doctor,
#[command(description = "Show all commands")]
Help,
}
impl From<TelegramCommand> for TrustyCommand {
fn from(cmd: TelegramCommand) -> Self {
match cmd {
TelegramCommand::Sessions => TrustyCommand::Sessions,
TelegramCommand::Status(session_id) => TrustyCommand::Status { session_id },
TelegramCommand::Approve(session_id) => TrustyCommand::Approve { session_id },
TelegramCommand::Deny(session_id) => TrustyCommand::Deny { session_id },
TelegramCommand::Overseer => TrustyCommand::Overseer,
TelegramCommand::Tmux => TrustyCommand::Tmux,
TelegramCommand::Projects => TrustyCommand::Projects,
TelegramCommand::Discover => TrustyCommand::Discover,
TelegramCommand::Adopt(session) => TrustyCommand::Adopt { session },
TelegramCommand::Config(project) => TrustyCommand::Config { project },
TelegramCommand::Snapshot(session) => TrustyCommand::Snapshot { session },
TelegramCommand::Kill(session_id) => TrustyCommand::Kill { session_id },
TelegramCommand::Send(args) => {
let (session, prompt) = split_send_args(&args);
TrustyCommand::Send { session, prompt }
}
TelegramCommand::Alerts => TrustyCommand::Alerts,
TelegramCommand::Pair(code) => TrustyCommand::Pair {
code: non_empty(code),
},
TelegramCommand::Connect(project) => TrustyCommand::Connect {
project: project.trim().into(),
session_name: None,
},
TelegramCommand::Start(_) => TrustyCommand::Start,
TelegramCommand::Doctor => TrustyCommand::Doctor,
TelegramCommand::Help => TrustyCommand::Help,
}
}
}
fn split_send_args(args: &str) -> (String, String) {
let trimmed = args.trim();
match trimmed.split_once(char::is_whitespace) {
Some((session, prompt)) => (session.to_string(), prompt.trim().to_string()),
None => (trimmed.to_string(), String::new()),
}
}
fn non_empty(s: String) -> Option<String> {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn telegram_command_converts_to_trusty_command() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Sessions),
TrustyCommand::Sessions
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Help),
TrustyCommand::Help
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Status("abc-123".into())),
TrustyCommand::Status {
session_id: "abc-123".into()
}
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Pair("A4X9KZ".into())),
TrustyCommand::Pair {
code: Some("A4X9KZ".into())
}
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Pair(String::new())),
TrustyCommand::Pair { code: None }
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Start("A4X9KZ".into())),
TrustyCommand::Start
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Doctor),
TrustyCommand::Doctor
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Connect("/work/p".into())),
TrustyCommand::Connect {
project: "/work/p".into(),
session_name: None,
}
);
}
#[test]
fn bot_commands_lists_every_command() {
let descriptions = TelegramCommand::bot_commands();
assert_eq!(descriptions.len(), 19);
assert!(descriptions.iter().any(|c| c.command == "/sessions"));
assert!(descriptions.iter().any(|c| c.command == "/connect"));
assert!(descriptions.iter().any(|c| c.command == "/pair"));
assert!(descriptions.iter().any(|c| c.command == "/start"));
assert!(descriptions.iter().any(|c| c.command == "/projects"));
assert!(descriptions.iter().any(|c| c.command == "/adopt"));
assert!(descriptions.iter().any(|c| c.command == "/send"));
assert!(descriptions.iter().any(|c| c.command == "/discover"));
assert!(descriptions.iter().any(|c| c.command == "/doctor"));
}
#[test]
fn bot_command_descriptions_fit_telegram_limits() {
for cmd in TelegramCommand::bot_commands() {
let name = cmd.command.trim_start_matches('/');
assert!(
(1..=32).contains(&name.chars().count()),
"command `{name}` name length out of Telegram's 1..=32 range",
);
assert!(
(3..=256).contains(&cmd.description.chars().count()),
"command `{}` description length {} out of Telegram's 3..=256 range",
cmd.command,
cmd.description.chars().count(),
);
}
}
#[test]
fn split_send_args_separates_session_and_prompt() {
let (session, prompt) = split_send_args("frontend run the tests please");
assert_eq!(session, "frontend");
assert_eq!(prompt, "run the tests please");
let (session, prompt) = split_send_args("frontend");
assert_eq!(session, "frontend");
assert!(prompt.is_empty());
}
#[test]
fn send_command_converts_to_trusty_command() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Send("frontend build now".into())),
TrustyCommand::Send {
session: "frontend".into(),
prompt: "build now".into(),
}
);
}
#[test]
fn parse_round_trips_a_command() {
let cmd = TelegramCommand::parse("/status abc-123", "trusty_mpm_bot").unwrap();
assert_eq!(cmd, TelegramCommand::Status("abc-123".into()));
}
}