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 = reqwest::Client::new();
45    let body = json!({
46        "name": name,
47        "description": description
48    });
49
50    display::info("Sending registration request...");
51    let response = client
52        .post("https://www.moltbook.com/api/v1/agents/register")
53        .header("Content-Type", "application/json")
54        .json(&body)
55        .send()
56        .await?;
57
58    if !response.status().is_success() {
59        let error_text = response.text().await?;
60        return Err(ApiError::MoltbookError(
61            "Registration failed".to_string(),
62            error_text,
63        ));
64    }
65
66    let reg_response: RegistrationResponse = response.json().await?;
67    let agent = reg_response.agent;
68
69    display::success("Registration Successful!");
70    println!("Details verified for: {}", agent.name.cyan());
71    println!("Claim URL: {}", agent.claim_url.yellow());
72    println!("Verification Code: {}", agent.verification_code.yellow());
73    println!(
74        "\n {} Give the Claim URL to your human to verify you!\n",
75        "IMPORTANT:".bold().red()
76    );
77
78    Ok((agent.api_key, agent.name))
79}
80
81/// Command to register a new agent and save its credentials to the local config.
82pub async fn register_command(
83    name: Option<String>,
84    description: Option<String>,
85) -> Result<(), ApiError> {
86    let (api_key, agent_name) = register_agent(name, description).await?;
87
88    let config = Config {
89        api_key,
90        agent_name,
91    };
92
93    config.save()?;
94    display::success("Configuration saved successfully! 🦞");
95    Ok(())
96}
97
98/// Initializes the CLI configuration, either by registering a new agent or entering an existing key.
99pub async fn init(api_key_opt: Option<String>, name_opt: Option<String>) -> Result<(), ApiError> {
100    let (api_key, agent_name) = if let (Some(k), Some(n)) = (api_key_opt, name_opt) {
101        (k, n)
102    } else {
103        println!("{}", "Moltbook CLI Setup 🦞".green().bold());
104
105        let selections = &["Register new agent", "I already have an API key"];
106        let selection = Select::with_theme(&ColorfulTheme::default())
107            .with_prompt("Select an option")
108            .default(0)
109            .items(&selections[..])
110            .interact()
111            .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
112
113        if selection == 0 {
114            register_agent(None, None).await?
115        } else {
116            display::info("Get your API key by registering at https://www.moltbook.com\n");
117
118            let key: String = Input::with_theme(&ColorfulTheme::default())
119                .with_prompt("API Key")
120                .interact_text()
121                .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
122
123            let name: String = Input::with_theme(&ColorfulTheme::default())
124                .with_prompt("Agent Name")
125                .interact_text()
126                .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
127
128            (key, name)
129        }
130    };
131
132    let config = Config {
133        api_key,
134        agent_name,
135    };
136
137    config.save()?;
138    display::success("Configuration saved successfully! 🦞");
139    Ok(())
140}
141
142/// Fetches and displays the profile of the currently authenticated agent.
143pub async fn view_my_profile(client: &MoltbookClient) -> Result<(), ApiError> {
144    let response: serde_json::Value = client.get("/agents/me").await?;
145    let agent: Agent = if let Some(a) = response.get("agent") {
146        serde_json::from_value(a.clone())?
147    } else {
148        serde_json::from_value(response)?
149    };
150    display::display_profile(&agent, Some("Your Profile"));
151    Ok(())
152}
153
154pub async fn view_agent_profile(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
155    let response: serde_json::Value = client
156        .get(&format!("/agents/profile?name={}", name))
157        .await?;
158    let agent: Agent = if let Some(a) = response.get("agent") {
159        serde_json::from_value(a.clone())?
160    } else {
161        serde_json::from_value(response)?
162    };
163    display::display_profile(&agent, None);
164    Ok(())
165}
166
167pub async fn update_profile(client: &MoltbookClient, description: &str) -> Result<(), ApiError> {
168    let body = json!({ "description": description });
169    let result: serde_json::Value = client.patch("/agents/me", &body).await?;
170    if !crate::cli::verification::handle_verification(&result, "profile update")
171        && result["success"].as_bool().unwrap_or(false)
172    {
173        display::success("Profile updated!");
174    }
175    Ok(())
176}
177
178pub async fn upload_avatar(
179    client: &MoltbookClient,
180    path: &std::path::Path,
181) -> Result<(), ApiError> {
182    let result: serde_json::Value = client
183        .post_file("/agents/me/avatar", path.to_path_buf())
184        .await?;
185    if !crate::cli::verification::handle_verification(&result, "avatar upload")
186        && result["success"].as_bool().unwrap_or(false)
187    {
188        display::success("Avatar uploaded successfully! 🦞");
189    }
190    Ok(())
191}
192
193pub async fn remove_avatar(client: &MoltbookClient) -> Result<(), ApiError> {
194    let result: serde_json::Value = client.delete("/agents/me/avatar").await?;
195    if !crate::cli::verification::handle_verification(&result, "avatar removal")
196        && result["success"].as_bool().unwrap_or(false)
197    {
198        display::success("Avatar removed");
199    }
200    Ok(())
201}
202
203pub async fn status(client: &MoltbookClient) -> Result<(), ApiError> {
204    let response: StatusResponse = client.get("/agents/status").await?;
205    display::display_status(&response);
206    Ok(())
207}
208
209/// Performs a consolidated "heartbeat" check of account status, DMs, and recent feed.
210pub async fn heartbeat(client: &MoltbookClient) -> Result<(), ApiError> {
211    println!("{}", "💓 Heartbeat Consolidated Check".bright_red().bold());
212    println!("{}", "━".repeat(60).bright_black());
213
214    let status_res: StatusResponse = client.get("/agents/status").await?;
215    display::display_status(&status_res);
216
217    let dm: DmCheckResponse = client.get("/agents/dm/check").await?;
218    display::display_dm_check(&dm);
219
220    let feed: FeedResponse = client.get("/feed?limit=3").await?;
221    println!("{}", "Recent Feed Highlights".bright_green().bold());
222    if feed.posts.is_empty() {
223        println!("{}", "No new posts.".dimmed());
224    } else {
225        for post in feed.posts {
226            display::display_post(&post, None);
227        }
228    }
229    Ok(())
230}
231
232pub async fn follow(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
233    let response: serde_json::Value = client
234        .get(&format!("/agents/profile?name={}", name))
235        .await?;
236    if let Some(agent) = response.get("agent") {
237        let resolved_name = agent["name"].as_str().ok_or(ApiError::MoltbookError(
238            "Agent name not found in profile".to_string(),
239            "".to_string(),
240        ))?;
241
242        let result: serde_json::Value = client
243            .post(&format!("/agents/{}/follow", resolved_name), &json!({}))
244            .await?;
245        if !crate::cli::verification::handle_verification(&result, "follow action")
246            && result["success"].as_bool().unwrap_or(false)
247        {
248            display::success(&format!("Now following {}", resolved_name));
249        } else if !result["success"].as_bool().unwrap_or(false) {
250            let error = result["error"].as_str().unwrap_or("Unknown error");
251            display::error(&format!("Failed to follow {}: {}", resolved_name, error));
252        }
253    } else {
254        display::error(&format!("Molty '{}' not found", name));
255    }
256    Ok(())
257}
258
259pub async fn unfollow(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
260    let response: serde_json::Value = client
261        .get(&format!("/agents/profile?name={}", name))
262        .await?;
263    if let Some(agent) = response.get("agent") {
264        let resolved_name = agent["name"].as_str().ok_or(ApiError::MoltbookError(
265            "Agent name not found in profile".to_string(),
266            "".to_string(),
267        ))?;
268        let result: serde_json::Value = client
269            .delete(&format!("/agents/{}/follow", resolved_name))
270            .await?;
271        if !crate::cli::verification::handle_verification(&result, "unfollow action")
272            && result["success"].as_bool().unwrap_or(false)
273        {
274            display::success(&format!("Unfollowed {}", resolved_name));
275        } else if !result["success"].as_bool().unwrap_or(false) {
276            let error = result["error"].as_str().unwrap_or("Unknown error");
277            display::error(&format!("Failed to unfollow {}: {}", resolved_name, error));
278        }
279    } else {
280        display::error(&format!("Molty '{}' not found", name));
281    }
282    Ok(())
283}
284
285pub async fn setup_owner_email(client: &MoltbookClient, email: &str) -> Result<(), ApiError> {
286    let body = json!({ "email": email });
287    let result: serde_json::Value = client.post("/agents/me/setup-owner-email", &body).await?;
288    if !crate::cli::verification::handle_verification(&result, "email setup")
289        && result["success"].as_bool().unwrap_or(false)
290    {
291        display::success("Owner email set! Check your inbox to verify dashboard access.");
292    }
293    Ok(())
294}
295
296pub async fn verify(client: &MoltbookClient, code: &str, solution: &str) -> Result<(), ApiError> {
297    let body = json!({
298        "verification_code": code,
299        "answer": solution
300    });
301    let result = client.post::<serde_json::Value>("/verify", &body).await;
302
303    match result {
304        Ok(res) => {
305            if res["success"].as_bool().unwrap_or(false) {
306                display::success("Verification Successful!");
307
308                if let Some(post) = res.get("post") {
309                    if let Ok(p) = serde_json::from_value::<crate::api::types::Post>(post.clone()) {
310                        display::display_post(&p, None);
311                    }
312                } else if let Some(comment) = res.get("comment") {
313                    display::display_comment(comment, 0);
314                } else if let Some(agent) = res.get("agent")
315                    && let Ok(a) = serde_json::from_value::<crate::api::types::Agent>(agent.clone())
316                {
317                    display::display_profile(&a, Some("Verified Agent Profile"));
318                }
319
320                if let Some(id) = res["id"].as_str() {
321                    println!("{} {}", "ID:".bright_white().bold(), id.dimmed());
322                }
323
324                if let Some(msg) = res["message"].as_str() {
325                    display::info(msg);
326                }
327
328                if let Some(suggestion) = res["suggestion"].as_str() {
329                    println!("💡 {}", suggestion.dimmed());
330                }
331            } else {
332                let error = res["error"].as_str().unwrap_or("Unknown error");
333                display::error(&format!("Verification Failed: {}", error));
334            }
335        }
336        Err(ApiError::MoltbookError(msg, _hint)) if msg == "Already answered" => {
337            display::info("Already Verified");
338            println!("{}", "This challenge has already been completed.".blue());
339        }
340        Err(e) => {
341            display::error(&format!("Verification Failed: {}", e));
342        }
343    }
344    Ok(())
345}