1mod cli;
2mod event_processor;
3mod event_processor_with_human_output;
4mod event_processor_with_json_output;
5
6use std::io::IsTerminal;
7use std::io::Read;
8use std::path::PathBuf;
9
10use agcodex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
11use agcodex_core::ConversationManager;
12use agcodex_core::NewConversation;
13use agcodex_core::config::Config;
14use agcodex_core::config::ConfigOverrides;
15use agcodex_core::protocol::AskForApproval;
16use agcodex_core::protocol::Event;
17use agcodex_core::protocol::EventMsg;
18use agcodex_core::protocol::InputItem;
19use agcodex_core::protocol::Op;
20use agcodex_core::protocol::TaskCompleteEvent;
21use agcodex_core::util::is_inside_git_repo;
22use agcodex_ollama::DEFAULT_OSS_MODEL;
23use agcodex_protocol::config_types::SandboxMode;
24pub use cli::Cli;
25use event_processor_with_human_output::EventProcessorWithHumanOutput;
26use event_processor_with_json_output::EventProcessorWithJsonOutput;
27use tracing::debug;
28use tracing::error;
29use tracing::info;
30use tracing_subscriber::EnvFilter;
31
32use crate::event_processor::CodexStatus;
33use crate::event_processor::EventProcessor;
34
35pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
36 let Cli {
37 images,
38 model: model_cli_arg,
39 oss,
40 config_profile,
41 full_auto,
42 dangerously_bypass_approvals_and_sandbox,
43 cwd,
44 skip_git_repo_check,
45 color,
46 last_message_file,
47 json: json_mode,
48 sandbox_mode: sandbox_mode_cli_arg,
49 prompt,
50 config_overrides,
51 } = cli;
52
53 let prompt = match prompt {
55 Some(p) if p != "-" => p,
56 maybe_dash => {
58 let force_stdin = matches!(maybe_dash.as_deref(), Some("-"));
61
62 if std::io::stdin().is_terminal() && !force_stdin {
63 eprintln!(
64 "No prompt provided. Either specify one as an argument or pipe the prompt into stdin."
65 );
66 std::process::exit(1);
67 }
68
69 if !force_stdin {
74 eprintln!("Reading prompt from stdin...");
75 }
76 let mut buffer = String::new();
77 if let Err(e) = std::io::stdin().read_to_string(&mut buffer) {
78 eprintln!("Failed to read prompt from stdin: {e}");
79 std::process::exit(1);
80 } else if buffer.trim().is_empty() {
81 eprintln!("No prompt provided via stdin.");
82 std::process::exit(1);
83 }
84 buffer
85 }
86 };
87
88 let (stdout_with_ansi, stderr_with_ansi) = match color {
89 cli::Color::Always => (true, true),
90 cli::Color::Never => (false, false),
91 cli::Color::Auto => (
92 std::io::stdout().is_terminal(),
93 std::io::stderr().is_terminal(),
94 ),
95 };
96
97 let default_level = "error";
99 let _ = tracing_subscriber::fmt()
100 .with_env_filter(
103 EnvFilter::try_from_default_env()
104 .or_else(|_| EnvFilter::try_new(default_level))
105 .unwrap_or_else(|_| EnvFilter::new(default_level)),
106 )
107 .with_ansi(stderr_with_ansi)
108 .with_writer(std::io::stderr)
109 .try_init();
110
111 let sandbox_mode = if full_auto {
112 Some(SandboxMode::WorkspaceWrite)
113 } else if dangerously_bypass_approvals_and_sandbox {
114 Some(SandboxMode::DangerFullAccess)
115 } else {
116 sandbox_mode_cli_arg.map(Into::<SandboxMode>::into)
117 };
118
119 let model = if let Some(model) = model_cli_arg {
123 Some(model)
124 } else if oss {
125 Some(DEFAULT_OSS_MODEL.to_owned())
126 } else {
127 None };
129
130 let model_provider = if oss {
131 Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_string())
132 } else {
133 None };
135
136 let overrides = ConfigOverrides {
138 model,
139 config_profile,
140 approval_policy: Some(AskForApproval::Never),
143 sandbox_mode,
144 cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)),
145 model_provider,
146 codex_linux_sandbox_exe,
147 base_instructions: None,
148 include_plan_tool: None,
149 include_apply_patch_tool: None,
150 disable_response_storage: oss.then_some(true),
151 show_raw_agent_reasoning: oss.then_some(true),
152 };
153 let cli_kv_overrides = match config_overrides.parse_overrides() {
155 Ok(v) => v,
156 Err(e) => {
157 eprintln!("Error parsing -c overrides: {e}");
158 std::process::exit(1);
159 }
160 };
161
162 let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?;
163 let mut event_processor: Box<dyn EventProcessor> = if json_mode {
164 Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone()))
165 } else {
166 Box::new(EventProcessorWithHumanOutput::create_with_ansi(
167 stdout_with_ansi,
168 &config,
169 last_message_file.clone(),
170 ))
171 };
172
173 if oss {
174 agcodex_ollama::ensure_oss_ready(&config)
175 .await
176 .map_err(|e| anyhow::anyhow!("OSS setup failed: {e}"))?;
177 }
178
179 event_processor.print_config_summary(&config, &prompt);
182
183 if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) {
184 eprintln!("Not inside a trusted directory and --skip-git-repo-check was not specified.");
185 std::process::exit(1);
186 }
187
188 let conversation_manager = ConversationManager::default();
189 let NewConversation {
190 conversation_id: _,
191 conversation,
192 session_configured,
193 } = conversation_manager.new_conversation(config).await?;
194 info!("Codex initialized with event: {session_configured:?}");
195
196 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Event>();
197 {
198 let conversation = conversation.clone();
199 tokio::spawn(async move {
200 loop {
201 tokio::select! {
202 _ = tokio::signal::ctrl_c() => {
203 tracing::debug!("Keyboard interrupt");
204 conversation.submit(Op::Interrupt).await.ok();
206
207 break;
210 }
211 res = conversation.next_event() => match res {
212 Ok(event) => {
213 debug!("Received event: {event:?}");
214
215 let is_shutdown_complete = matches!(event.msg, EventMsg::ShutdownComplete);
216 if let Err(e) = tx.send(event) {
217 error!("Error sending event: {e:?}");
218 break;
219 }
220 if is_shutdown_complete {
221 info!("Received shutdown event, exiting event loop.");
222 break;
223 }
224 },
225 Err(e) => {
226 error!("Error receiving event: {e:?}");
227 break;
228 }
229 }
230 }
231 }
232 });
233 }
234
235 if !images.is_empty() {
237 let items: Vec<InputItem> = images
238 .into_iter()
239 .map(|path| InputItem::LocalImage { path })
240 .collect();
241 let initial_images_event_id = conversation.submit(Op::UserInput { items }).await?;
242 info!("Sent images with event ID: {initial_images_event_id}");
243 while let Ok(event) = conversation.next_event().await {
244 if event.id == initial_images_event_id
245 && matches!(
246 event.msg,
247 EventMsg::TaskComplete(TaskCompleteEvent {
248 last_agent_message: _,
249 })
250 )
251 {
252 break;
253 }
254 }
255 }
256
257 let items: Vec<InputItem> = vec![InputItem::Text { text: prompt }];
259 let initial_prompt_task_id = conversation.submit(Op::UserInput { items }).await?;
260 info!("Sent prompt with event ID: {initial_prompt_task_id}");
261
262 while let Some(event) = rx.recv().await {
264 let shutdown: CodexStatus = event_processor.process_event(event);
265 match shutdown {
266 CodexStatus::Running => continue,
267 CodexStatus::InitiateShutdown => {
268 conversation.submit(Op::Shutdown).await?;
269 }
270 CodexStatus::Shutdown => {
271 break;
272 }
273 }
274 }
275
276 Ok(())
277}