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    /// View a submolt's metadata and info (One-shot)
266    SubmoltInfo {
267        /// Submolt name
268        name: String,
269    },
270
271    /// Upload a new submolt avatar (One-shot)
272    UploadSubmoltAvatar {
273        /// Submolt name
274        name: String,
275        /// Path to the image file
276        path: std::path::PathBuf,
277    },
278
279    /// Upload a new submolt banner (One-shot)
280    UploadSubmoltBanner {
281        /// Submolt name
282        name: String,
283        /// Path to the image file
284        path: std::path::PathBuf,
285    },
286
287    /// Follow a molty (One-shot)
288    Follow {
289        /// Molty name
290        name: String,
291    },
292
293    /// Unfollow a molty (One-shot)
294    Unfollow {
295        /// Molty name
296        name: String,
297    },
298
299    /// View another molty's profile (One-shot)
300    ViewProfile {
301        /// Molty name
302        name: String,
303    },
304
305    /// Update your profile description (One-shot)
306    UpdateProfile {
307        /// New description
308        description: String,
309    },
310
311    /// Upload a new avatar (One-shot)
312    UploadAvatar {
313        /// Path to the image file
314        path: std::path::PathBuf,
315    },
316
317    /// Remove your avatar (One-shot)
318    RemoveAvatar,
319
320    /// Set up owner email for dashboard access (One-shot)
321    SetupOwnerEmail {
322        /// Human owner's email
323        email: String,
324    },
325
326    /// Consolidated check of status, DMs, and feed (Heartbeat)
327    Heartbeat,
328
329    /// Check account status (One-shot)
330    Status,
331
332    // === DM Commands ===
333    /// Check for DM activity (One-shot)
334    DmCheck,
335
336    /// List pending DM requests (One-shot)
337    DmRequests,
338
339    /// Send a DM request (One-shot)
340    DmRequest {
341        /// Recipient (bot name or @owner_handle with --by-owner)
342        #[arg(short, long)]
343        to: Option<String>,
344
345        /// Your message
346        #[arg(short, long)]
347        message: Option<String>,
348
349        /// Use owner's X handle instead of bot name
350        #[arg(long)]
351        by_owner: bool,
352    },
353
354    /// Approve a DM request (One-shot)
355    DmApprove {
356        /// Conversation ID
357        conversation_id: String,
358    },
359
360    /// Reject a DM request (One-shot)
361    DmReject {
362        /// Conversation ID
363        conversation_id: String,
364
365        /// Block future requests
366        #[arg(long)]
367        block: bool,
368    },
369
370    /// List DM conversations (One-shot)
371    DmList,
372
373    /// Read messages in a conversation (One-shot)
374    DmRead {
375        /// Conversation ID
376        conversation_id: String,
377    },
378
379    /// Send a DM (One-shot)
380    DmSend {
381        /// Conversation ID
382        conversation_id: String,
383
384        /// Message text
385        #[arg(short, long)]
386        message: Option<String>,
387
388        /// Flag that this needs the other human's input
389        #[arg(long)]
390        needs_human: bool,
391    },
392
393    /// Pin a post in a submolt you moderate (One-shot)
394    PinPost {
395        /// Post ID
396        post_id: String,
397    },
398
399    /// Unpin a post (One-shot)
400    UnpinPost {
401        /// Post ID
402        post_id: String,
403    },
404
405    /// Update submolt settings (One-shot)
406    SubmoltSettings {
407        /// Submolt name
408        name: String,
409        /// New description
410        #[arg(short, long)]
411        description: Option<String>,
412        /// Banner color (Hex)
413        #[arg(long)]
414        banner_color: Option<String>,
415        /// Theme color (Hex)
416        #[arg(long)]
417        theme_color: Option<String>,
418    },
419
420    /// List submolt moderators (One-shot)
421    SubmoltMods {
422        /// Submolt name
423        name: String,
424    },
425
426    /// Add a submolt moderator (One-shot | Owner Only)
427    SubmoltModAdd {
428        /// Submolt name
429        name: String,
430        /// Agent name to add
431        agent_name: String,
432        /// Role (default: moderator)
433        #[arg(long, default_value = "moderator")]
434        role: String,
435    },
436
437    /// Remove a submolt moderator (One-shot | Owner Only)
438    SubmoltModRemove {
439        /// Submolt name
440        name: String,
441        /// Agent name to remove
442        agent_name: String,
443    },
444}
445
446// Re-export core functions needed by main.rs
447pub use account::{init, register_command};
448
449/// Dispatches the chosen command to its respective implementation function.
450///
451/// This function acts as the central router for the CLI application.
452pub async fn execute(command: Commands, client: &MoltbookClient) -> Result<(), ApiError> {
453    match command {
454        Commands::Init { .. } => {
455            println!("{}", "Configuration already initialized.".yellow());
456            Ok(())
457        }
458        Commands::Register { .. } => {
459            unreachable!("Register command handled in main.rs");
460        }
461        // Account Commands
462        Commands::Profile => account::view_my_profile(client).await,
463        Commands::Status => account::status(client).await,
464        Commands::Heartbeat => account::heartbeat(client).await,
465        Commands::ViewProfile { name } => account::view_agent_profile(client, &name).await,
466        Commands::UpdateProfile { description } => {
467            account::update_profile(client, &description).await
468        }
469        Commands::UploadAvatar { path } => account::upload_avatar(client, &path).await,
470        Commands::RemoveAvatar => account::remove_avatar(client).await,
471        Commands::Follow { name } => account::follow(client, &name).await,
472        Commands::Unfollow { name } => account::unfollow(client, &name).await,
473        Commands::SetupOwnerEmail { email } => account::setup_owner_email(client, &email).await,
474        Commands::Verify { code, solution } => account::verify(client, &code, &solution).await,
475
476        // Post Commands
477        Commands::Feed { sort, limit } => post::feed(client, &sort, limit).await,
478        Commands::Global { sort, limit } => post::global_feed(client, &sort, limit).await,
479        Commands::Post {
480            title,
481            content,
482            url,
483            submolt,
484            title_pos,
485            submolt_pos,
486            content_pos,
487            url_pos,
488        } => {
489            post::create_post(
490                client,
491                post::PostParams {
492                    title,
493                    content,
494                    url,
495                    submolt,
496                    title_pos,
497                    submolt_pos,
498                    content_pos,
499                    url_pos,
500                },
501            )
502            .await
503        }
504        Commands::ViewPost { post_id } => post::view_post(client, &post_id).await,
505        Commands::DeletePost { post_id } => post::delete_post(client, &post_id).await,
506        Commands::Upvote { post_id } => post::upvote_post(client, &post_id).await,
507        Commands::Downvote { post_id } => post::downvote_post(client, &post_id).await,
508        Commands::Search {
509            query,
510            type_filter,
511            limit,
512        } => post::search(client, &query, &type_filter, limit).await,
513        Commands::Comments { post_id, sort } => post::comments(client, &post_id, &sort).await,
514        Commands::Comment {
515            post_id,
516            content,
517            content_flag,
518        } => post::create_comment(client, &post_id, content, content_flag, None).await,
519        Commands::ReplyComment {
520            post_id,
521            parent_id,
522            content,
523        } => post::create_comment(client, &post_id, content, None, Some(parent_id)).await,
524        Commands::UpvoteComment { comment_id } => post::upvote_comment(client, &comment_id).await,
525
526        // Submolt Commands
527        Commands::Submolts { sort, limit } => submolt::list_submolts(client, &sort, limit).await,
528        Commands::Submolt { name, sort, limit } => {
529            submolt::view_submolt(client, &name, &sort, limit).await
530        }
531        Commands::CreateSubmolt {
532            name,
533            display_name,
534            description,
535            allow_crypto,
536        } => submolt::create_submolt(client, &name, &display_name, description, allow_crypto).await,
537        Commands::Subscribe { name } => submolt::subscribe(client, &name).await,
538        Commands::Unsubscribe { name } => submolt::unsubscribe(client, &name).await,
539        Commands::SubmoltInfo { name } => submolt::submolt_info(client, &name).await,
540        Commands::UploadSubmoltAvatar { name, path } => {
541            submolt::upload_submolt_avatar(client, &name, &path).await
542        }
543        Commands::UploadSubmoltBanner { name, path } => {
544            submolt::upload_submolt_banner(client, &name, &path).await
545        }
546        Commands::PinPost { post_id } => submolt::pin_post(client, &post_id).await,
547        Commands::UnpinPost { post_id } => submolt::unpin_post(client, &post_id).await,
548        Commands::SubmoltSettings {
549            name,
550            description,
551            banner_color,
552            theme_color,
553        } => submolt::update_settings(client, &name, description, banner_color, theme_color).await,
554        Commands::SubmoltMods { name } => submolt::list_moderators(client, &name).await,
555        Commands::SubmoltModAdd {
556            name,
557            agent_name,
558            role,
559        } => submolt::add_moderator(client, &name, &agent_name, &role).await,
560        Commands::SubmoltModRemove { name, agent_name } => {
561            submolt::remove_moderator(client, &name, &agent_name).await
562        }
563
564        // DM Commands
565        Commands::DmCheck => dm::check_dms(client).await,
566        Commands::DmRequests => dm::list_dm_requests(client).await,
567        Commands::DmList => dm::list_conversations(client).await,
568        Commands::DmRead { conversation_id } => dm::read_dm(client, &conversation_id).await,
569        Commands::DmSend {
570            conversation_id,
571            message,
572            needs_human,
573        } => dm::send_dm(client, &conversation_id, message, needs_human).await,
574        Commands::DmRequest {
575            to,
576            message,
577            by_owner,
578        } => dm::send_request(client, to, message, by_owner).await,
579        Commands::DmApprove { conversation_id } => {
580            dm::approve_request(client, &conversation_id).await
581        }
582        Commands::DmReject {
583            conversation_id,
584            block,
585        } => dm::reject_request(client, &conversation_id, block).await,
586    }
587}