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