use std::io::{self, IsTerminal};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{CommandFactory, Parser, builder::TypedValueParser, error::ErrorKind};
use liminal_cli::{Config, Live, Server};
mod identity;
const DEFAULT_FILTER: &str = "warn,liminal_server=info,liminal=info";
#[derive(Debug, Parser)]
#[command(name = "liminal", arg_required_else_help = true)]
struct Cli {
#[arg(long, global = true, value_parser = clap::builder::OsStringValueParser::new().map(PathBuf::from))]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, clap::Subcommand)]
enum Command {
Check,
Identity,
MigrateParticipantLogs,
CheckpointCensus {
#[arg(long)]
conversation: u64,
#[arg(long)]
store: Option<PathBuf>,
},
Mcp,
}
impl Command {
const fn usage(&self) -> &'static str {
match self {
Self::Check => "liminal check --config <CONFIG>",
Self::Identity => "liminal identity --config <CONFIG>",
Self::MigrateParticipantLogs => "liminal migrate-participant-logs --config <CONFIG>",
Self::CheckpointCensus { .. } => {
"liminal checkpoint-census --config <CONFIG> --conversation <CONVERSATION> [--store <STORE>]"
}
Self::Mcp => "liminal mcp --config <CONFIG>",
}
}
}
fn init_tracing() -> anyhow::Result<()> {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(DEFAULT_FILTER));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(io::stderr().is_terminal())
.with_writer(io::stderr)
.try_init()
.map_err(|error| anyhow::anyhow!("failed to initialize tracing: {error}"))
}
fn run_server(config: &Path, command: Option<Command>) -> anyhow::Result<()> {
if !matches!(command, Some(Command::Check)) {
init_tracing()?;
}
match command {
None => liminal_server::run(config)?,
Some(Command::Check) => {
liminal_server::config::file::load_config(config)?;
println!("{}: configuration valid", config.display());
}
Some(Command::Identity) => identity::run(config)?,
Some(Command::MigrateParticipantLogs) => {
let migration = liminal_server::run_participant_log_migration(config)?;
for conversation in &migration.conversations {
println!(
"{} {:?}",
conversation.conversation_id, conversation.outcome
);
}
}
Some(Command::CheckpointCensus {
conversation,
store,
}) => {
for line in
liminal_server::run_checkpoint_census(config, conversation, store.as_deref())?
{
println!("{line}");
}
}
Some(Command::Mcp) => anyhow::bail!("mcp must use the seat configuration"),
}
Ok(())
}
fn run_mcp(path: &Path) -> ExitCode {
let config = match Config::load(path) {
Ok(config) => config,
Err(error) => {
eprintln!("liminal mcp: {error}");
return ExitCode::from(1);
}
};
let frame_bytes = config.limits.frame_bytes;
let live = match Live::new(&config) {
Ok(live) => live,
Err(error) => {
eprintln!("liminal mcp: {error}");
return ExitCode::from(1);
}
};
let mut server = Server::new(live, frame_bytes);
let stdin = io::stdin();
let stdout = io::stdout();
let mut input = stdin.lock();
let mut output = stdout;
match server.serve(&mut input, &mut output) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("liminal mcp: {error}");
ExitCode::from(3)
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
let Some(config) = cli.config else {
let usage = cli
.command
.as_ref()
.map_or("liminal --config <CONFIG>", Command::usage);
Cli::command()
.override_usage(usage)
.error(
ErrorKind::MissingRequiredArgument,
"--config <CONFIG> is required",
)
.exit();
};
if matches!(cli.command, Some(Command::Mcp)) {
return run_mcp(&config);
}
let checking = matches!(cli.command, Some(Command::Check));
match run_server(&config, cli.command) {
Ok(()) => ExitCode::SUCCESS,
Err(error) if checking => {
eprintln!("{error}");
ExitCode::from(2)
}
Err(error) => {
eprintln!("Error: {error:#}");
ExitCode::from(1)
}
}
}