mod client; mod config; use clap::{Parser, Subcommand}; use client::ColonyClient; use colony_types::*; use config::Config; #[derive(Parser)] #[command(name = "colony", about = "Ape Colony CLI — chat from the terminal")] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Show current identity Whoami { #[arg(long)] json: bool, }, /// List channels Channels { #[arg(long)] json: bool, }, /// Read messages from a channel Read { channel: String, #[arg(long)] since: Option, #[arg(long)] json: bool, }, /// Post a message to a channel Post { channel: String, message: String, #[arg(long, default_value = "text")] r#type: String, #[arg(long)] reply_to: Option, #[arg(long)] metadata: Option, #[arg(long)] json: bool, #[arg(long)] quiet: bool, }, /// Create a new channel CreateChannel { name: String, #[arg(long, default_value = "")] description: String, #[arg(long)] json: bool, }, /// Check inbox (unacked mentions + activity) Inbox { #[arg(long)] json: bool, }, /// Acknowledge inbox items Ack { /// Inbox item IDs to ack ids: Vec, /// Ack all unacked items #[arg(long)] all: bool, #[arg(long)] quiet: bool, }, /// Initialize .colony.toml Init { #[arg(long)] api_url: String, #[arg(long)] user: String, #[arg(long)] token: Option, }, } #[tokio::main] async fn main() { let cli = Cli::parse(); match cli.command { Commands::Init { api_url, user, token } => { let config = Config { api_url, user, token, password: None, }; let toml_str = toml::to_string_pretty(&config).unwrap(); std::fs::write(".colony.toml", &toml_str).unwrap(); eprintln!("wrote .colony.toml"); } _ => { let config = match Config::load() { Ok(c) => c, Err(e) => { eprintln!("{}", e); std::process::exit(1); } }; let client = ColonyClient::new(config); run_command(cli.command, &client).await; } } } async fn run_command(cmd: Commands, client: &ColonyClient) { match cmd { Commands::Whoami { json } => { let me = client.get_me().await; if json { println!("{}", serde_json::to_string(&me).unwrap()); } else { println!("{} ({}) — {}", me.username, match me.role { UserRole::Ape => "ape", UserRole::Agent => "agent", }, client.config.api_url); } } Commands::Channels { json } => { let channels = client.get_channels().await; if json { println!("{}", serde_json::to_string(&channels).unwrap()); } else { for ch in &channels { println!("#{:<16} {}", ch.name, ch.description); } } } Commands::Read { channel, since, json } => { let channel_id = client.resolve_channel(&channel).await; let messages = client.get_messages(&channel_id, since).await; if json { println!("{}", serde_json::to_string(&messages).unwrap()); } else { for msg in &messages { println!("[{}] {}: {}", msg.seq, msg.user.username, msg.content); } } } Commands::Post { channel, message, r#type, reply_to, metadata, json, quiet } => { let channel_id = client.resolve_channel(&channel).await; let msg_type = match r#type.as_str() { "code" => MessageType::Code, "result" => MessageType::Result, "error" => MessageType::Error, "plan" => MessageType::Plan, _ => MessageType::Text, }; let meta_value = metadata.and_then(|m| serde_json::from_str(&m).ok()); let reply = reply_to.and_then(|r| uuid::Uuid::parse_str(&r).ok()); let body = PostMessage { content: message, r#type: msg_type, metadata: meta_value, reply_to: reply, }; let msg = client.post_message(&channel_id, &body).await; if json { println!("{}", serde_json::to_string(&msg).unwrap()); } else if !quiet { println!("posted message #{} to #{}", msg.seq, channel); } } Commands::CreateChannel { name, description, json } => { let body = CreateChannel { name: name.clone(), description }; let ch = client.create_channel(&body).await; if json { println!("{}", serde_json::to_string(&ch).unwrap()); } else { println!("created #{}", name); } } Commands::Inbox { json } => { let items = client.get_inbox().await; if json { println!("{}", serde_json::to_string(&items).unwrap()); } else if items.is_empty() { println!("inbox empty"); } else { for item in &items { println!("[{}] #{} [{}] {}: {} ({})", item.id, item.channel_name, item.message.seq, item.message.user.username, item.message.content, item.trigger, ); } } } Commands::Ack { ids, all, quiet } => { let to_ack = if all { let items = client.get_inbox().await; items.iter().map(|i| i.id).collect::>() } else { ids }; if to_ack.is_empty() { if !quiet { println!("nothing to ack"); } } else { let result = client.ack_inbox(&to_ack).await; if !quiet { println!("acked {} items", result["acked"]); } } } Commands::Init { .. } => unreachable!(), } }