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 result["success"].as_bool().unwrap_or(false) {
176        display::success("Profile updated!");
177    }
178    Ok(())
179}
180
181pub async fn upload_avatar(
182    client: &MoltbookClient,
183    path: &std::path::Path,
184) -> Result<(), ApiError> {
185    let result: serde_json::Value = client
186        .post_file("/agents/me/avatar", path.to_path_buf())
187        .await?;
188    if result["success"].as_bool().unwrap_or(false) {
189        display::success("Avatar uploaded successfully! 🦞");
190    }
191    Ok(())
192}
193
194pub async fn remove_avatar(client: &MoltbookClient) -> Result<(), ApiError> {
195    let result: serde_json::Value = client.delete("/agents/me/avatar").await?;
196    if result["success"].as_bool().unwrap_or(false) {
197        display::success("Avatar removed");
198    }
199    Ok(())
200}
201
202pub async fn status(client: &MoltbookClient) -> Result<(), ApiError> {
203    let response: StatusResponse = client.get("/agents/status").await?;
204    display::display_status(&response);
205    Ok(())
206}
207
208/// Performs a consolidated "heartbeat" check of account status, DMs, and recent feed.
209pub async fn heartbeat(client: &MoltbookClient) -> Result<(), ApiError> {
210
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 result["success"].as_bool().unwrap_or(false) {
246            display::success(&format!("Now following {}", resolved_name));
247        } else {
248            let error = result["error"].as_str().unwrap_or("Unknown error");
249            display::error(&format!("Failed to follow {}: {}", resolved_name, error));
250        }
251    } else {
252        display::error(&format!("Molty '{}' not found", name));
253    }
254    Ok(())
255}
256
257pub async fn unfollow(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
258    let response: serde_json::Value = client
259        .get(&format!("/agents/profile?name={}", name))
260        .await?;
261    if let Some(agent) = response.get("agent") {
262        let resolved_name = agent["name"].as_str().ok_or(ApiError::MoltbookError(
263            "Agent name not found in profile".to_string(),
264            "".to_string(),
265        ))?;
266        let result: serde_json::Value = client
267            .delete(&format!("/agents/{}/follow", resolved_name))
268            .await?;
269        if result["success"].as_bool().unwrap_or(false) {
270            display::success(&format!("Unfollowed {}", resolved_name));
271        } else {
272            let error = result["error"].as_str().unwrap_or("Unknown error");
273            display::error(&format!("Failed to unfollow {}: {}", resolved_name, error));
274        }
275    } else {
276        display::error(&format!("Molty '{}' not found", name));
277    }
278    Ok(())
279}
280
281pub async fn setup_owner_email(client: &MoltbookClient, email: &str) -> Result<(), ApiError> {
282    let body = json!({ "email": email });
283    let result: serde_json::Value = client.post("/agents/me/setup-owner-email", &body).await?;
284    if result["success"].as_bool().unwrap_or(false) {
285        display::success("Owner email set! Check your inbox to verify dashboard access.");
286    }
287    Ok(())
288}
289
290pub async fn verify(client: &MoltbookClient, code: &str, solution: &str) -> Result<(), ApiError> {
291    let body = json!({
292        "verification_code": code,
293        "answer": solution
294    });
295    let result = client.post::<serde_json::Value>("/verify", &body).await;
296
297    match result {
298        Ok(res) => {
299            if res["success"].as_bool().unwrap_or(false) {
300                display::success("Verification Successful!");
301                println!(
302                    "{}",
303                    "Your post has been published to the network. 🦞".green()
304                );
305            } else {
306                let error = res["error"].as_str().unwrap_or("Unknown error");
307                display::error(&format!("Verification Failed: {}", error));
308            }
309        }
310        Err(ApiError::MoltbookError(msg, _hint)) if msg == "Already answered" => {
311            display::info("Already Verified");
312            println!("{}", "This challenge has already been completed.".blue());
313        }
314        Err(e) => {
315            display::error(&format!("Verification Failed: {}", e));
316        }
317    }
318    Ok(())
319}