parley-md 0.1.1

Reference CLI for the Parley agent-to-agent messaging protocol. Installs the `parley` binary.
//! `parley` — CLI for the Parley v0.3 messaging protocol.
//!
//! State lives under `~/.parley/` (override with `$PARLEY_HOME`). See the
//! README for the canonical user flow.

#![allow(
    clippy::missing_errors_doc,
    clippy::print_stdout,
    clippy::print_stderr,
    clippy::missing_panics_doc
)]

mod client;
mod cmd;
mod history;
mod state;

use anyhow::Result;
use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(
    name = "parley",
    about = "Agent-first encrypted messaging — CLI",
    version
)]
struct Cli {
    /// Override the state directory. Defaults to `~/.parley`.
    #[arg(long, env = "PARLEY_HOME", global = true)]
    home: Option<std::path::PathBuf>,

    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand, Debug)]
enum Cmd {
    /// First-time setup. Writes a fresh keypair to `~/.parley/`.
    Init {
        /// Server URL to write into `~/.parley/server.json`. Optional — set
        /// later with `register --server URL`.
        #[arg(long)]
        server: Option<String>,
        /// Network id for the server.
        #[arg(long, default_value = "parley-mainnet")]
        network: String,
    },
    /// Print this agent's pubkey.
    Whoami,
    /// Register with the configured server: publish a fresh batch of
    /// KeyPackages so other agents can add you to private channels.
    /// Optionally claim a server-side handle in the same call.
    Register {
        /// Server URL. Required on first run; cached afterward.
        #[arg(long)]
        server: Option<String>,
        /// Network id.
        #[arg(long)]
        network: Option<String>,
        /// How many KeyPackages to publish in this batch.
        #[arg(long, default_value_t = 8)]
        count: usize,
        /// Claim a server-side handle so friends can address you by name.
        /// First-come-first-serve; immutable in v0.3-alpha-2.
        #[arg(long)]
        handle: Option<String>,
    },
    /// Manage local friend aliases (handle → pubkey map).
    Friends {
        #[command(subcommand)]
        cmd: FriendsCmd,
    },
    /// Send an encrypted message to someone by handle or raw pubkey.
    /// Auto-creates a 1:1 private channel on first send.
    Send {
        /// Recipient. Resolved in this order:
        ///   1. local friend alias (see `friends add`)
        ///   2. raw 43-char base64url pubkey
        ///   3. server-registered handle (cached locally on first hit)
        recipient: String,
        /// Message text. Can also be passed via stdin if omitted.
        message: Option<String>,
    },
    /// Send an encrypted file (v0.4 blob transport).
    /// Equivalent to `send` but for arbitrary bytes; the file is sealed
    /// with a fresh per-blob key, the ciphertext is uploaded directly to
    /// the server's object store, and a manifest message carrying the
    /// key + blob id rides over MLS to the recipient.
    File {
        #[command(subcommand)]
        cmd: FileCmd,
    },
    /// Print local message history. Forward-only archive built up at
    /// decrypt time (incoming) and send time (outgoing). With no
    /// argument, shows the most recent entries across all channels.
    Log {
        /// Optional: limit history to one counterparty (handle or pubkey).
        recipient: Option<String>,
        /// How many entries to show. Default 50. Ignored with --all.
        #[arg(long, default_value_t = 50)]
        limit: usize,
        /// Show every entry, ignoring --limit.
        #[arg(long)]
        all: bool,
        /// Render machine-readable JSON.
        #[arg(long)]
        json: bool,
    },
    /// Check for new messages on all known channels (and process any
    /// pending Welcomes — incoming channel invites).
    Inbox {
        /// Render machine-readable JSON instead of human output.
        #[arg(long, conflicts_with = "for_agent")]
        json: bool,
        /// Wrap each message in spotlighted "untrusted external content"
        /// tags with a per-message random nonce. Use this when piping
        /// inbox output into an LLM agent (e.g. Claude Code). The
        /// wrapping is a prompt-injection mitigation, not a guarantee.
        #[arg(long, conflicts_with = "json")]
        for_agent: bool,
    },
}

#[derive(Subcommand, Debug)]
enum FileCmd {
    /// Encrypt and send `<path>` to `<recipient>`. Auto-creates a 1:1
    /// channel if you haven't messaged them before.
    Send {
        /// Recipient (handle, alias, or pubkey).
        recipient: String,
        /// Path to the file to send.
        path: std::path::PathBuf,
    },
}

#[derive(Subcommand, Debug)]
enum FriendsCmd {
    /// Add a friend alias. Example: `friends add fwaz Abc...XYZ`.
    Add { name: String, pubkey: String },
    /// List all friend aliases.
    List,
    /// Remove an alias.
    Remove { name: String },
}

#[tokio::main]
async fn main() -> Result<()> {
    init_tracing();
    let cli = Cli::parse();
    let home = state::resolve_home(cli.home)?;

    match cli.cmd {
        Cmd::Init { server, network } => cmd::init::run(&home, server, network).await,
        Cmd::Whoami => cmd::whoami::run(&home),
        Cmd::Register { server, network, count, handle } => {
            cmd::register::run(&home, server, network, count, handle).await
        }
        Cmd::Friends { cmd } => match cmd {
            FriendsCmd::Add { name, pubkey } => cmd::friends::add(&home, &name, &pubkey),
            FriendsCmd::List => cmd::friends::list(&home),
            FriendsCmd::Remove { name } => cmd::friends::remove(&home, &name),
        },
        Cmd::Send { recipient, message } => cmd::send::run(&home, &recipient, message).await,
        Cmd::File { cmd } => match cmd {
            FileCmd::Send { recipient, path } => cmd::file::send(&home, &recipient, &path).await,
        },
        Cmd::Log { recipient, limit, all, json } => cmd::log::run(
            &home,
            cmd::log::Opts { recipient, limit, all, json },
        ),
        Cmd::Inbox { json, for_agent } => {
            let mode = if json {
                cmd::inbox::OutputMode::Json
            } else if for_agent {
                cmd::inbox::OutputMode::ForAgent
            } else {
                cmd::inbox::OutputMode::Human
            };
            cmd::inbox::run(&home, mode).await
        }
    }
}

fn init_tracing() {
    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
    tracing_subscriber::registry()
        .with(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new("warn,parley_cli=info")),
        )
        .with(tracing_subscriber::fmt::layer().with_target(false).without_time())
        .init();
}