mod ansi_parse;
mod completer;
mod diff_render;
mod headless;
mod highlight;
mod history_render;
mod input;
mod md_render;
mod mouse_select;
mod onboarding;
mod repl;
mod scroll_buffer;
mod server;
mod sink;
mod startup;
mod tool_history;
mod tui_app;
mod tui_commands;
mod tui_context;
mod tui_handlers_inference;
mod tui_output;
mod tui_render;
mod tui_types;
mod tui_viewport;
mod tui_wizards;
mod widgets;
mod wrap_input;
mod wrap_util;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use koda_core::persistence::Persistence;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "koda", version, about)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(short, long, value_name = "PROMPT")]
prompt: Option<String>,
#[arg(value_name = "PROMPT", conflicts_with = "prompt")]
positional_prompt: Option<String>,
#[arg(long, default_value = "text", value_parser = ["text", "json"])]
output_format: String,
#[arg(short, long, default_value = "default")]
agent: String,
#[arg(short, long = "resume", alias = "session")]
session: Option<String>,
#[arg(long)]
project_root: Option<PathBuf>,
#[arg(long, env = "KODA_BASE_URL")]
base_url: Option<String>,
#[arg(long, env = "KODA_MODEL")]
model: Option<String>,
#[arg(long, env = "KODA_PROVIDER")]
provider: Option<String>,
#[arg(long)]
max_tokens: Option<u32>,
#[arg(long)]
temperature: Option<f64>,
#[arg(long)]
thinking_budget: Option<u32>,
#[arg(long)]
reasoning_effort: Option<String>,
}
#[derive(Subcommand, Debug)]
enum Command {
Server {
#[arg(long, default_value = "9999")]
port: u16,
#[arg(long)]
stdio: bool,
},
Connect { url: String },
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(cmd) = &cli.command {
match cmd {
Command::Server { port, stdio } => {
if *stdio {
match koda_core::keystore::KeyStore::load() {
Ok(store) => store.inject_into_env(),
Err(e) => tracing::warn!("Failed to load keystore: {e}"),
}
let project_root = cli.project_root.clone().unwrap_or_else(|| {
std::env::current_dir().expect("Failed to get current directory")
});
let project_root = std::fs::canonicalize(&project_root)?;
let config = koda_core::config::KodaConfig::load(&project_root, &cli.agent)?;
let config = config
.with_overrides(
cli.base_url.clone(),
cli.model.clone(),
cli.provider.clone(),
)
.with_model_overrides(
cli.max_tokens,
cli.temperature,
cli.thinking_budget,
cli.reasoning_effort.clone(),
);
server::run_stdio_server(project_root, config).await?;
} else {
eprintln!("WebSocket server (--port {port}) not yet implemented. Use --stdio.");
std::process::exit(1);
}
return Ok(());
}
Command::Connect { url } => {
println!("Not implemented: Connect to {}", url);
std::process::exit(0);
}
}
}
let headless_prompt = resolve_headless_prompt(&cli)?;
let project_root = cli
.project_root
.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory"));
let project_root = std::fs::canonicalize(&project_root)?;
let log_dir = koda_core::db::config_dir()?.join("logs");
std::fs::create_dir_all(&log_dir)?;
let file_appender = tracing_appender::rolling::daily(&log_dir, "koda.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_env_filter("koda_agent=debug")
.init();
tracing::info!("Koda starting. Project root: {:?}", project_root);
match koda_core::keystore::KeyStore::load() {
Ok(store) => store.inject_into_env(),
Err(e) => tracing::warn!("Failed to load keystore: {e}"),
}
if let Some(prompt) = headless_prompt {
let config = koda_core::config::KodaConfig::load(&project_root, &cli.agent)?;
let config = config
.with_overrides(cli.base_url, cli.model, cli.provider)
.with_model_overrides(
cli.max_tokens,
cli.temperature,
cli.thinking_budget,
cli.reasoning_effort,
);
let db = koda_core::db::Database::init(&koda_core::db::config_dir()?).await?;
let session_id = match cli.session {
Some(id) => id,
None => db.create_session(&config.agent_name, &project_root).await?,
};
let exit_code = headless::run_headless(
project_root,
config,
db,
session_id,
prompt,
&cli.output_format,
)
.await?;
std::process::exit(exit_code);
}
let version_check = koda_core::version::spawn_version_check();
let first_run = onboarding::is_first_run();
let config = koda_core::config::KodaConfig::load(&project_root, &cli.agent)?;
let config = config
.with_overrides(cli.base_url, cli.model, cli.provider)
.with_model_overrides(
cli.max_tokens,
cli.temperature,
cli.thinking_budget,
cli.reasoning_effort,
);
let db = koda_core::db::Database::init(&koda_core::db::config_dir()?).await?;
let session_id = match cli.session {
Some(id) => id,
None => db.create_session(&config.agent_name, &project_root).await?,
};
tui_app::run(
project_root,
config,
db,
session_id,
version_check,
first_run,
)
.await
}
fn resolve_headless_prompt(cli: &Cli) -> Result<Option<String>> {
if let Some(ref p) = cli.prompt {
if p == "-" {
use std::io::Read;
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.context("Failed to read from stdin")?;
return Ok(Some(input.trim().to_string()));
}
return Ok(Some(p.clone()));
}
if let Some(ref p) = cli.positional_prompt {
return Ok(Some(p.clone()));
}
if !atty_is_terminal() {
use std::io::Read;
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.context("Failed to read from stdin")?;
let trimmed = input.trim().to_string();
if !trimmed.is_empty() {
return Ok(Some(trimmed));
}
}
Ok(None)
}
fn atty_is_terminal() -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal()
}