1use std::io::Read;
5use std::process::ExitCode;
6use std::sync::Arc;
7
8use locode_core::{
9 CacheHint, EngineConfig, EventSink, FnSink, Host, HostConfig, NullSink, PackContext,
10 PathPolicy, ProviderInit, ProviderRegistry, SamplingArgs, Session, grok,
11};
12
13use crate::cli::{Cli, Harness, OutputFormat};
14use crate::output;
15
16pub struct PreRunError(pub String);
18
19impl<E: std::fmt::Display> From<E> for PreRunError {
20 fn from(e: E) -> Self {
21 PreRunError(e.to_string())
22 }
23}
24
25pub async fn run(cli: Cli, providers: &ProviderRegistry) -> Result<ExitCode, PreRunError> {
35 #[cfg(unix)]
39 let cancel_slot = crate::signal::install_sigterm();
40
41 let prompt = resolve_prompt(cli.prompt.as_deref())?;
43
44 let cwd = match cli.cwd {
48 Some(dir) => dir,
49 None => std::env::current_dir()?,
50 };
51 let cwd = std::fs::canonicalize(&cwd)
52 .map_err(|e| PreRunError(format!("--cwd {}: {e}", cwd.display())))?;
53
54 let mut host_config = HostConfig::new(&cwd);
55 if cli.dangerously_skip_permissions {
56 host_config.path_policy = PathPolicy::Unrestricted;
57 }
58 let host = Arc::new(Host::new(host_config)?);
59
60 let pack = locode_core::resolve(cli.harness.as_str())?;
62 let registry = pack.build_registry(&host);
63 let pack_ctx = PackContext {
64 cwd: cwd.clone(),
65 os: std::env::consts::OS.to_string(),
66 shell: std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()),
67 date: chrono::Local::now().format("%Y-%m-%d").to_string(),
68 headless: true,
69 strip_identity: cli.strip_identity,
70 };
71 let preamble = pack.preamble(&pack_ctx);
72
73 let user_prompt = match cli.harness {
76 Harness::Grok => grok::prompt::user_query(&prompt),
77 };
78
79 let session_id = new_session_id();
82 let built = providers
83 .build(
84 &cli.api_schema,
85 &ProviderInit {
86 session_id: session_id.clone(),
87 },
88 )
89 .map_err(|e| PreRunError(e.to_string()))?;
90 let (provider, model) = (built.provider, built.model);
91
92 let config = EngineConfig {
94 session_id,
95 harness: cli.harness.as_str().to_string(),
96 api_schema: provider.api_schema().to_string(),
97 model,
98 cwd: cwd.clone(),
99 workspace_root: cwd,
100 max_turns: cli.max_turns,
101 sampling_args: SamplingArgs::default(),
102 cache_hint: CacheHint::Standard,
103 ..EngineConfig::default()
104 };
105 let sink: Box<dyn EventSink> = match cli.output_format {
106 OutputFormat::StreamJson => Box::new(FnSink(|event| output::write_json_line(&event))),
109 OutputFormat::Json | OutputFormat::Text => Box::new(NullSink),
111 };
112
113 let mut session = Session::new(provider, registry, preamble, config, sink);
115 #[cfg(unix)]
116 crate::signal::arm(&cancel_slot, session.cancel_handle());
117 let report = session.run_text(user_prompt).await;
118
119 match cli.output_format {
120 OutputFormat::Json => output::write_json_line(&report),
121 OutputFormat::Text => output::write_text(report.final_message.as_deref().unwrap_or("")),
122 OutputFormat::StreamJson => {} }
124 Ok(output::exit_code(report.status))
125}
126
127fn resolve_prompt(arg: Option<&str>) -> Result<String, PreRunError> {
130 let prompt = match arg {
131 Some("-") | None => {
132 let mut buf = String::new();
133 std::io::stdin().read_to_string(&mut buf)?;
134 buf
135 }
136 Some(text) => text.to_string(),
137 };
138 let prompt = prompt.trim().to_string();
139 if prompt.is_empty() {
140 return Err(PreRunError(
141 "no prompt: pass it as the positional argument or on stdin".to_string(),
142 ));
143 }
144 Ok(prompt)
145}
146
147fn new_session_id() -> String {
149 let now = std::time::SystemTime::now()
150 .duration_since(std::time::UNIX_EPOCH)
151 .map_or(0, |d| d.as_millis());
152 format!("sess-{now}-{}", std::process::id())
153}