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