use agent_client_protocol::schema::{
ContentBlock, InitializeRequest, NewSessionRequest, PromptRequest, ProtocolVersion,
RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse,
SelectedPermissionOutcome, SessionNotification, TextContent,
};
use agent_client_protocol::{Agent, Client, ConnectionTo};
use clap::Parser;
use std::path::PathBuf;
use tokio::process::Child;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
#[derive(Parser)]
#[command(name = "yolo-one-shot-client")]
#[command(about = "A simple ACP client for one-shot prompts", long_about = None)]
struct Cli {
#[arg(short, long)]
command: String,
prompt: String,
}
fn parse_command_string(s: &str) -> Result<(PathBuf, Vec<String>), Box<dyn std::error::Error>> {
let parts = shell_words::split(s)?;
if parts.is_empty() {
return Err("Command string cannot be empty".into());
}
let command = PathBuf::from(&parts[0]);
let args = parts[1..].to_vec();
Ok((command, args))
}
fn spawn_agent_process(
command: PathBuf,
args: Vec<String>,
) -> Result<
(
tokio::process::ChildStdin,
tokio::process::ChildStdout,
Child,
),
Box<dyn std::error::Error>,
> {
let mut cmd = tokio::process::Command::new(&command);
cmd.args(&args);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
let child_stdin = child.stdin.take().ok_or("Failed to open stdin")?;
let child_stdout = child.stdout.take().ok_or("Failed to open stdout")?;
Ok((child_stdin, child_stdout, child))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let (command, args) = parse_command_string(&cli.command)?;
eprintln!("🚀 Spawning agent: {} {:?}", command.display(), args);
let (child_stdin, child_stdout, mut child) = spawn_agent_process(command, args)?;
let transport =
agent_client_protocol::ByteStreams::new(child_stdin.compat_write(), child_stdout.compat());
Client
.builder()
.on_receive_notification(
async move |notification: SessionNotification, _cx| {
println!("{:?}", notification.update);
Ok(())
},
agent_client_protocol::on_receive_notification!(),
)
.on_receive_request(
async move |request: RequestPermissionRequest, responder, _connection| {
eprintln!("✅ Auto-approving permission request: {request:?}");
let option_id = request.options.first().map(|opt| opt.option_id.clone());
if let Some(id) = option_id {
responder.respond(RequestPermissionResponse::new(
RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(id)),
))
} else {
eprintln!("⚠️ No options provided in permission request, cancelling");
responder.respond(RequestPermissionResponse::new(
RequestPermissionOutcome::Cancelled,
))
}
},
agent_client_protocol::on_receive_request!(),
)
.connect_with(transport, |connection: ConnectionTo<Agent>| async move {
eprintln!("🤝 Initializing agent...");
let init_response = connection
.send_request(InitializeRequest::new(ProtocolVersion::V1))
.block_task()
.await?;
eprintln!("✓ Agent initialized: {:?}", init_response.agent_info);
eprintln!("📝 Creating new session...");
let new_session_response = connection
.send_request(NewSessionRequest::new(
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
))
.block_task()
.await?;
let session_id = new_session_response.session_id;
eprintln!("✓ Session created");
eprintln!("💬 Sending prompt: \"{}\"", cli.prompt);
let prompt_response = connection
.send_request(PromptRequest::new(
session_id.clone(),
vec![ContentBlock::Text(TextContent::new(cli.prompt.clone()))],
))
.block_task()
.await?;
eprintln!("✅ Agent completed!");
eprintln!("Stop reason: {:?}", prompt_response.stop_reason);
Ok(())
})
.await?;
drop(child.kill().await);
Ok(())
}