use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "xmaster",
version,
about = "Enterprise-grade X/Twitter CLI — post, reply, like, retweet, DM, search, and more",
long_about = "Built by 199 Biotechnologies for AI agents and humans.\n\nAgent-friendly: auto-JSON when piped, semantic exit codes, structured errors."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true)]
pub json: bool,
}
#[derive(Subcommand)]
pub enum Commands {
Post {
text: String,
#[arg(long)]
reply_to: Option<String>,
#[arg(long)]
quote: Option<String>,
#[arg(long, num_args = 1..=4)]
media: Vec<String>,
#[arg(long)]
poll: Option<String>,
#[arg(long, default_value = "1440")]
poll_duration: u64,
},
Delete {
id: String,
},
Like {
id: String,
},
Unlike {
id: String,
},
Retweet {
id: String,
},
Unretweet {
id: String,
},
Bookmark {
id: String,
},
Unbookmark {
id: String,
},
Follow {
username: String,
},
Unfollow {
username: String,
},
Dm {
#[command(subcommand)]
action: DmCommands,
},
Timeline {
#[arg(long)]
user: Option<String>,
#[arg(long, short, default_value = "10")]
count: usize,
},
Mentions {
#[arg(long, short, default_value = "10")]
count: usize,
#[arg(long)]
since_id: Option<String>,
},
Search {
query: String,
#[arg(long, default_value = "recent")]
mode: String,
#[arg(long, short, default_value = "10")]
count: usize,
},
SearchAi {
query: String,
#[arg(long, short, default_value = "10")]
count: usize,
#[arg(long)]
from_date: Option<String>,
#[arg(long)]
to_date: Option<String>,
},
Trending {
#[arg(long)]
region: Option<String>,
#[arg(long)]
category: Option<String>,
},
User {
username: String,
},
Me,
Bookmarks {
#[command(subcommand)]
action: BookmarkCommands,
},
Followers {
username: String,
#[arg(long, short, default_value = "20")]
count: usize,
},
Following {
username: String,
#[arg(long, short, default_value = "20")]
count: usize,
},
Config {
#[command(subcommand)]
action: ConfigCommands,
},
AgentInfo,
Thread {
texts: Vec<String>,
#[arg(long, num_args = 1..=4)]
media: Vec<String>,
},
Reply {
id: String,
text: String,
#[arg(long, num_args = 1..=4)]
media: Vec<String>,
},
Metrics {
ids: Vec<String>,
},
Lists {
#[command(subcommand)]
action: ListCommands,
},
HideReply {
id: String,
},
UnhideReply {
id: String,
},
RateLimits,
Block {
username: String,
},
Unblock {
username: String,
},
Mute {
username: String,
},
Unmute {
username: String,
},
Analyze {
text: String,
#[arg(long)]
goal: Option<String>,
},
Track {
#[command(subcommand)]
action: TrackCommands,
},
Report {
#[command(subcommand)]
action: ReportCommands,
},
Suggest {
#[command(subcommand)]
action: SuggestCommands,
},
Schedule {
#[command(subcommand)]
action: ScheduleCommands,
},
Engage {
#[command(subcommand)]
action: EngageCommands,
},
Skill {
#[command(subcommand)]
action: SkillCommands,
},
Update {
#[arg(long)]
check: bool,
},
}
#[derive(Subcommand)]
pub enum SkillCommands {
Install,
Update,
Status,
}
#[derive(Subcommand)]
pub enum EngageCommands {
Recommend {
#[arg(long)]
topic: Option<String>,
#[arg(long, default_value = "1000")]
min_followers: u32,
#[arg(long, short, default_value = "5")]
count: usize,
},
}
#[derive(Subcommand)]
pub enum ScheduleCommands {
Add {
content: String,
#[arg(long)]
at: String,
#[arg(long)]
reply_to: Option<String>,
#[arg(long)]
quote: Option<String>,
#[arg(long, num_args = 1..=4)]
media: Vec<String>,
},
List {
#[arg(long)]
status: Option<String>,
},
Cancel {
id: String,
},
Reschedule {
id: String,
#[arg(long)]
at: String,
},
Fire,
Setup,
}
#[derive(Subcommand)]
pub enum TrackCommands {
Run,
Status,
}
#[derive(Subcommand)]
pub enum ReportCommands {
Daily,
Weekly,
}
#[derive(Subcommand)]
pub enum SuggestCommands {
BestTime,
NextPost,
}
#[derive(Subcommand)]
pub enum ListCommands {
Create {
name: String,
#[arg(long)]
description: Option<String>,
},
Delete {
id: String,
},
Add {
list_id: String,
username: String,
},
Remove {
list_id: String,
username: String,
},
Timeline {
list_id: String,
#[arg(long, short, default_value = "10")]
count: usize,
},
Mine {
#[arg(long, short, default_value = "20")]
count: usize,
},
}
#[derive(Subcommand)]
pub enum DmCommands {
Send {
username: String,
text: String,
},
Inbox {
#[arg(long, short, default_value = "10")]
count: usize,
},
Thread {
id: String,
#[arg(long, short, default_value = "20")]
count: usize,
},
}
#[derive(Subcommand)]
pub enum ConfigCommands {
Show,
Set {
key: String,
value: String,
},
Check,
Guide,
Auth,
WebLogin,
}
#[derive(Subcommand)]
pub enum BookmarkCommands {
List {
#[arg(long, short, default_value = "10")]
count: usize,
#[arg(long)]
unread: bool,
},
Sync {
#[arg(long, short, default_value = "100")]
count: usize,
},
Search {
query: String,
},
Export {
#[arg(long, short)]
output: Option<String>,
#[arg(long)]
unread: bool,
},
Digest {
#[arg(long, short, default_value = "7")]
days: u32,
},
Stats,
}
pub fn parse_tweet_id(input: &str) -> String {
let input = input.trim();
if input.contains("x.com/") || input.contains("twitter.com/") {
let parts: Vec<&str> = input.split('/').filter(|s| !s.is_empty()).collect();
if let Some(pos) = parts.iter().position(|&p| p == "status") {
if let Some(id_part) = parts.get(pos + 1) {
let id = id_part.split('?').next().unwrap_or(id_part);
if !id.is_empty() && id.chars().all(|c| c.is_ascii_digit()) {
return id.to_string();
}
}
}
}
input.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_id() {
assert_eq!(parse_tweet_id("1234567890"), "1234567890");
}
#[test]
fn x_url() {
assert_eq!(
parse_tweet_id("https://x.com/user/status/1234567890"),
"1234567890"
);
}
#[test]
fn twitter_url() {
assert_eq!(
parse_tweet_id("https://twitter.com/user/status/1234567890"),
"1234567890"
);
}
#[test]
fn url_with_query() {
assert_eq!(
parse_tweet_id("https://x.com/user/status/1234567890?s=20"),
"1234567890"
);
}
#[test]
fn url_with_trailing_slash() {
assert_eq!(
parse_tweet_id("https://x.com/user/status/1234567890/"),
"1234567890"
);
}
#[test]
fn url_with_multiple_query_params() {
assert_eq!(
parse_tweet_id("https://x.com/user/status/9876543210?s=20&t=abc"),
"9876543210"
);
}
#[test]
fn whitespace_trimmed() {
assert_eq!(parse_tweet_id(" 1234567890 "), "1234567890");
}
#[test]
fn url_with_photo_suffix() {
assert_eq!(
parse_tweet_id("https://x.com/user/status/1234567890/photo/1"),
"1234567890"
);
}
#[test]
fn url_with_video_suffix() {
assert_eq!(
parse_tweet_id("https://x.com/user/status/9876543210/video/1"),
"9876543210"
);
}
}