use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(name = "seekr-code")]
#[command(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true)]
json: bool,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
}
#[derive(Subcommand)]
enum Commands {
Search {
query: String,
#[arg(short, long, default_value = "hybrid")]
mode: String,
#[arg(short = 'k', long, default_value = "20")]
top_k: usize,
#[arg(short, long, default_value = ".")]
path: String,
},
Index {
#[arg(default_value = ".")]
path: String,
#[arg(long)]
force: bool,
},
Serve {
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(short, long, default_value = "7720")]
port: u16,
#[arg(long)]
mcp: bool,
#[arg(long)]
watch: Option<String>,
},
Status {
#[arg(default_value = ".")]
path: String,
},
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let default_filter = match cli.verbose {
0 => "seekr_code=warn",
1 => "seekr_code=info",
2 => "seekr_code=debug",
_ => "seekr_code=trace",
};
let env_filter = EnvFilter::try_from_env("SEEKR_LOG")
.or_else(|_| EnvFilter::try_from_env("RUST_LOG"))
.unwrap_or_else(|_| EnvFilter::new(default_filter));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.init();
let config = seekr_code::config::SeekrConfig::load()
.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
match cli.command {
Commands::Search {
query,
mode,
top_k,
path,
} => {
tracing::info!(query = %query, mode = %mode, top_k = top_k, path = %path, "Starting search");
seekr_code::server::cli::cmd_search(&query, &mode, top_k, &path, &config, cli.json)?;
}
Commands::Index { path, force } => {
tracing::info!(path = %path, force = force, "Building index");
seekr_code::server::cli::cmd_index(&path, force, &config, cli.json)?;
}
Commands::Serve { host, port, mcp, watch } => {
if mcp {
if watch.is_some() {
tracing::warn!("--watch is not supported in MCP stdio mode, ignoring");
}
tracing::info!("Starting MCP server on stdio");
seekr_code::server::mcp::run_mcp_stdio(&config)?;
} else {
tracing::info!(host = %host, port = port, "Starting HTTP server");
let rt = tokio::runtime::Runtime::new()
.map_err(|e| anyhow::anyhow!("Failed to create tokio runtime: {}", e))?;
rt.block_on(async {
if let Some(watch_path) = watch {
let watch_dir = std::path::Path::new(&watch_path).canonicalize()
.map_err(|e| anyhow::anyhow!("Invalid watch path '{}': {}", watch_path, e))?;
let index_dir = config.index_dir.join(
watch_dir.file_name().unwrap_or_default().to_string_lossy().as_ref()
);
let index = match seekr_code::index::store::SeekrIndex::load(&index_dir) {
Ok(idx) => idx,
Err(_) => {
tracing::info!("No existing index found, starting with empty index");
seekr_code::index::store::SeekrIndex::new(384)
}
};
let shared_index = std::sync::Arc::new(tokio::sync::RwLock::new(index));
let daemon_config = config.clone();
let daemon_index = shared_index.clone();
let daemon_path = watch_dir.clone();
tokio::spawn(async move {
if let Err(e) = seekr_code::server::daemon::run_watch_daemon(
&daemon_path,
&daemon_config,
daemon_index,
None,
).await {
tracing::error!("Watch daemon error: {}", e);
}
});
tracing::info!(
watch_path = %watch_dir.display(),
"Watch daemon started alongside HTTP server"
);
}
seekr_code::server::http::start_http_server(&host, port, config).await
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
}
}
Commands::Status { path } => {
tracing::info!(path = %path, "Checking status");
seekr_code::server::cli::cmd_status(&path, &config, cli.json)?;
}
}
Ok(())
}