1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
//! CLI argument definitions (clap derive structs and enums).
//!
//! Pure data structures with no runtime behavior. Command dispatch lives in main.rs.
use crate::output::OutputFormat;
use clap::Parser;
#[derive(Parser)]
#[command(name = "bird", about = "X API CLI", version)]
pub(crate) struct Cli {
#[command(subcommand)]
pub command: Command,
/// Username for multi-user token selection (maps to xurl -u)
#[arg(long, short = 'u', global = true)]
pub username: Option<String>,
/// Plain output (no color, no hyperlinks; script-friendly)
#[arg(long, global = true)]
pub plain: bool,
/// Disable ANSI colors (or set NO_COLOR)
#[arg(long, global = true)]
pub no_color: bool,
/// Bypass store read, still write response to store
#[arg(long, global = true)]
pub refresh: bool,
/// Disable entity store entirely (no read, no write)
#[arg(long, global = true)]
pub no_cache: bool,
/// Only serve from local store; never make API requests
#[arg(long, global = true)]
pub cache_only: bool,
/// Suppress informational stderr output (keep only fatal errors)
#[arg(
long,
short = 'q',
global = true,
env = "BIRD_QUIET",
value_parser = clap::builder::FalseyValueParser::new(),
)]
pub quiet: bool,
/// Error output format: text (default for TTY), json (default for non-TTY)
#[arg(long, global = true, value_enum, env = "BIRD_OUTPUT")]
pub output: Option<OutputFormat>,
}
#[derive(clap::Subcommand)]
pub(crate) enum Command {
/// Authenticate via xurl (OAuth2 PKCE browser flow)
Login,
/// Show current user (GET /2/users/me)
Me {
/// Human-readable output
#[arg(long)]
pretty: bool,
},
/// GET request to path (e.g. /2/users/me or /2/users/{id}/bookmarks with -p id=123)
Get {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[arg(long)]
pretty: bool,
},
/// POST request to path
Post {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[arg(long, value_name = "JSON")]
body: Option<String>,
#[arg(long)]
pretty: bool,
},
/// PUT request to path
Put {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[arg(long, value_name = "JSON")]
body: Option<String>,
#[arg(long)]
pretty: bool,
},
/// List bookmarks for the current user (paginated, max_results=100)
Bookmarks {
#[arg(long)]
pretty: bool,
},
/// Look up a user profile by username
Profile {
/// X/Twitter username (with or without @)
username: String,
/// Pretty-print JSON output
#[arg(long)]
pretty: bool,
},
/// Search recent tweets (GET /2/tweets/search/recent)
Search {
/// Search query (X API search syntax)
query: String,
/// Pretty-print JSON output
#[arg(long)]
pretty: bool,
/// Sort results: recent (default), likes
#[arg(long, default_value = "recent")]
sort: String,
/// Minimum like count threshold
#[arg(long)]
min_likes: Option<u64>,
/// Maximum results per page (10-100, default: 100)
#[arg(long)]
max_results: Option<u32>,
/// Number of pages to fetch (1-10, default: 1)
#[arg(long)]
pages: Option<u32>,
},
/// Reconstruct a conversation thread from a tweet
Thread {
/// Tweet ID (root tweet or any reply in the thread)
tweet_id: String,
/// Pretty-print JSON output
#[arg(long)]
pretty: bool,
/// Maximum number of search result pages (default: 10, max: 25)
#[arg(long, default_value = "10")]
max_pages: u32,
},
/// DELETE request to path
Delete {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[arg(long)]
pretty: bool,
},
/// Monitor users: check recent activity, manage watchlist
Watchlist {
#[command(subcommand)]
action: WatchlistCommand,
/// Pretty-print JSON output
#[arg(long)]
pretty: bool,
},
/// View API usage and costs
Usage {
/// Show usage since this date (YYYY-MM-DD; default: 30 days ago)
#[arg(long)]
since: Option<String>,
/// Sync actual usage from X API (requires Bearer token via xurl)
#[arg(long)]
sync: bool,
/// Pretty-print output
#[arg(long)]
pretty: bool,
},
/// Post a tweet (via xurl)
Tweet {
/// Tweet text
text: String,
/// Media ID to attach
#[arg(long)]
media_id: Option<String>,
},
/// Reply to a tweet (via xurl)
Reply {
/// Tweet ID to reply to
tweet_id: String,
/// Reply text
text: String,
},
/// Like a tweet (via xurl)
Like {
/// Tweet ID to like
tweet_id: String,
},
/// Unlike a tweet (via xurl)
Unlike {
/// Tweet ID to unlike
tweet_id: String,
},
/// Repost (retweet) a tweet (via xurl)
Repost {
/// Tweet ID to repost
tweet_id: String,
},
/// Undo a repost (via xurl)
Unrepost {
/// Tweet ID to unrepost
tweet_id: String,
},
/// Follow a user (via xurl)
Follow {
/// Username to follow
username: String,
},
/// Unfollow a user (via xurl)
Unfollow {
/// Username to unfollow
username: String,
},
/// Send a direct message (via xurl)
Dm {
/// Username to message
username: String,
/// Message text
text: String,
},
/// Block a user (via xurl)
Block {
/// Username to block
username: String,
},
/// Unblock a user (via xurl)
Unblock {
/// Username to unblock
username: String,
},
/// Mute a user (via xurl)
Mute {
/// Username to mute
username: String,
},
/// Unmute a user (via xurl)
Unmute {
/// Username to unmute
username: String,
},
/// Show what is available: xurl status, commands, and entity store health
Doctor {
/// Scope report to this command only (e.g. me, bookmarks, get)
command: Option<String>,
#[arg(long)]
pretty: bool,
},
/// Manage the HTTP response cache
Cache {
#[command(subcommand)]
action: CacheAction,
},
/// Generate shell completions
Completions {
/// Shell to generate completions for
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
#[derive(clap::Subcommand)]
pub(crate) enum CacheAction {
/// Delete all cache entries
Clear,
/// Show cache status (JSON default, --pretty for human-readable)
Stats {
#[arg(long)]
pretty: bool,
},
}
#[derive(clap::Subcommand)]
pub(crate) enum WatchlistCommand {
/// Check recent activity for all watched users
Check,
/// Add a user to the watchlist
Add {
/// X/Twitter username (with or without @)
username: String,
},
/// Remove a user from the watchlist
Remove {
/// X/Twitter username to remove
username: String,
},
/// Show the current watchlist
List,
}