#[cfg(unix)]
use std::io::Write;
#[cfg(unix)]
use std::path::PathBuf;
#[cfg(unix)]
use std::time::Duration;
use clap::Parser;
#[cfg(unix)]
use winx_code_agent::daemon::{default_socket_path, DaemonClient};
use winx_code_agent::{start_winx_server, Result, WinxError};
#[derive(Parser)]
#[command(name = "winx")]
#[command(author = "Gabriel Maia")]
#[command(version)]
#[command(about = "High-performance MCP server for shell and file operations", long_about = None)]
struct Cli {
#[arg(short, long)]
verbose: bool,
#[arg(long)]
debug: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(clap::Subcommand)]
enum Commands {
Serve {
#[arg(long)]
debug_mode: bool,
#[arg(long)]
http: bool,
#[arg(long, default_value = "127.0.0.1:8000")]
bind: String,
#[arg(long)]
token: Option<String>,
#[arg(long = "allowed-host")]
allowed_host: Vec<String>,
#[arg(long)]
allow_query_token: bool,
},
#[cfg(unix)]
List {
#[arg(long)]
socket: Option<PathBuf>,
},
#[cfg(unix)]
Attach {
thread_id: String,
#[arg(long, default_value = "cli")]
consumer: String,
#[arg(long)]
follow: bool,
#[arg(long)]
socket: Option<PathBuf>,
},
#[cfg(unix)]
Kill {
thread_id: String,
#[arg(long)]
socket: Option<PathBuf>,
},
}
fn setup_logging(verbose: bool, debug: bool) {
let level = if debug {
tracing::Level::DEBUG
} else if verbose {
tracing::Level::INFO
} else {
tracing::Level::WARN
};
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env().add_directive(level.into()),
)
.with_writer(std::io::stderr)
.with_ansi(true)
.init();
}
fn main() -> Result<()> {
let cli = Cli::parse();
setup_logging(cli.verbose, cli.debug);
winx_code_agent::sandbox::apply_if_requested();
tokio::runtime::Runtime::new()
.map_err(|e| {
WinxError::ShellInitializationError(format!("failed to start tokio runtime: {e}"))
})?
.block_on(async_main(cli))
}
async fn async_main(cli: Cli) -> Result<()> {
match cli.command {
Some(Commands::Serve {
http: true, bind, token, allowed_host, allow_query_token, ..
}) => run_http_server(bind, token, allowed_host, allow_query_token).await,
#[cfg(unix)]
Some(Commands::List { socket }) => run_list(socket).await,
#[cfg(unix)]
Some(Commands::Attach { thread_id, consumer, follow, socket }) => {
run_attach(socket, thread_id, consumer, follow).await
}
#[cfg(unix)]
Some(Commands::Kill { thread_id, socket }) => run_kill(socket, thread_id).await,
None | Some(Commands::Serve { .. }) => run_server().await,
}
}
#[cfg(unix)]
fn daemon_client(socket: Option<PathBuf>) -> DaemonClient {
DaemonClient::new(socket.unwrap_or_else(default_socket_path))
}
#[cfg(unix)]
async fn run_list(socket: Option<PathBuf>) -> Result<()> {
let sessions = daemon_client(socket).list_sessions().await?;
serde_json::to_writer_pretty(std::io::stdout().lock(), &sessions)
.map_err(|error| WinxError::SerializationError(error.to_string()))?;
writeln!(std::io::stdout().lock())?;
Ok(())
}
#[cfg(unix)]
async fn run_attach(
socket: Option<PathBuf>,
thread_id: String,
consumer: String,
follow: bool,
) -> Result<()> {
let client = daemon_client(socket);
loop {
let read = client.read_output(&thread_id, &consumer).await?;
if read.gap {
writeln!(std::io::stderr().lock(), "(...daemon scrollback truncated...)")?;
}
if !read.output.is_empty() {
let mut stdout = std::io::stdout().lock();
stdout.write_all(read.output.as_bytes())?;
stdout.flush()?;
}
if !follow {
return Ok(());
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
#[cfg(unix)]
async fn run_kill(socket: Option<PathBuf>, thread_id: String) -> Result<()> {
let killed = daemon_client(socket).kill_session(&thread_id).await?;
writeln!(std::io::stdout().lock(), "{}", if killed { "killed" } else { "not found" })?;
Ok(())
}
async fn run_http_server(
bind: String,
token: Option<String>,
allowed_hosts: Vec<String>,
allow_query_token: bool,
) -> Result<()> {
let token = token.or_else(|| std::env::var("WINX_HTTP_TOKEN").ok()).unwrap_or_default();
tracing::info!("Starting winx remote MCP (HTTP) v{} on {bind}", env!("CARGO_PKG_VERSION"));
winx_code_agent::http_server::start_http_server(&bind, token, allowed_hosts, allow_query_token)
.await
.map_err(|e| WinxError::ShellInitializationError(format!("HTTP server failed: {e}")))
}
async fn run_server() -> Result<()> {
tracing::info!("Starting winx MCP server v{}", env!("CARGO_PKG_VERSION"));
match start_winx_server().await {
Ok(()) => {
tracing::info!("Server shutting down normally");
Ok(())
}
Err(e) => {
tracing::error!("Server error: {}", e);
Err(WinxError::ShellInitializationError(format!("Failed to start server: {e}")))
}
}
}