Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ
| Group | Key commands |
|-------|-------------|
| `buzz agents` | `draft-create`, `draft-update` |
| `buzz voice` | `join`, `remove`, `mute`, `unmute`, `set-voice` |
| `buzz messages` | `send`, `get`, `thread`, `search` |
| `buzz channels` | `list`, `get`, `create`, `join`, `members` |
| `buzz canvas` | `get`, `set` |
Expand All @@ -22,6 +23,8 @@ The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ

Run `buzz --help` or `buzz <group> --help` for full usage. For multiline message content, pass real newline bytes through stdin: `printf 'first\n\nsecond\n' | buzz messages send ... --content -`. Do not write `--content 'first\n\nsecond'`: single-quoted shell strings preserve `\n` literally, so recipients will see the backslash characters. `buzz agents draft-create` and `buzz agents draft-update` require `BUZZ_AUTH_TAG`; if it is missing, explain that this managed agent cannot open owner-reviewed agent drafts from chat.

Voice-room controls also require `BUZZ_AUTH_TAG`. Use `buzz voice join --agent-name <name>` to add an existing personal agent without UI automation; run `buzz voice --help` for removal, muting, output, and voice-selection controls. These commands control the owner's currently open Voice room and do not create agents.

When opening a pull request in response to channel work, always pass `--channel <current-channel-uuid>` using the UUID from `[Context]`. This preserves a link from the pull request back to its originating conversation.

