use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use serde_json::{Value, json};
use kura_cli::commands;
use kura_cli::util::{
CliOutputMode, CliRuntimeOptions, exit_error, print_json_stderr, print_json_stdout,
resolve_token, set_cli_runtime_options,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum OutputFormatArg {
Json,
JsonCompact,
}
impl From<OutputFormatArg> for CliOutputMode {
fn from(value: OutputFormatArg) -> Self {
match value {
OutputFormatArg::Json => CliOutputMode::Json,
OutputFormatArg::JsonCompact => CliOutputMode::JsonCompact,
}
}
}
#[derive(Parser)]
#[command(
name = "kura",
version,
about = "Kura Training CLI — Agent interface for training, nutrition, and health data"
)]
struct Cli {
#[arg(long, env = "KURA_API_URL", default_value = "http://localhost:3000")]
api_url: String,
#[arg(long, env = "KURA_NO_AUTH")]
no_auth: bool,
#[arg(long, global = true, value_enum, default_value_t = OutputFormatArg::Json, env = "KURA_OUTPUT")]
output: OutputFormatArg,
#[arg(long, global = true, env = "KURA_QUIET_STDERR")]
quiet_stderr: bool,
#[arg(long, global = true, env = "KURA_DRY_RUN")]
dry_run: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Health,
Access {
#[command(subcommand)]
command: commands::access::AccessCommands,
},
Api(commands::api::ApiArgs),
Exec(commands::exec::ExecArgs),
Schema {
#[arg(value_name = "COMMAND", num_args = 0..)]
path: Vec<String>,
},
Event {
#[command(subcommand)]
command: commands::event::EventCommands,
},
Projection {
#[command(subcommand)]
command: commands::projection::ProjectionCommands,
},
Analysis {
#[command(subcommand)]
command: commands::analysis::AnalysisCommands,
},
Read {
#[command(subcommand)]
command: commands::read::ReadCommands,
},
Agent {
#[command(subcommand)]
command: commands::agent::AgentCommands,
},
Log(commands::agent::LogTrainingArgs),
Observation {
#[command(subcommand)]
command: commands::observation::ObservationCommands,
},
Mcp {
#[command(subcommand)]
command: commands::mcp::McpCommands,
},
Import {
#[command(subcommand)]
command: commands::imports::ImportCommands,
},
Provider {
#[command(subcommand)]
command: commands::provider::ProviderCommands,
},
Eval {
#[command(subcommand)]
command: commands::eval::EvalCommands,
},
Snapshot,
Config,
#[command(hide = true)]
Context {
#[arg(long)]
exercise_limit: Option<u32>,
#[arg(long)]
strength_limit: Option<u32>,
#[arg(long)]
custom_limit: Option<u32>,
#[arg(long)]
task_intent: Option<String>,
#[arg(long)]
include_system: Option<bool>,
#[arg(long)]
budget_tokens: Option<u32>,
},
#[command(hide = true)]
WriteWithProof(commands::agent::WriteWithProofArgs),
Doctor,
Discover {
#[arg(long, conflicts_with = "exercises")]
endpoints: bool,
#[arg(long, conflicts_with = "endpoints")]
exercises: bool,
#[arg(long, requires = "exercises")]
query: Option<String>,
#[arg(long, requires = "exercises", value_parser = clap::value_parser!(u32).range(1..=200))]
limit: Option<u32>,
},
Account {
#[command(subcommand)]
command: commands::account::AccountCommands,
},
Admin {
#[command(subcommand)]
command: commands::admin::AdminCommands,
},
Login {
#[arg(long)]
device: bool,
},
Logout,
}
#[tokio::main]
async fn main() {
let _ = dotenvy::dotenv();
let cli = Cli::parse();
set_cli_runtime_options(CliRuntimeOptions {
output_mode: cli.output.into(),
quiet_stderr: cli.quiet_stderr,
dry_run: cli.dry_run,
});
let code = match cli.command {
Commands::Health => commands::health::run(&cli.api_url).await,
Commands::Access { command } => commands::access::run(&cli.api_url, command).await,
Commands::Api(mut args) => {
if cli.no_auth {
args.no_auth = true;
}
commands::api::run(&cli.api_url, args).await
}
Commands::Exec(args) => commands::exec::run(&cli.api_url, cli.no_auth, args).await,
Commands::Schema { path } => emit_cli_schema(&path),
Commands::Event { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::event::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Projection { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::projection::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Analysis { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::analysis::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Read { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::read::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Agent { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::agent::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Log(args) => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::agent::log_training(&cli.api_url, token.as_deref(), args).await
}
Commands::Observation { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::observation::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Mcp { command } => commands::mcp::run(&cli.api_url, cli.no_auth, command).await,
Commands::Import { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::imports::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Provider { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::provider::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Eval { command } => commands::eval::run(command).await,
Commands::Snapshot => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::system::snapshot(&cli.api_url, token.as_deref()).await
}
Commands::Config => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::system::config(&cli.api_url, token.as_deref()).await
}
Commands::Context {
exercise_limit,
strength_limit,
custom_limit,
task_intent,
include_system,
budget_tokens,
} => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::agent::context(
&cli.api_url,
token.as_deref(),
exercise_limit,
strength_limit,
custom_limit,
task_intent,
include_system,
budget_tokens,
)
.await
}
Commands::WriteWithProof(args) => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::agent::write_with_proof(&cli.api_url, token.as_deref(), args).await
}
Commands::Doctor => commands::system::doctor(&cli.api_url).await,
Commands::Discover {
endpoints,
exercises,
query,
limit,
} => {
let token = if exercises {
resolve_or_exit(&cli.api_url, cli.no_auth).await
} else {
None
};
commands::system::discover(
&cli.api_url,
endpoints,
exercises,
query,
limit,
token.as_deref(),
)
.await
}
Commands::Account { command } => {
let token = resolve_or_exit(&cli.api_url, cli.no_auth).await;
commands::account::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Admin { command } => {
commands::admin::ensure_admin_surface_enabled_or_exit();
let token = if commands::admin::requires_api_auth(&command) {
resolve_or_exit(&cli.api_url, cli.no_auth).await
} else {
None
};
commands::admin::run(&cli.api_url, token.as_deref(), command).await
}
Commands::Login { device } => {
if let Err(e) = commands::auth::login(&cli.api_url, device).await {
exit_error(&e.to_string(), None);
}
0
}
Commands::Logout => {
if let Err(e) = commands::auth::logout() {
exit_error(&e.to_string(), None);
}
0
}
};
std::process::exit(code);
}
fn emit_cli_schema(path: &[String]) -> i32 {
let root = Cli::command();
let command = match resolve_schema_path(root, path) {
Ok(command) => command,
Err((message, available_subcommands)) => {
let err = json!({
"error": "usage_error",
"message": message,
"available_subcommands": available_subcommands,
});
print_json_stderr(&err);
return 4;
}
};
let mut payload = command_schema(&command);
payload["requested_path"] = json!(path);
print_json_stdout(&payload);
0
}
fn resolve_schema_path(
mut command: clap::Command,
path: &[String],
) -> Result<clap::Command, (String, Vec<String>)> {
for segment in path {
let next = command
.get_subcommands()
.find(|candidate| candidate.get_name() == segment)
.cloned();
match next {
Some(candidate) => {
command = candidate;
}
None => {
let available_subcommands: Vec<String> = command
.get_subcommands()
.filter(|candidate| !candidate.is_hide_set())
.map(|candidate| candidate.get_name().to_string())
.collect();
return Err((
format!("Unknown command path segment '{segment}' while resolving schema."),
available_subcommands,
));
}
}
}
Ok(command)
}
fn command_schema(command: &clap::Command) -> Value {
let args: Vec<Value> = command
.get_arguments()
.filter(|arg| !arg.is_hide_set())
.map(arg_schema)
.collect();
let subcommands: Vec<Value> = command
.get_subcommands()
.filter(|child| !child.is_hide_set())
.map(|child| {
json!({
"name": child.get_name(),
"about": child.get_about().map(|value| value.to_string())
})
})
.collect();
json!({
"schema_version": "kura.cli.schema.v1",
"name": command.get_name(),
"about": command.get_about().map(|value| value.to_string()),
"args": args,
"subcommands": subcommands,
})
}
fn arg_schema(arg: &clap::Arg) -> Value {
let value_names: Vec<String> = arg
.get_value_names()
.map(|names| names.iter().map(|name| name.to_string()).collect())
.unwrap_or_default();
let default_values: Vec<String> = arg
.get_default_values()
.iter()
.map(|value| value.to_string_lossy().to_string())
.collect();
let mut payload = json!({
"id": arg.get_id().as_str(),
"long": arg.get_long(),
"short": arg.get_short().map(|value| value.to_string()),
"required": arg.is_required_set(),
"action": format!("{:?}", arg.get_action()),
"help": arg.get_help().map(|value| value.to_string()),
"value_names": value_names,
"default_values": default_values,
});
if let Some(num_args) = arg.get_num_args() {
payload["num_args"] = json!(format!("{num_args:?}"));
}
payload
}
async fn resolve_or_exit(api_url: &str, no_auth: bool) -> Option<String> {
if no_auth {
return None;
}
match resolve_token(api_url).await {
Ok(t) => Some(t),
Err(e) => exit_error(&e.to_string(), Some("Run `kura login` or set KURA_API_KEY")),
}
}