steer/commands/
headless.rs1use 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 let messages = if let Some(json_path) = &self.messages_json {
35 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 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 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 let model_to_use = self.model.unwrap_or(self.global_model);
65
66 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 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 let session_manager = crate::create_session_manager().await?;
92
93 let result = match &self.session {
95 Some(session_id) => {
96 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 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 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 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 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}