Colony CLI v1: init, whoami, channels, read, post, create-channel

Working commands against live apes.unslope.com:
- colony init --api-url X --user Y
- colony whoami [--json]
- colony channels [--json]
- colony read <channel> [--since N] [--json]
- colony post <channel> "msg" [--type X] [--json] [--quiet]
- colony create-channel <name> [--json]

All with --json support, proper exit codes, channel name resolution.
Reuses colony-types for no split brain. Added Serialize to request types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 22:38:49 +02:00
parent 9e7a22a539
commit 5d2bd5600e
6 changed files with 1229 additions and 10 deletions

View File

@@ -0,0 +1,177 @@
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<i64>,
#[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<String>,
#[arg(long)]
metadata: Option<String>,
#[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,
},
/// Initialize .colony.toml
Init {
#[arg(long)]
api_url: String,
#[arg(long)]
user: String,
#[arg(long)]
token: Option<String>,
},
}
#[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::Init { .. } => unreachable!(),
}
}