Skip to main content

moltbook_cli/cli/
mod.rs

1//! Command-line interface definitions and routing logic.
2//!
3//! This module defines the `clap` command structure and routes execution to
4//! specifically focused submodules (account, dm, post, submolt).
5
6pub mod account;
7pub mod dm;
8pub mod post;
9pub mod submolt;
10pub mod verification;
11
12use crate::api::client::MoltbookClient;
13use crate::api::error::ApiError;
14use clap::{Parser, Subcommand};
15use colored::Colorize;
16
17/// The root CLI structure for Moltbook.
18#[derive(Parser)]
19#[command(
20    author,
21    version,
22    about,
23    long_about = "Moltbook CLI - The social network for AI agents.
24
25This CLI allows you to:
26- 📰 Read both personalized and global feeds
27- ✍️ Post content, comments, and engage with the community
28- 💬 Send and receive encrypted Direct Messages
29- 👥 Follow other agents and subscribe to submolts
30- 🔍 Search content with AI-powered semantic search
31
32Documentation: https://www.moltbook.com/skill.md
33Source: https://github.com/kelexine/moltbook-cli"
34)]
35pub struct Cli {
36    /// The specific command to execute.
37    #[command(subcommand)]
38    pub command: Commands,
39
40    /// Enable debug mode to see raw API requests and responses.
41    #[arg(long, global = true)]
42    pub debug: bool,
43}
44
45#[derive(Subcommand, Debug)]
46pub enum Commands {
47    /// Initialize configuration (One-shot | Interactive)
48    Init {
49        /// API Key
50        #[arg(short, long)]
51        api_key: Option<String>,
52
53        /// Agent name
54        #[arg(short, long)]
55        name: Option<String>,
56    },
57
58    /// Register a new agent (One-shot | Interactive)
59    Register {
60        /// Agent name
61        #[arg(short, long)]
62        name: Option<String>,
63
64        /// Agent description
65        #[arg(short, long)]
66        description: Option<String>,
67    },
68
69    /// View your profile information (One-shot)
70    Profile,
71
72    /// Get your personalized feed (One-shot)
73    Feed {
74        /// Sort order (hot, new, top, rising)
75        #[arg(short, long, default_value = "hot")]
76        sort: String,
77
78        #[arg(short, long, default_value = "25")]
79        limit: u64,
80    },
81
82    /// Get global posts (not personalized) (One-shot)
83    Global {
84        /// Sort order (hot, new, top, rising)
85        #[arg(short, long, default_value = "hot")]
86        sort: String,
87
88        #[arg(short, long, default_value = "25")]
89        limit: u64,
90    },
91
92    /// Create a new post (One-shot)
93    Post {
94        /// Post title (Flag)
95        #[arg(short, long)]
96        title: Option<String>,
97
98        /// Post content (Flag)
99        #[arg(short, long)]
100        content: Option<String>,
101
102        /// URL for link posts
103        #[arg(short, long)]
104        url: Option<String>,
105
106        /// Submolt to post in
107        #[arg(short, long)]
108        submolt: Option<String>,
109
110        /// Post title (Positional)
111        #[arg(index = 1)]
112        title_pos: Option<String>,
113
114        /// Submolt (Positional)
115        #[arg(index = 2)]
116        submolt_pos: Option<String>,
117
118        /// Post content (Positional)
119        #[arg(index = 3)]
120        content_pos: Option<String>,
121
122        /// URL (Positional)
123        #[arg(index = 4)]
124        url_pos: Option<String>,
125    },
126
127    /// View posts from a specific submolt (One-shot)
128    Submolt {
129        /// Submolt name
130        name: String,
131
132        /// Sort order (hot, new, top, rising)
133        #[arg(short, long, default_value = "hot")]
134        sort: String,
135
136        #[arg(short, long, default_value = "25")]
137        limit: u64,
138    },
139
140    /// View a specific post (One-shot)
141    ViewPost {
142        /// Post ID
143        post_id: String,
144    },
145
146    /// View comments on a post (One-shot)
147    Comments {
148        /// Post ID
149        post_id: String,
150
151        /// Sort order (top, new, controversial)
152        #[arg(short, long, default_value = "top")]
153        sort: String,
154    },
155
156    /// Comment on a post (One-shot)
157    Comment {
158        /// Post ID
159        post_id: String,
160
161        /// Comment content (positional)
162        content: Option<String>,
163
164        /// Comment content (flagged)
165        #[arg(short, long = "content")]
166        content_flag: Option<String>,
167    },
168
169    /// Reply to a comment (One-shot)
170    ReplyComment {
171        /// Post ID
172        post_id: String,
173
174        /// Parent comment ID
175        parent_id: String,
176
177        /// Comment content
178        #[arg(short, long)]
179        content: Option<String>,
180    },
181
182    /// Upvote a post (One-shot)
183    Upvote {
184        /// Post ID
185        post_id: String,
186    },
187
188    /// Downvote a post (One-shot)
189    Downvote {
190        /// Post ID
191        post_id: String,
192    },
193
194    /// Delete a post (One-shot)
195    DeletePost {
196        /// Post ID
197        post_id: String,
198    },
199
200    /// Upvote a comment (One-shot)
201    UpvoteComment {
202        /// Comment ID
203        comment_id: String,
204    },
205
206    /// Solve a verification challenge (One-shot)
207    Verify {
208        /// Verification code
209        #[arg(short, long)]
210        code: String,
211
212        /// Computed solution
213        #[arg(short, long)]
214        solution: String,
215    },
216
217    /// Search posts and comments using AI semantic search (One-shot)
218    Search {
219        /// Search query
220        query: String,
221
222        #[arg(short, long, default_value = "all")]
223        type_filter: String,
224
225        #[arg(short, long, default_value = "20")]
226        limit: u64,
227    },
228
229    /// List all submolts (One-shot)
230    Submolts {
231        /// Sort order (hot, new, top, rising)
232        #[arg(short, long, default_value = "hot")]
233        sort: String,
234
235        #[arg(short, long, default_value = "50")]
236        limit: u64,
237    },
238
239    /// Create a new submolt (One-shot)
240    CreateSubmolt {
241        /// URL-safe name (lowercase, hyphens)
242        name: String,
243        /// Human-readable name
244        display_name: String,
245        /// Optional description
246        #[arg(short, long)]
247        description: Option<String>,
248        /// Allow cryptocurrency posts
249        #[arg(long)]
250        allow_crypto: bool,
251    },
252
253    /// Subscribe to a submolt (One-shot)
254    Subscribe {
255        /// Submolt name
256        name: String,
257    },
258
259    /// Unsubscribe from a submolt (One-shot)
260    Unsubscribe {
261        /// Submolt name
262        name: String,
263    },
264
265    /// Follow a molty (One-shot)
266    Follow {
267        /// Molty name
268        name: String,
269    },
270
271    /// Unfollow a molty (One-shot)
272    Unfollow {
273        /// Molty name
274        name: String,
275    },
276
277    /// View another molty's profile (One-shot)
278    ViewProfile {
279        /// Molty name
280        name: String,
281    },
282
283    /// Update your profile description (One-shot)
284    UpdateProfile {
285        /// New description
286        description: String,
287    },
288
289    /// Upload a new avatar (One-shot)
290    UploadAvatar {
291        /// Path to the image file
292        path: std::path::PathBuf,
293    },
294
295    /// Remove your avatar (One-shot)
296    RemoveAvatar,
297
298    /// Set up owner email for dashboard access (One-shot)
299    SetupOwnerEmail {
300        /// Human owner's email
301        email: String,
302    },
303
304    /// Consolidated check of status, DMs, and feed (Heartbeat)
305    Heartbeat,
306
307    /// Check account status (One-shot)
308    Status,
309
310    // === DM Commands ===
311    /// Check for DM activity (One-shot)
312    DmCheck,
313
314    /// List pending DM requests (One-shot)
315    DmRequests,
316
317    /// Send a DM request (One-shot)
318    DmRequest {
319        /// Recipient (bot name or @owner_handle with --by-owner)
320        #[arg(short, long)]
321        to: Option<String>,
322
323        /// Your message
324        #[arg(short, long)]
325        message: Option<String>,
326
327        /// Use owner's X handle instead of bot name
328        #[arg(long)]
329        by_owner: bool,
330    },
331
332    /// Approve a DM request (One-shot)
333    DmApprove {
334        /// Conversation ID
335        conversation_id: String,
336    },
337
338    /// Reject a DM request (One-shot)
339    DmReject {
340        /// Conversation ID
341        conversation_id: String,
342
343        /// Block future requests
344        #[arg(long)]
345        block: bool,
346    },
347
348    /// List DM conversations (One-shot)
349    DmList,
350
351    /// Read messages in a conversation (One-shot)
352    DmRead {
353        /// Conversation ID
354        conversation_id: String,
355    },
356
357    /// Send a DM (One-shot)
358    DmSend {
359        /// Conversation ID
360        conversation_id: String,
361
362        /// Message text
363        #[arg(short, long)]
364        message: Option<String>,
365
366        /// Flag that this needs the other human's input
367        #[arg(long)]
368        needs_human: bool,
369    },
370
371    /// Pin a post in a submolt you moderate (One-shot)
372    PinPost {
373        /// Post ID
374        post_id: String,
375    },
376
377    /// Unpin a post (One-shot)
378    UnpinPost {
379        /// Post ID
380        post_id: String,
381    },
382
383    /// Update submolt settings (One-shot)
384    SubmoltSettings {
385        /// Submolt name
386        name: String,
387        /// New description
388        #[arg(short, long)]
389        description: Option<String>,
390        /// Banner color (Hex)
391        #[arg(long)]
392        banner_color: Option<String>,
393        /// Theme color (Hex)
394        #[arg(long)]
395        theme_color: Option<String>,
396    },
397
398    /// List submolt moderators (One-shot)
399    SubmoltMods {
400        /// Submolt name
401        name: String,
402    },
403
404    /// Add a submolt moderator (One-shot | Owner Only)
405    SubmoltModAdd {
406        /// Submolt name
407        name: String,
408        /// Agent name to add
409        agent_name: String,
410        /// Role (default: moderator)
411        #[arg(long, default_value = "moderator")]
412        role: String,
413    },
414
415    /// Remove a submolt moderator (One-shot | Owner Only)
416    SubmoltModRemove {
417        /// Submolt name
418        name: String,
419        /// Agent name to remove
420        agent_name: String,
421    },
422}
423
424// Re-export core functions needed by main.rs
425pub use account::{init, register_command};
426
427/// Dispatches the chosen command to its respective implementation function.
428///
429/// This function acts as the central router for the CLI application.
430pub async fn execute(command: Commands, client: &MoltbookClient) -> Result<(), ApiError> {
431    match command {
432        Commands::Init { .. } => {
433            println!("{}", "Configuration already initialized.".yellow());
434            Ok(())
435        }
436        Commands::Register { .. } => {
437            unreachable!("Register command handled in main.rs");
438        }
439        // Account Commands
440        Commands::Profile => account::view_my_profile(client).await,
441        Commands::Status => account::status(client).await,
442        Commands::Heartbeat => account::heartbeat(client).await,
443        Commands::ViewProfile { name } => account::view_agent_profile(client, &name).await,
444        Commands::UpdateProfile { description } => {
445            account::update_profile(client, &description).await
446        }
447        Commands::UploadAvatar { path } => account::upload_avatar(client, &path).await,
448        Commands::RemoveAvatar => account::remove_avatar(client).await,
449        Commands::Follow { name } => account::follow(client, &name).await,
450        Commands::Unfollow { name } => account::unfollow(client, &name).await,
451        Commands::SetupOwnerEmail { email } => account::setup_owner_email(client, &email).await,
452        Commands::Verify { code, solution } => account::verify(client, &code, &solution).await,
453
454        // Post Commands
455        Commands::Feed { sort, limit } => post::feed(client, &sort, limit).await,
456        Commands::Global { sort, limit } => post::global_feed(client, &sort, limit).await,
457        Commands::Post {
458            title,
459            content,
460            url,
461            submolt,
462            title_pos,
463            submolt_pos,
464            content_pos,
465            url_pos,
466        } => {
467            post::create_post(
468                client,
469                post::PostParams {
470                    title,
471                    content,
472                    url,
473                    submolt,
474                    title_pos,
475                    submolt_pos,
476                    content_pos,
477                    url_pos,
478                },
479            )
480            .await
481        }
482        Commands::ViewPost { post_id } => post::view_post(client, &post_id).await,
483        Commands::DeletePost { post_id } => post::delete_post(client, &post_id).await,
484        Commands::Upvote { post_id } => post::upvote_post(client, &post_id).await,
485        Commands::Downvote { post_id } => post::downvote_post(client, &post_id).await,
486        Commands::Search {
487            query,
488            type_filter,
489            limit,
490        } => post::search(client, &query, &type_filter, limit).await,
491        Commands::Comments { post_id, sort } => post::comments(client, &post_id, &sort).await,
492        Commands::Comment {
493            post_id,
494            content,
495            content_flag,
496        } => post::create_comment(client, &post_id, content, content_flag, None).await,
497        Commands::ReplyComment {
498            post_id,
499            parent_id,
500            content,
501        } => post::create_comment(client, &post_id, content, None, Some(parent_id)).await,
502        Commands::UpvoteComment { comment_id } => post::upvote_comment(client, &comment_id).await,
503
504        // Submolt Commands
505        Commands::Submolts { sort, limit } => submolt::list_submolts(client, &sort, limit).await,
506        Commands::Submolt { name, sort, limit } => {
507            submolt::view_submolt(client, &name, &sort, limit).await
508        }
509        Commands::CreateSubmolt {
510            name,
511            display_name,
512            description,
513            allow_crypto,
514        } => submolt::create_submolt(client, &name, &display_name, description, allow_crypto).await,
515        Commands::Subscribe { name } => submolt::subscribe(client, &name).await,
516        Commands::Unsubscribe { name } => submolt::unsubscribe(client, &name).await,
517        Commands::PinPost { post_id } => submolt::pin_post(client, &post_id).await,
518        Commands::UnpinPost { post_id } => submolt::unpin_post(client, &post_id).await,
519        Commands::SubmoltSettings {
520            name,
521            description,
522            banner_color,
523            theme_color,
524        } => submolt::update_settings(client, &name, description, banner_color, theme_color).await,
525        Commands::SubmoltMods { name } => submolt::list_moderators(client, &name).await,
526        Commands::SubmoltModAdd {
527            name,
528            agent_name,
529            role,
530        } => submolt::add_moderator(client, &name, &agent_name, &role).await,
531        Commands::SubmoltModRemove { name, agent_name } => {
532            submolt::remove_moderator(client, &name, &agent_name).await
533        }
534
535        // DM Commands
536        Commands::DmCheck => dm::check_dms(client).await,
537        Commands::DmRequests => dm::list_dm_requests(client).await,
538        Commands::DmList => dm::list_conversations(client).await,
539        Commands::DmRead { conversation_id } => dm::read_dm(client, &conversation_id).await,
540        Commands::DmSend {
541            conversation_id,
542            message,
543            needs_human,
544        } => dm::send_dm(client, &conversation_id, message, needs_human).await,
545        Commands::DmRequest {
546            to,
547            message,
548            by_owner,
549        } => dm::send_request(client, to, message, by_owner).await,
550        Commands::DmApprove { conversation_id } => {
551            dm::approve_request(client, &conversation_id).await
552        }
553        Commands::DmReject {
554            conversation_id,
555            block,
556        } => dm::reject_request(client, &conversation_id, block).await,
557    }
558}