steer/commands/
headless.rs

1use async_trait::async_trait;
2use eyre::{Result, eyre};
3use std::fs;
4use std::io::{self, Read};
5use std::path::PathBuf;
6use steer_core::tools::dispatch_agent::DISPATCH_AGENT_TOOL_NAME;
7use steer_core::tools::fetch::FETCH_TOOL_NAME;
8use steer_tools::tools::{
9    BASH_TOOL_NAME, EDIT_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, LS_TOOL_NAME,
10    MULTI_EDIT_TOOL_NAME, REPLACE_TOOL_NAME, TODO_READ_TOOL_NAME, TODO_WRITE_TOOL_NAME,
11    VIEW_TOOL_NAME,
12};
13
14use super::Command;
15use crate::session_config::{SessionConfigLoader, SessionConfigOverrides};
16use steer_core::api::Model;
17use steer_core::app::conversation::{Message, MessageData, UserContent};
18
19pub struct HeadlessCommand {
20    pub model: Option<Model>,
21    pub messages_json: Option<PathBuf>,
22    pub global_model: Model,
23    pub session: Option<String>,
24    pub session_config: Option<PathBuf>,
25    pub system_prompt: Option<String>,
26    pub remote: Option<String>,
27    pub directory: Option<PathBuf>,
28}
29
30#[async_trait]
31impl Command for HeadlessCommand {
32    async fn execute(&self) -> Result<()> {
33        // Parse input into Vec<Message>
34        let messages = if let Some(json_path) = &self.messages_json {
35            // Read messages from JSON file
36            let json_content = fs::read_to_string(json_path)
37                .map_err(|e| eyre!("Failed to read messages JSON file: {}", e))?;
38
39            serde_json::from_str::<Vec<Message>>(&json_content)
40                .map_err(|e| eyre!("Failed to parse messages JSON: {}", e))?
41        } else {
42            // Read prompt from stdin
43            let mut buffer = String::new();
44            match io::stdin().read_to_string(&mut buffer) {
45                Ok(_) => {
46                    if buffer.trim().is_empty() {
47                        return Err(eyre!("No input provided via stdin"));
48                    }
49                }
50                Err(e) => return Err(eyre!("Failed to read from stdin: {}", e)),
51            }
52            // Create a single user message from stdin content
53            vec![Message {
54                data: MessageData::User {
55                    content: vec![UserContent::Text { text: buffer }],
56                },
57                timestamp: Message::current_timestamp(),
58                id: Message::generate_id("user", Message::current_timestamp()),
59                parent_message_id: None,
60            }]
61        };
62
63        // Use model override if provided, otherwise use the global setting
64        let model_to_use = self.model.unwrap_or(self.global_model);
65
66        // Load session configuration if provided
67        let session_config = if let Some(config_path) = &self.session_config {
68            let overrides = SessionConfigOverrides {
69                system_prompt: self.system_prompt.clone(),
70                ..Default::default()
71            };
72
73            let loader =
74                SessionConfigLoader::new(Some(config_path.clone())).with_overrides(overrides);
75
76            Some(loader.load().await?)
77        } else {
78            None
79        };
80
81        // Extract tool config and system prompt from session config if available
82        let (tool_config, system_prompt_to_use) = match &session_config {
83            Some(config) => (
84                Some(config.tool_config.clone()),
85                config.system_prompt.clone().or(self.system_prompt.clone()),
86            ),
87            None => (None, self.system_prompt.clone()),
88        };
89
90        // Create session manager
91        let session_manager = crate::create_session_manager().await?;
92
93        // Determine execution mode and run
94        let result = match &self.session {
95            Some(session_id) => {
96                // Run in existing session
97                if messages.len() != 1 {
98                    return Err(eyre!(
99                        "When using --session, only single message input is supported (use stdin, not --messages-json)"
100                    ));
101                }
102
103                let message = match &messages[0].data {
104                    MessageData::User { content, .. } => {
105                        // Extract text from the first UserContent block
106                        match content.first() {
107                            Some(UserContent::Text { text }) => text.clone(),
108                            _ => {
109                                return Err(eyre!(
110                                    "Only text messages are supported when using --session"
111                                ));
112                            }
113                        }
114                    }
115                    _ => {
116                        return Err(eyre!(
117                            "Only user messages are supported when using --session"
118                        ));
119                    }
120                };
121
122                crate::run_once_in_session(&session_manager, session_id.clone(), message).await?
123            }
124            _ => {
125                // Run in new ephemeral session (default behavior)
126                // For headless mode, auto-approve all tools for convenience
127                let auto_approve_policy = {
128                    let all_tools = [
129                        BASH_TOOL_NAME,
130                        GREP_TOOL_NAME,
131                        GLOB_TOOL_NAME,
132                        LS_TOOL_NAME,
133                        VIEW_TOOL_NAME,
134                        EDIT_TOOL_NAME,
135                        MULTI_EDIT_TOOL_NAME,
136                        REPLACE_TOOL_NAME,
137                        TODO_READ_TOOL_NAME,
138                        TODO_WRITE_TOOL_NAME,
139                        FETCH_TOOL_NAME,
140                        DISPATCH_AGENT_TOOL_NAME,
141                    ]
142                    .iter()
143                    .map(|s| s.to_string())
144                    .collect::<std::collections::HashSet<String>>();
145                    crate::session::ToolApprovalPolicy::PreApproved { tools: all_tools }
146                };
147
148                // Convert API messages to app messages
149                let app_messages: Result<Vec<crate::app::Message>, _> = messages
150                    .into_iter()
151                    .map(crate::app::Message::try_from)
152                    .collect();
153
154                let app_messages =
155                    app_messages.map_err(|e| eyre!("Failed to convert messages: {}", e))?;
156
157                crate::run_once_ephemeral(
158                    &session_manager,
159                    app_messages,
160                    model_to_use,
161                    tool_config,
162                    Some(auto_approve_policy),
163                    system_prompt_to_use,
164                )
165                .await?
166            }
167        };
168
169        // Output the result as JSON
170        let json_output = serde_json::to_string_pretty(&result)
171            .map_err(|e| eyre!("Failed to serialize result to JSON: {}", e))?;
172
173        println!("{}", json_output);
174        Ok(())
175    }
176}