use std::io::{self, BufRead, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use thal::llm::{AnthropicProvider, OpenAiCompatProvider};
use thal::Reactor;
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "thal=warn".into()),
)
.with_target(false)
.compact()
.init();
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("setup") => thal::setup::run(args.get(1).map(String::as_str)).await,
Some("login") => match args.get(1) {
Some(provider) => login_command(provider),
None => {
eprintln!("usage: thal login <provider>");
ExitCode::from(2)
}
},
Some(path) => run_program(path).await,
None => {
eprintln!("usage:");
eprintln!(" thal <file.thal> run a thal program");
eprintln!(" thal setup [<name>] interactive provider setup wizard");
eprintln!(" thal login <provider> save a provider token to ~/.config/thal");
ExitCode::from(2)
}
}
}
async fn run_program(path: &str) -> ExitCode {
print_banner();
let program = match thal::load(path) {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(1);
}
};
let reactor = Reactor::new(program);
register_providers_from_env(&reactor);
if let Err(e) = reactor.run().await {
eprintln!("runtime error: {e}");
return ExitCode::from(1);
}
ExitCode::SUCCESS
}
fn print_banner() {
use owo_colors::OwoColorize;
let lines = [
" ████████╗██╗ ██╗ █████╗ ██╗ ",
" ╚══██╔══╝██║ ██║██╔══██╗██║ ",
" ██║ ███████║███████║██║ ",
" ██║ ██╔══██║██╔══██║██║ ",
" ██║ ██║ ██║██║ ██║███████╗",
" ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝",
];
eprintln!();
for line in lines {
eprintln!("{}", line.cyan().bold());
}
eprintln!(
" {}",
"reactive semantic runtime".dimmed()
);
eprintln!(
" {}\n",
"/help · /exit · Ctrl+D to quit".dimmed()
);
}
fn register_providers_from_env(reactor: &Reactor) {
let providers = reactor.llm_providers();
if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") {
providers.register(Arc::new(AnthropicProvider::new(key)));
tracing::info!("registered Anthropic provider from ANTHROPIC_API_KEY");
}
if let Ok(key) = std::env::var("OPENAI_API_KEY") {
providers.register(Arc::new(OpenAiCompatProvider::openai(key)));
tracing::info!("registered OpenAI provider from OPENAI_API_KEY");
}
if let Ok(key) = std::env::var("OPENROUTER_API_KEY") {
providers.register(Arc::new(OpenAiCompatProvider::openrouter(key)));
tracing::info!("registered OpenRouter provider from OPENROUTER_API_KEY");
}
}
fn login_command(provider: &str) -> ExitCode {
let dir = match config_dir() {
Some(d) => d,
None => {
eprintln!("error: cannot determine config directory (set XDG_CONFIG_HOME or HOME)");
return ExitCode::from(1);
}
};
if let Err(e) = std::fs::create_dir_all(&dir) {
eprintln!("error: create {}: {e}", dir.display());
return ExitCode::from(1);
}
let path = dir.join(format!("{provider}.token"));
let stderr = io::stderr();
let mut stderr = stderr.lock();
let _ = write!(stderr, "paste your {provider} token: ");
let _ = stderr.flush();
let stdin = io::stdin();
let mut line = String::new();
if let Err(e) = stdin.lock().read_line(&mut line) {
eprintln!("error: read stdin: {e}");
return ExitCode::from(1);
}
let token = line.trim();
if token.is_empty() {
eprintln!("error: empty token");
return ExitCode::from(1);
}
if let Err(e) = std::fs::write(&path, token) {
eprintln!("error: write {}: {e}", path.display());
return ExitCode::from(1);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
}
eprintln!("saved token to {}", path.display());
eprintln!(
"tip: register the provider by exporting:\n {NAME}_TOKEN_FILE={path}\n {NAME}_BASE_URL=<api base url>",
NAME = provider.to_uppercase(),
path = path.display()
);
ExitCode::SUCCESS
}
fn config_dir() -> Option<PathBuf> {
if let Some(d) = std::env::var_os("XDG_CONFIG_HOME") {
return Some(PathBuf::from(d).join("thal"));
}
if let Some(home) = std::env::var_os("HOME") {
return Some(PathBuf::from(home).join(".config").join("thal"));
}
None
}