11//! YOLO one-shot client: A simple ACP client that runs a single prompt against an agent.
22//!
3- //! This client:
4- //! - Takes a prompt and agent configuration as arguments
5- //! - Spawns the agent
6- //! - Sends the prompt
7- //! - Auto-approves all permission requests
8- //! - Prints all session updates to stdout
9- //! - Runs until the agent completes
3+ //! This is a simplified example showing basic ACP client usage. It only supports
4+ //! simple command strings (not JSON configs or environment variables).
105//!
11- //! # Usage
6+ //! For a more full-featured client with JSON config support, see the `yopo` binary crate.
127//!
13- //! With a command:
14- //! ```bash
15- //! cargo run --example yolo_one_shot_client -- "What is 2+2?" "python my_agent.py"
16- //! ```
8+ //! # Usage
179//!
18- //! With JSON config:
1910//! ```bash
20- //! cargo run --example yolo_one_shot_client -- "Hello!" '{"type":"stdio","name":"my-agent"," command":" python","args":["agent .py"],"env":[]}'
11+ //! cargo run --example yolo_one_shot_client -- -- command " python my_agent .py" "What is 2+2?"
2112//! ```
2213
14+ use clap:: Parser ;
2315use sacp:: JrConnection ;
2416use sacp:: schema:: {
25- ContentBlock , InitializeRequest , McpServer , NewSessionRequest , PromptRequest ,
26- RequestPermissionOutcome , RequestPermissionRequest , RequestPermissionResponse ,
27- SessionNotification , TextContent , VERSION as PROTOCOL_VERSION ,
17+ ContentBlock , InitializeRequest , NewSessionRequest , PromptRequest , RequestPermissionOutcome ,
18+ RequestPermissionRequest , RequestPermissionResponse , SessionNotification , TextContent ,
19+ VERSION as PROTOCOL_VERSION ,
2820} ;
2921use std:: path:: PathBuf ;
3022use tokio:: process:: Child ;
3123use tokio_util:: compat:: { TokioAsyncReadCompatExt , TokioAsyncWriteCompatExt } ;
3224
33- // NOTE: This code is inlined here to avoid a circular dependency between `sacp` and `sacp-tokio`.
34- // If you're writing your own client, you can use `sacp_tokio::AcpAgent` and `JrConnectionExt::to_agent()`
35- // to simplify this setup significantly.
36-
37- /// Parse agent configuration from either a command string or JSON.
38- fn parse_agent_config ( s : & str ) -> Result < McpServer , Box < dyn std :: error :: Error > > {
39- let trimmed = s . trim ( ) ;
25+ # [ derive ( Parser ) ]
26+ # [ command ( name = "yolo-one-shot- client" ) ]
27+ # [ command ( about = "A simple ACP client for one-shot prompts" , long_about = None ) ]
28+ struct Cli {
29+ /// The command to run the agent (e.g., "python my_agent.py")
30+ # [ arg ( short , long ) ]
31+ command : String ,
4032
41- // If it starts with '{', try to parse as JSON
42- if trimmed. starts_with ( '{' ) {
43- let server: McpServer = serde_json:: from_str ( trimmed) ?;
44- return Ok ( server) ;
45- }
33+ /// The prompt to send to the agent
34+ prompt : String ,
35+ }
4636
47- // Otherwise, parse as a command string
48- let parts = shell_words:: split ( trimmed) ?;
37+ /// Parse a command string into command and args
38+ fn parse_command_string ( s : & str ) -> Result < ( PathBuf , Vec < String > ) , Box < dyn std:: error:: Error > > {
39+ let parts = shell_words:: split ( s) ?;
4940 if parts. is_empty ( ) {
5041 return Err ( "Command string cannot be empty" . into ( ) ) ;
5142 }
52-
5343 let command = PathBuf :: from ( & parts[ 0 ] ) ;
5444 let args = parts[ 1 ..] . to_vec ( ) ;
55- let name = command
56- . file_name ( )
57- . and_then ( |n| n. to_str ( ) )
58- . unwrap_or ( "agent" )
59- . to_string ( ) ;
60-
61- Ok ( McpServer :: Stdio {
62- name,
63- command,
64- args,
65- env : vec ! [ ] ,
66- } )
45+ Ok ( ( command, args) )
6746}
6847
6948/// Spawn a process for the agent and get stdio streams.
7049fn spawn_agent_process (
71- server : & McpServer ,
50+ command : PathBuf ,
51+ args : Vec < String > ,
7252) -> Result <
7353 (
7454 tokio:: process:: ChildStdin ,
@@ -77,64 +57,31 @@ fn spawn_agent_process(
7757 ) ,
7858 Box < dyn std:: error:: Error > ,
7959> {
80- match server {
81- McpServer :: Stdio {
82- command,
83- args,
84- env,
85- name : _,
86- } => {
87- let mut cmd = tokio:: process:: Command :: new ( command) ;
88- cmd. args ( args) ;
89- for env_var in env {
90- cmd. env ( & env_var. name , & env_var. value ) ;
91- }
92- cmd. stdin ( std:: process:: Stdio :: piped ( ) )
93- . stdout ( std:: process:: Stdio :: piped ( ) ) ;
60+ let mut cmd = tokio:: process:: Command :: new ( & command) ;
61+ cmd. args ( & args) ;
62+ cmd. stdin ( std:: process:: Stdio :: piped ( ) )
63+ . stdout ( std:: process:: Stdio :: piped ( ) ) ;
9464
95- let mut child = cmd. spawn ( ) ?;
96- let child_stdin = child. stdin . take ( ) . ok_or ( "Failed to open stdin" ) ?;
97- let child_stdout = child. stdout . take ( ) . ok_or ( "Failed to open stdout" ) ?;
65+ let mut child = cmd. spawn ( ) ?;
66+ let child_stdin = child. stdin . take ( ) . ok_or ( "Failed to open stdin" ) ?;
67+ let child_stdout = child. stdout . take ( ) . ok_or ( "Failed to open stdout" ) ?;
9868
99- Ok ( ( child_stdin, child_stdout, child) )
100- }
101- McpServer :: Http { .. } => Err ( "HTTP transport not yet supported" . into ( ) ) ,
102- McpServer :: Sse { .. } => Err ( "SSE transport not yet supported" . into ( ) ) ,
103- }
69+ Ok ( ( child_stdin, child_stdout, child) )
10470}
10571
10672#[ tokio:: main]
10773async fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
108- // Parse command line arguments
109- let args: Vec < String > = std:: env:: args ( ) . collect ( ) ;
110- if args. len ( ) != 3 {
111- eprintln ! ( "Usage: {} <prompt> <agent-config>" , args[ 0 ] ) ;
112- eprintln ! ( ) ;
113- eprintln ! ( " <prompt> - The prompt to send to the agent" ) ;
114- eprintln ! ( " <agent-config> - Either a command string or JSON (starting with '{{')" ) ;
115- eprintln ! ( ) ;
116- eprintln ! ( "Examples:" ) ;
117- eprintln ! ( " {} \" What is 2+2?\" \" python my_agent.py\" " , args[ 0 ] ) ;
118- eprintln ! (
119- " {} \" Hello!\" '{{\" type\" :\" stdio\" ,\" name\" :\" agent\" ,\" command\" :\" python\" ,\" args\" :[\" agent.py\" ],\" env\" :[]}}'" ,
120- args[ 0 ]
121- ) ;
122- std:: process:: exit ( 1 ) ;
123- }
124-
125- let prompt = & args[ 1 ] ;
126- let agent_config = & args[ 2 ] ;
74+ let cli = Cli :: parse ( ) ;
12775
128- // Parse the agent configuration
129- let server = parse_agent_config ( agent_config ) ?;
76+ // Parse the command string
77+ let ( command , args ) = parse_command_string ( & cli . command ) ?;
13078
131- eprintln ! ( "🚀 Spawning agent and connecting..." ) ;
79+ eprintln ! ( "🚀 Spawning agent: {} {:?}" , command . display ( ) , args ) ;
13280
13381 // Spawn the agent process
134- let ( child_stdin, child_stdout, mut child) = spawn_agent_process ( & server ) ?;
82+ let ( child_stdin, child_stdout, mut child) = spawn_agent_process ( command , args ) ?;
13583
13684 // Create a JrConnection with the agent's stdio streams
137- // NOTE: Using sacp_tokio::JrConnectionExt::to_agent() would simplify this setup
13885 let connection = JrConnection :: new ( child_stdin. compat_write ( ) , child_stdout. compat ( ) ) ;
13986
14087 // Run the client
@@ -192,12 +139,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
192139 eprintln ! ( "✓ Session created: {}" , session_id) ;
193140
194141 // Send the prompt
195- eprintln ! ( "💬 Sending prompt: \" {}\" " , prompt) ;
142+ eprintln ! ( "💬 Sending prompt: \" {}\" " , cli . prompt) ;
196143 let prompt_response = cx
197144 . send_request ( PromptRequest {
198145 session_id : session_id. clone ( ) ,
199146 prompt : vec ! [ ContentBlock :: Text ( TextContent {
200- text: prompt. to_string ( ) ,
147+ text: cli . prompt. clone ( ) ,
201148 annotations: None ,
202149 meta: None ,
203150 } ) ] ,
0 commit comments