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