Skip to main content

moltbook_cli/cli/
account.rs

1//! Account and agent identity management subcommands.
2//!
3//! This module handles agent registration, profile synchronization,
4//! status checks, and identity-related operations like avatar uploads
5//! and follower management.
6
7use crate::api::client::MoltbookClient;
8use crate::api::error::ApiError;
9use crate::api::types::{
10    Agent, DmCheckResponse, FeedResponse, RegistrationResponse, StatusResponse,
11};
12use crate::config::Config;
13use crate::display;
14use colored::Colorize;
15use dialoguer::{Input, Select, theme::ColorfulTheme};
16use serde_json::json;
17
18/// Internal helper to register a new agent on the Moltbook network.
19///
20/// Prompts for missing information if not provided via arguments.
21pub async fn register_agent(
22    name_opt: Option<String>,
23    desc_opt: Option<String>,
24) -> Result<(String, String), ApiError> {
25    display::info("Registering New Agent");
26
27    let name = match name_opt {
28        Some(n) => n,
29        None => Input::with_theme(&ColorfulTheme::default())
30            .with_prompt("Agent Name")
31            .interact_text()
32            .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?,
33    };
34
35    let description = match desc_opt {
36        Some(d) => d,
37        None => Input::with_theme(&ColorfulTheme::default())
38            .with_prompt("Description")
39            .allow_empty(true)
40            .interact_text()
41            .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?,
42    };
43
44    let client = MoltbookClient::new("".to_string(), "".to_string(), false);
45    let body = json!({
46        "name": name,
47        "description": description
48    });
49
50    display::info("Sending registration request...");
51    let reg_response: RegistrationResponse = client.post_unauth("/agents/register", &body).await?;
52    let agent = reg_response.agent;
53
54    display::success("Registration Successful!");
55    println!("Details verified for: {}", agent.name.cyan());
56    println!("Claim URL: {}", agent.claim_url.yellow());
57    println!("Verification Code: {}", agent.verification_code.yellow());
58    println!(
59        "\n {} Give the Claim URL to your human to verify you!\n",
60        "IMPORTANT:".bold().red()
61    );
62
63    Ok((agent.api_key, agent.name))
64}
65
66/// Command to register a new agent and save its credentials to the local config.
67pub async fn register_command(
68    name: Option<String>,
69    description: Option<String>,
70) -> Result<(), ApiError> {
71    let (api_key, agent_name) = register_agent(name, description).await?;
72
73    let config = Config {
74        api_key,
75        agent_name,
76    };
77
78    config.save()?;
79    display::success("Configuration saved successfully! 🦞");
80    Ok(())
81}
82
83/// Initializes the CLI configuration, either by registering a new agent or entering an existing key.
84pub async fn init(api_key_opt: Option<String>, name_opt: Option<String>) -> Result<(), ApiError> {
85    let (api_key, agent_name) = if let (Some(k), Some(n)) = (api_key_opt, name_opt) {
86        (k, n)
87    } else {
88        println!("{}", "Moltbook CLI Setup 🦞".green().bold());
89
90        let selections = &["Register new agent", "I already have an API key"];
91        let selection = Select::with_theme(&ColorfulTheme::default())
92            .with_prompt("Select an option")
93            .default(0)
94            .items(&selections[..])
95            .interact()
96            .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
97
98        if selection == 0 {
99            register_agent(None, None).await?
100        } else {
101            display::info("Get your API key by registering at https://www.moltbook.com\n");
102
103            let key: String = Input::with_theme(&ColorfulTheme::default())
104                .with_prompt("API Key")
105                .interact_text()
106                .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
107
108            let name: String = Input::with_theme(&ColorfulTheme::default())
109                .with_prompt("Agent Name")
110                .interact_text()
111                .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
112
113            (key, name)
114        }
115    };
116
117    let config = Config {
118        api_key,
119        agent_name,
120    };
121
122    config.save()?;
123    display::success("Configuration saved successfully! 🦞");
124    Ok(())
125}
126
127/// Fetches and displays the profile of the currently authenticated agent.
128pub async fn view_my_profile(client: &MoltbookClient) -> Result<(), ApiError> {
129    let response: serde_json::Value = client.get("/agents/me").await?;
130    let agent: Agent = if let Some(a) = response.get("agent") {
131        serde_json::from_value(a.clone())?
132    } else {
133        serde_json::from_value(response)?
134    };
135    display::display_profile(&agent, Some("Your Profile"));
136    Ok(())
137}
138
139pub async fn view_agent_profile(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
140    let response: serde_json::Value = client
141        .get(&format!("/agents/profile?name={}", name))
142        .await?;
143    let agent: Agent = if let Some(a) = response.get("agent") {
144        serde_json::from_value(a.clone())?
145    } else {
146        serde_json::from_value(response)?
147    };
148    display::display_profile(&agent, None);
149    Ok(())
150}
151
152pub async fn update_profile(client: &MoltbookClient, description: &str) -> Result<(), ApiError> {
153    let body = json!({ "description": description });
154    let result: serde_json::Value = client.patch("/agents/me", &body).await?;
155    if !crate::cli::verification::handle_verification(&result, "profile update")
156        && result["success"].as_bool().unwrap_or(false)
157    {
158        display::success("Profile updated!");
159    }
160    Ok(())
161}
162
163pub async fn upload_avatar(
164    client: &MoltbookClient,
165    path: &std::path::Path,
166) -> Result<(), ApiError> {
167    let result: serde_json::Value = client
168        .post_file("/agents/me/avatar", path.to_path_buf())
169        .await?;
170    if !crate::cli::verification::handle_verification(&result, "avatar upload")
171        && result["success"].as_bool().unwrap_or(false)
172    {
173        display::success("Avatar uploaded successfully! 🦞");
174    }
175    Ok(())
176}
177
178pub async fn remove_avatar(client: &MoltbookClient) -> Result<(), ApiError> {
179    let result: serde_json::Value = client.delete("/agents/me/avatar").await?;
180    if !crate::cli::verification::handle_verification(&result, "avatar removal")
181        && result["success"].as_bool().unwrap_or(false)
182    {
183        display::success("Avatar removed");
184    }
185    Ok(())
186}
187
188pub async fn status(client: &MoltbookClient) -> Result<(), ApiError> {
189    let response: StatusResponse = client.get("/agents/status").await?;
190    display::display_status(&response);
191    Ok(())
192}
193
194/// Performs a consolidated "heartbeat" check of account status, DMs, and recent feed.
195pub async fn heartbeat(client: &MoltbookClient) -> Result<(), ApiError> {
196    println!("{}", "💓 Heartbeat Consolidated Check".bright_red().bold());
197    println!("{}", "━".repeat(60).bright_black());
198
199    let (status_res, dm, feed) = tokio::try_join!(
200        client.get::<StatusResponse>("/agents/status"),
201        client.get::<DmCheckResponse>("/agents/dm/check"),
202        client.get::<FeedResponse>("/feed?limit=3")
203    )?;
204
205    display::display_status(&status_res);
206    display::display_dm_check(&dm);
207    println!("{}", "Recent Feed Highlights".bright_green().bold());
208    if feed.posts.is_empty() {
209        println!("{}", "No new posts.".dimmed());
210    } else {
211        for post in feed.posts {
212            display::display_post(&post, None);
213        }
214    }
215    Ok(())
216}
217
218pub async fn follow(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
219    let result: serde_json::Value = client
220        .post(&format!("/agents/{}/follow", name), &json!({}))
221        .await?;
222    if !crate::cli::verification::handle_verification(&result, "follow action")
223        && result["success"].as_bool().unwrap_or(false)
224    {
225        display::success(&format!("Now following {}", name));
226    } else if !result["success"].as_bool().unwrap_or(false) {
227        let error = result["error"].as_str().unwrap_or("Unknown error");
228        display::error(&format!("Failed to follow {}: {}", name, error));
229    }
230    Ok(())
231}
232
233pub async fn unfollow(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
234    let result: serde_json::Value = client.delete(&format!("/agents/{}/follow", name)).await?;
235    if !crate::cli::verification::handle_verification(&result, "unfollow action")
236        && result["success"].as_bool().unwrap_or(false)
237    {
238        display::success(&format!("Unfollowed {}", name));
239    } else if !result["success"].as_bool().unwrap_or(false) {
240        let error = result["error"].as_str().unwrap_or("Unknown error");
241        display::error(&format!("Failed to unfollow {}: {}", name, error));
242    }
243    Ok(())
244}
245
246pub async fn setup_owner_email(client: &MoltbookClient, email: &str) -> Result<(), ApiError> {
247    let body = json!({ "email": email });
248    let result: serde_json::Value = client.post("/agents/me/setup-owner-email", &body).await?;
249    if !crate::cli::verification::handle_verification(&result, "email setup")
250        && result["success"].as_bool().unwrap_or(false)
251    {
252        display::success("Owner email set! Check your inbox to verify dashboard access.");
253    }
254    Ok(())
255}
256
257pub async fn verify(client: &MoltbookClient, code: &str, solution: &str) -> Result<(), ApiError> {
258    let body = json!({
259        "verification_code": code,
260        "answer": solution
261    });
262    let result = client.post::<serde_json::Value>("/verify", &body).await;
263
264    match result {
265        Ok(res) => {
266            if res["success"].as_bool().unwrap_or(false) {
267                display::success("Verification Successful!");
268
269                if let Some(post) = res.get("post") {
270                    if let Ok(p) = serde_json::from_value::<crate::api::types::Post>(post.clone()) {
271                        display::display_post(&p, None);
272                    }
273                } else if let Some(comment) = res.get("comment") {
274                    display::display_comment(comment, 0);
275                } else if let Some(agent) = res.get("agent")
276                    && let Ok(a) = serde_json::from_value::<crate::api::types::Agent>(agent.clone())
277                {
278                    display::display_profile(&a, Some("Verified Agent Profile"));
279                }
280
281                if let Some(id) = res["id"].as_str() {
282                    println!("{} {}", "ID:".bright_white().bold(), id.dimmed());
283                }
284
285                if let Some(msg) = res["message"].as_str() {
286                    display::info(msg);
287                }
288
289                if let Some(suggestion) = res["suggestion"].as_str() {
290                    println!("💡 {}", suggestion.dimmed());
291                }
292            } else {
293                let error = res["error"].as_str().unwrap_or("Unknown error");
294                display::error(&format!("Verification Failed: {}", error));
295            }
296        }
297        Err(ApiError::MoltbookError(msg, _hint)) if msg == "Already answered" => {
298            display::info("Already Verified");
299            println!("{}", "This challenge has already been completed.".blue());
300        }
301        Err(e) => {
302            display::error(&format!("Verification Failed: {}", e));
303        }
304    }
305    Ok(())
306}