use std::io::Read;
use std::process::ExitCode;
use std::sync::Arc;
use locode_core::{
CacheHint, EngineConfig, EventSink, FnSink, Host, HostConfig, NullSink, PackContext,
PathPolicy, ProviderInit, ProviderRegistry, SamplingArgs, Session, grok,
};
use crate::cli::{Cli, Harness, OutputFormat};
use crate::output;
pub struct PreRunError(pub String);
impl<E: std::fmt::Display> From<E> for PreRunError {
fn from(e: E) -> Self {
PreRunError(e.to_string())
}
}
pub async fn run(cli: Cli, providers: &ProviderRegistry) -> Result<ExitCode, PreRunError> {
#[cfg(unix)]
let cancel_slot = crate::signal::install_sigterm();
let prompt = resolve_prompt(cli.prompt.as_deref())?;
let cwd = match cli.cwd {
Some(dir) => dir,
None => std::env::current_dir()?,
};
let cwd = std::fs::canonicalize(&cwd)
.map_err(|e| PreRunError(format!("--cwd {}: {e}", cwd.display())))?;
let mut host_config = HostConfig::new(&cwd);
if cli.dangerously_skip_permissions {
host_config.path_policy = PathPolicy::Unrestricted;
}
let host = Arc::new(Host::new(host_config)?);
let pack = locode_core::resolve(cli.harness.as_str())?;
let registry = pack.build_registry(&host);
let pack_ctx = PackContext {
cwd: cwd.clone(),
os: std::env::consts::OS.to_string(),
shell: std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()),
date: chrono::Local::now().format("%Y-%m-%d").to_string(),
headless: true,
strip_identity: cli.strip_identity,
};
let preamble = pack.preamble(&pack_ctx);
let user_prompt = match cli.harness {
Harness::Grok => grok::prompt::user_query(&prompt),
};
let session_id = new_session_id();
let built = providers
.build(
&cli.api_schema,
&ProviderInit {
session_id: session_id.clone(),
},
)
.map_err(|e| PreRunError(e.to_string()))?;
let (provider, model) = (built.provider, built.model);
let config = EngineConfig {
session_id,
harness: cli.harness.as_str().to_string(),
api_schema: provider.api_schema().to_string(),
model,
cwd: cwd.clone(),
workspace_root: cwd,
max_turns: cli.max_turns,
sampling_args: SamplingArgs::default(),
cache_hint: CacheHint::Standard,
..EngineConfig::default()
};
let sink: Box<dyn EventSink> = match cli.output_format {
OutputFormat::StreamJson => Box::new(FnSink(|event| output::write_json_line(&event))),
OutputFormat::Json | OutputFormat::Text => Box::new(NullSink),
};
let mut session = Session::new(provider, registry, preamble, config, sink);
#[cfg(unix)]
crate::signal::arm(&cancel_slot, session.cancel_handle());
let report = session.run_text(user_prompt).await;
match cli.output_format {
OutputFormat::Json => output::write_json_line(&report),
OutputFormat::Text => output::write_text(report.final_message.as_deref().unwrap_or("")),
OutputFormat::StreamJson => {} }
Ok(output::exit_code(report.status))
}
fn resolve_prompt(arg: Option<&str>) -> Result<String, PreRunError> {
let prompt = match arg {
Some("-") | None => {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
buf
}
Some(text) => text.to_string(),
};
let prompt = prompt.trim().to_string();
if prompt.is_empty() {
return Err(PreRunError(
"no prompt: pass it as the positional argument or on stdin".to_string(),
));
}
Ok(prompt)
}
fn new_session_id() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_millis());
format!("sess-{now}-{}", std::process::id())
}