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