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 = "Report daemon health (reachability, catalog, fleet)")]
Health,
#[command(description = "Launch a managed session: /launch <workdir> | <task>")]
Launch(String),
#[command(description = "List managed fleet sessions")]
Fleet,
#[command(description = "Get a managed session by id or name")]
Get(String),
#[command(description = "Send text to a managed session")]
Msend(String),
#[command(description = "Answer a managed session's pending decision")]
Answer(String),
#[command(description = "Inspect a managed session's activity")]
Activity(String),
#[command(description = "Resume a stopped managed session")]
Resume(String),
#[command(description = "Decommission a managed session (terminal)")]
Decommission(String),
#[command(description = "Focus a session so plain messages route to it")]
Focus(String),
#[command(description = "Clear the focused session")]
Unfocus,
#[command(description = "Summarize the focused session's activity")]
Summary,
#[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::Health => TrustyCommand::Health,
TelegramCommand::Launch(args) => {
let (repo_url, task) = split_launch_args(&args);
TrustyCommand::ManagedNew {
repo_url,
git_ref: String::new(),
task,
name_hint: None,
runtime: None,
inject_task: None,
deliverable_id: None,
force_new: false,
}
}
TelegramCommand::Fleet => TrustyCommand::ManagedFleet,
TelegramCommand::Get(target) => TrustyCommand::ManagedGet {
target: target.trim().to_string(),
},
TelegramCommand::Msend(args) => {
let (target, text) = split_target_args(&args);
TrustyCommand::ManagedSend { target, text }
}
TelegramCommand::Answer(args) => {
let (target, answer) = split_target_args(&args);
TrustyCommand::ManagedAnswer { target, answer }
}
TelegramCommand::Activity(target) => TrustyCommand::ManagedActivity {
target: target.trim().to_string(),
},
TelegramCommand::Resume(target) => TrustyCommand::ManagedResume {
target: target.trim().to_string(),
},
TelegramCommand::Decommission(target) => TrustyCommand::ManagedDecommission {
target: target.trim().to_string(),
},
TelegramCommand::Focus(_) | TelegramCommand::Unfocus | TelegramCommand::Summary => {
TrustyCommand::Help
}
TelegramCommand::Help => TrustyCommand::Help,
}
}
}
fn split_launch_args(args: &str) -> (String, String) {
match args.split_once('|') {
Some((workdir, task)) => (workdir.trim().to_string(), task.trim().to_string()),
None => (args.trim().to_string(), String::new()),
}
}
fn split_target_args(args: &str) -> (String, String) {
let trimmed = args.trim();
match trimmed.split_once(char::is_whitespace) {
Some((target, text)) => (target.to_string(), text.trim().to_string()),
None => (trimmed.to_string(), String::new()),
}
}
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::Health),
TrustyCommand::Health
);
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(), 31);
assert!(descriptions.iter().any(|c| c.command == "/health"));
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"));
assert!(descriptions.iter().any(|c| c.command == "/launch"));
assert!(descriptions.iter().any(|c| c.command == "/fleet"));
assert!(descriptions.iter().any(|c| c.command == "/get"));
assert!(descriptions.iter().any(|c| c.command == "/msend"));
assert!(descriptions.iter().any(|c| c.command == "/answer"));
assert!(descriptions.iter().any(|c| c.command == "/activity"));
assert!(descriptions.iter().any(|c| c.command == "/resume"));
assert!(descriptions.iter().any(|c| c.command == "/decommission"));
assert!(descriptions.iter().any(|c| c.command == "/focus"));
assert!(descriptions.iter().any(|c| c.command == "/unfocus"));
assert!(descriptions.iter().any(|c| c.command == "/summary"));
}
#[test]
fn proxy_verbs_are_adapter_local() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Focus("api".into())),
TrustyCommand::Help
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Unfocus),
TrustyCommand::Help
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Summary),
TrustyCommand::Help
);
}
#[test]
fn split_launch_args_splits_on_pipe() {
let (workdir, task) = split_launch_args("/work/repo | run the full test suite");
assert_eq!(workdir, "/work/repo");
assert_eq!(task, "run the full test suite");
let (workdir, task) = split_launch_args("/work/repo");
assert_eq!(workdir, "/work/repo");
assert!(task.is_empty());
}
#[test]
fn split_target_args_separates_target_and_text() {
let (target, text) = split_target_args("sess-1 please continue now");
assert_eq!(target, "sess-1");
assert_eq!(text, "please continue now");
let (target, text) = split_target_args("sess-1");
assert_eq!(target, "sess-1");
assert!(text.is_empty());
}
#[test]
fn launch_command_converts() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Launch("/work/repo | build it".into())),
TrustyCommand::ManagedNew {
repo_url: "/work/repo".into(),
git_ref: String::new(),
task: "build it".into(),
name_hint: None,
runtime: None,
inject_task: None,
deliverable_id: None,
force_new: false,
}
);
}
#[test]
fn fleet_command_converts() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Fleet),
TrustyCommand::ManagedFleet
);
}
#[test]
fn get_command_converts() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Get(" sess-1 ".into())),
TrustyCommand::ManagedGet {
target: "sess-1".into()
}
);
}
#[test]
fn msend_command_converts() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Msend("sess-1 build now".into())),
TrustyCommand::ManagedSend {
target: "sess-1".into(),
text: "build now".into(),
}
);
}
#[test]
fn answer_command_converts() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Answer("sess-1 yes proceed".into())),
TrustyCommand::ManagedAnswer {
target: "sess-1".into(),
answer: "yes proceed".into(),
}
);
}
#[test]
fn activity_resume_decommission_convert() {
assert_eq!(
TrustyCommand::from(TelegramCommand::Activity("a".into())),
TrustyCommand::ManagedActivity { target: "a".into() }
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Resume("b".into())),
TrustyCommand::ManagedResume { target: "b".into() }
);
assert_eq!(
TrustyCommand::from(TelegramCommand::Decommission("c".into())),
TrustyCommand::ManagedDecommission { target: "c".into() }
);
}
#[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()));
}
}