## Conversational Agent Creation
Expand Down
14 changes: 8 additions & 6 deletions crates/buzz-cli/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli
display_name,
system_prompt,
} => {
let owner = require_owner(client)?;
let owner = require_owner(client, "agent draft requests")?;
let built = build_create(
client.keys(),
&owner,
Expand Down Expand Up @@ -53,7 +53,7 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli
model,
respond_to,
} => {
let owner = require_owner(client)?;
let owner = require_owner(client, "agent draft requests")?;
let built = build_update(
client.keys(),
&owner,
Expand Down Expand Up @@ -151,12 +151,14 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli
}
}

/// Require `BUZZ_AUTH_TAG` and parse the owner pubkey from it. Used only by
/// the `draft-create` and `draft-update` paths.
fn require_owner(client: &BuzzClient) -> Result<PublicKey, CliError> {
/// Require `BUZZ_AUTH_TAG` and parse the owner pubkey from it.
pub(crate) fn require_owner(
client: &BuzzClient,
request_kind: &str,
) -> Result<PublicKey, CliError> {
let hex = client
.auth_tag_owner_hex()
.ok_or_else(|| CliError::Auth("agent draft requests require BUZZ_AUTH_TAG".into()))?;
.ok_or_else(|| CliError::Auth(format!("{request_kind} require BUZZ_AUTH_TAG")))?;
PublicKey::parse(&hex).map_err(|e| CliError::Auth(format!("invalid owner attestation: {e}")))
}

Expand Down
1 change: 1 addition & 0 deletions crates/buzz-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod repos;
pub mod social;
pub mod upload;
pub mod users;
pub mod voice;
pub mod workflows;

use crate::{client::normalize_write_response, error::CliError};
Expand Down
64 changes: 64 additions & 0 deletions crates/buzz-cli/src/commands/voice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use serde_json::json;

use crate::commands::agents::require_owner;
use crate::error::CliError;
use crate::voice_management::{build, VoiceAgentRef, VoiceRoomCommand};
use crate::{client::BuzzClient, VoiceCmd};

pub async fn dispatch(command: VoiceCmd, client: &BuzzClient) -> Result<(), CliError> {
let (action, command) = match command {
VoiceCmd::Join(agent) => (
"join",
VoiceRoomCommand::Join(VoiceAgentRef::try_from(agent)?),
),
VoiceCmd::Remove(agent) => (
"remove",
VoiceRoomCommand::Remove(VoiceAgentRef::try_from(agent)?),
),
VoiceCmd::Mute(agent) => (
"set-muted",
VoiceRoomCommand::SetMuted {
agent: VoiceAgentRef::try_from(agent)?,
muted: true,
},
),
VoiceCmd::Unmute(agent) => (
"set-muted",
VoiceRoomCommand::SetMuted {
agent: VoiceAgentRef::try_from(agent)?,
muted: false,
},
),
VoiceCmd::SetVoice { agent, voice } => (
"set-voice",
VoiceRoomCommand::SetVoice {
agent: VoiceAgentRef::try_from(agent)?,
voice: voice.as_str().to_owned(),
},
),
VoiceCmd::MuteOutput => (
"set-output-muted",
VoiceRoomCommand::SetOutputMuted { muted: true },
),
VoiceCmd::UnmuteOutput => (
"set-output-muted",
VoiceRoomCommand::SetOutputMuted { muted: false },
),
};
let owner = require_owner(client, "voice-room commands")?;
let built = build(client.keys(), &owner, command)?;
let response = client.publish_ephemeral_event(built.event).await?;
let relay: serde_json::Value = serde_json::from_str(&response)
.map_err(|error| CliError::Other(format!("invalid relay response: {error}")))?;
println!(
"{}",
json!({
"accepted": relay["accepted"],
"event_id": relay["event_id"],
"request_id": built.request_id,
"action": action,
"message": "Voice-room command sent to Buzz Desktop.",
})
);
Ok(())
}
83 changes: 83 additions & 0 deletions crates/buzz-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod client;
mod commands;
mod error;
mod validate;
pub mod voice_management;

use clap::{Parser, Subcommand};
use client::BuzzClient;
Expand Down Expand Up @@ -176,6 +177,9 @@ enum Cmd {
/// Draft owner-reviewed agent creation and updates
#[command(subcommand)]
Agents(AgentsCmd),
/// Control agents participating in the owner's active voice room
#[command(subcommand)]
Voice(VoiceCmd),
/// Send, read, search, and manage messages
#[command(subcommand)]
Messages(MessagesCmd),
Expand Down Expand Up @@ -241,6 +245,71 @@ enum Cmd {
Moderation(ModerationCmd),
}

#[derive(Subcommand)]
pub enum VoiceCmd {
/// Add an agent to the active voice room
Join(VoiceAgentArgs),
/// Remove an agent from the active voice room
Remove(VoiceAgentArgs),
/// Mute an agent's microphone in the active voice room
Mute(VoiceAgentArgs),
/// Unmute an agent's microphone in the active voice room
Unmute(VoiceAgentArgs),
/// Select an agent's synthesized voice
SetVoice {
#[command(flatten)]
agent: VoiceAgentArgs,
#[arg(long)]
voice: VoiceName,
},
/// Mute all synthesized voice output
MuteOutput,
/// Unmute all synthesized voice output
UnmuteOutput,
}

#[derive(Clone, Copy, clap::ValueEnum)]
pub enum VoiceName {
Sol,
Cove,
Ember,
Breeze,
Arbor,
Vale,
Juniper,
Maple,
Spruce,
}

impl VoiceName {
fn as_str(self) -> &'static str {
match self {
Self::Sol => "sol",
Self::Cove => "cove",
Self::Ember => "ember",
Self::Breeze => "breeze",
Self::Arbor => "arbor",
Self::Vale => "vale",
Self::Juniper => "juniper",
Self::Maple => "maple",
Self::Spruce => "spruce",
}
}
}

#[derive(clap::Args, Debug, Clone)]
pub struct VoiceAgentArgs {
/// Exact display name of the agent
#[arg(long)]
agent_name: Option<String>,
/// Agent public key (hex)
#[arg(long)]
agent_pubkey: Option<String>,
/// Existing agent session thread UUID
#[arg(long)]
thread_id: Option<String>,
}

#[derive(Clone, Copy, clap::ValueEnum)]
pub enum RespondToArg {
#[value(name = "owner-only")]
Expand Down Expand Up @@ -1972,6 +2041,7 @@ async fn run(cli: Cli) -> Result<(), CliError> {

match cli.command {
Cmd::Agents(sub) => commands::agents::dispatch(sub, &client).await,
Cmd::Voice(sub) => commands::voice::dispatch(sub, &client).await,
Cmd::Messages(sub) => commands::messages::dispatch(sub, &client, &cli.format).await,
Cmd::Channels(sub) => commands::channels::dispatch(sub, &client, &cli.format).await,
Cmd::Canvas(sub) => commands::channels::dispatch_canvas(sub, &client).await,
Expand Down Expand Up @@ -2100,6 +2170,7 @@ mod tests {
"social",
"upload",
"users",
"voice",
"workflows",
];

Expand Down Expand Up @@ -2187,6 +2258,18 @@ mod tests {
]
);
assert_eq!(names(&cmd, "canvas"), vec!["get", "set"]);
assert_eq!(
names(&cmd, "voice"),
vec![
"join",
"mute",
"mute-output",
"remove",
"set-voice",
"unmute",
"unmute-output"
]
);
assert_eq!(names(&cmd, "reactions"), vec!["add", "get", "remove"]);
assert_eq!(
names(&cmd, "emoji"),
Expand Down
Loading