Skip to main content

moltbook_cli/cli/
mod.rs

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