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 let prompt = resolve_prompt(cli.prompt.as_deref())?;
37
38 let cwd = match cli.cwd {
42 Some(dir) => dir,
43 None => std::env::current_dir()?,
44 };
45 let cwd = std::fs::canonicalize(&cwd)
46 .map_err(|e| PreRunError(format!("--cwd {}: {e}", cwd.display())))?;
47
48 let mut host_config = HostConfig::new(&cwd);
49 if cli.dangerously_skip_permissions {
50 host_config.path_policy = PathPolicy::Unrestricted;
51 }
52 let host = Arc::new(Host::new(host_config)?);
53
54 let pack = locode_core::resolve(cli.harness.as_str())?;
56 let registry = pack.build_registry(&host);
57 let pack_ctx = PackContext {
58 cwd: cwd.clone(),
59 os: std::env::consts::OS.to_string(),
60 shell: std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()),
61 date: chrono::Local::now().format("%Y-%m-%d").to_string(),
62 headless: true,
63 strip_identity: cli.strip_identity,
64 };
65 let preamble = pack.preamble(&pack_ctx);
66
67 let user_prompt = match cli.harness {
70 Harness::Grok => grok::prompt::user_query(&prompt),
71 };
72
73 let session_id = new_session_id();
76 let built = providers
77 .build(
78 &cli.api_schema,
79 &ProviderInit {
80 session_id: session_id.clone(),
81 },
82 )
83 .map_err(|e| PreRunError(e.to_string()))?;
84 let (provider, model) = (built.provider, built.model);
85
86 let config = EngineConfig {
88 session_id,
89 harness: cli.harness.as_str().to_string(),
90 api_schema: provider.api_schema().to_string(),
91 model,
92 cwd: cwd.clone(),
93 workspace_root: cwd,
94 max_turns: cli.max_turns,
95 sampling_args: SamplingArgs::default(),
96 cache_hint: CacheHint::Standard,
97 ..EngineConfig::default()
98 };
99 let sink: Box<dyn EventSink> = match cli.output_format {
100 OutputFormat::StreamJson => Box::new(FnSink(|event| output::write_json_line(&event))),
103 OutputFormat::Json | OutputFormat::Text => Box::new(NullSink),
105 };
106
107 let mut session = Session::new(provider, registry, preamble, config, sink);
109 let report = session.run_text(user_prompt).await;
110
111 match cli.output_format {
112 OutputFormat::Json => output::write_json_line(&report),
113 OutputFormat::Text => output::write_text(report.final_message.as_deref().unwrap_or("")),
114 OutputFormat::StreamJson => {} }
116 Ok(output::exit_code(report.status))
117}
118
119fn resolve_prompt(arg: Option<&str>) -> Result<String, PreRunError> {
122 let prompt = match arg {
123 Some("-") | None => {
124 let mut buf = String::new();
125 std::io::stdin().read_to_string(&mut buf)?;
126 buf
127 }
128 Some(text) => text.to_string(),
129 };
130 let prompt = prompt.trim().to_string();
131 if prompt.is_empty() {
132 return Err(PreRunError(
133 "no prompt: pass it as the positional argument or on stdin".to_string(),
134 ));
135 }
136 Ok(prompt)
137}
138
139fn new_session_id() -> String {
141 let now = std::time::SystemTime::now()
142 .duration_since(std::time::UNIX_EPOCH)
143 .map_or(0, |d| d.as_millis());
144 format!("sess-{now}-{}", std::process::id())
145}