use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
#[cfg(feature = "org")]
use std::process;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
#[derive(Parser)]
#[command(name = "reasonkit")]
#[command(author = "ReasonKit Team <team@reasonkit.sh>")]
#[command(version)]
#[command(about = "The Reasoning Engine — Auditable Reasoning for Production AI")]
#[command(long_about = r#"
ReasonKit — Complete Suite for Structured AI Reasoning
This unified CLI provides access to all ReasonKit components:
REASONING (reasonkit-core):
reasonkit think Execute ThinkTools protocols
reasonkit verify Triangulate claims with 3+ sources
MEMORY (reasonkit-mem):
reasonkit mem Memory and knowledge base operations
reasonkit rag Retrieval-augmented generation
WEB (reasonkit-web):
reasonkit web Browser automation and capture
reasonkit serve Start MCP server
ORG (reasonkit-org):
reasonkit task Taskwarrior passthrough
reasonkit time Timewarrior passthrough
reasonkit org Operational tooling (doctor/status)
reasonkit job Jobs-to-be-Done management
EXAMPLES:
# Quick reasoning analysis
reasonkit think --profile quick "Is this a good investment?"
# Deep analysis with full protocol chain
reasonkit think --profile paranoid "Validate this architecture"
# Search knowledge base
reasonkit mem search "machine learning fundamentals"
# Start unified MCP server
reasonkit serve
WEBSITE: https://reasonkit.sh
DOCS: https://docs.rs/reasonkit
"#)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[arg(short, long, default_value = "text", global = true)]
format: OutputFormat,
#[command(subcommand)]
command: Commands,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum OutputFormat {
Text,
Json,
}
#[derive(Subcommand)]
enum Commands {
#[cfg(feature = "core")]
#[command(alias = "t")]
Think {
query: String,
#[arg(short, long)]
protocol: Option<String>,
#[arg(long, default_value = "balanced")]
profile: String,
#[arg(long, default_value = "anthropic")]
provider: String,
#[arg(short, long)]
model: Option<String>,
#[arg(long)]
mock: bool,
#[arg(long)]
list: bool,
},
#[cfg(feature = "core")]
#[command(alias = "v")]
Verify {
claim: String,
#[arg(short, long, default_value = "3")]
sources: usize,
},
#[cfg(feature = "mem")]
#[command(alias = "m")]
Mem {
#[command(subcommand)]
action: MemAction,
},
#[cfg(feature = "mem")]
Rag {
query: String,
#[arg(short = 'k', long, default_value = "5")]
top_k: usize,
#[arg(long)]
hybrid: bool,
},
#[cfg(feature = "web")]
#[command(alias = "w")]
Web {
#[command(subcommand)]
action: WebAction,
},
#[cfg(feature = "org")]
Task {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[cfg(feature = "org")]
Time {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[cfg(feature = "org")]
#[command(alias = "o")]
Org {
#[command(subcommand)]
action: OrgAction,
},
#[cfg(feature = "org")]
#[command(alias = "j")]
Job {
#[command(subcommand)]
action: JobAction,
},
Serve {
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(short, long, default_value = "8080")]
port: u16,
#[arg(long, default_value = "full")]
mode: ServerMode,
},
Version,
Completions {
#[arg(value_enum)]
shell: Shell,
},
#[command(alias = "setup")]
Init {
#[arg(long)]
non_interactive: bool,
#[arg(long)]
force: bool,
},
#[command(alias = "health")]
Status {
#[arg(short, long)]
detailed: bool,
#[arg(long, default_value = "text")]
output: String,
},
Demo {
#[arg(long, default_value = "reasoning")]
demo_type: DemoType,
#[arg(long)]
mock: bool,
},
}
#[cfg(feature = "mem")]
#[derive(Subcommand)]
enum MemAction {
Docs {
#[command(subcommand)]
action: DocsAction,
},
}
#[cfg(feature = "mem")]
#[derive(Subcommand)]
enum DocsAction {
Add {
name: String,
start_url: String,
allowed_prefixes: Option<Vec<String>>,
},
List,
Query {
query: String,
#[arg(long)]
docset: Option<String>,
#[arg(short = 'k', long, default_value = "8")]
top_k: usize,
#[arg(long)]
json: bool,
},
Remove {
docset_id: String,
#[arg(long)]
keep_index: bool,
},
Refresh {
#[arg(long)]
due: bool,
#[arg(long)]
max_pages: Option<usize>,
#[arg(long)]
concurrency: Option<usize>,
#[arg(long)]
timeout_secs: Option<u64>,
},
}
#[cfg(feature = "web")]
#[derive(Subcommand)]
enum WebAction {
Capture {
url: String,
#[arg(long)]
screenshot: bool,
},
Extract {
url: String,
#[arg(long, default_value = "text")]
mode: String,
},
}
#[cfg(feature = "org")]
#[derive(Subcommand)]
enum OrgAction {
Doctor,
Status,
}
#[cfg(feature = "org")]
#[derive(Subcommand)]
enum JobAction {
Add {
statement: String,
},
Outcome {
job_id: String,
description: String,
#[arg(short, long, default_value = "functional")]
outcome_type: String,
},
Link {
job_id: String,
tasks: Vec<String>,
},
List,
Progress {
job_id: String,
},
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum ServerMode {
Core,
Web,
Full,
}
#[derive(Clone, Copy, Debug, Default, clap::ValueEnum)]
enum DemoType {
#[default]
Reasoning,
Memory,
Web,
All,
}
fn setup_logging(verbosity: u8) {
let level = match verbosity {
0 => Level::WARN,
1 => Level::INFO,
2 => Level::DEBUG,
_ => Level::TRACE,
};
let subscriber = FmtSubscriber::builder()
.with_max_level(level)
.with_target(false)
.with_thread_ids(false)
.with_file(verbosity >= 3)
.with_line_number(verbosity >= 3)
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
}
#[cfg(feature = "core")]
#[allow(clippy::too_many_arguments)]
async fn handle_think(
query: String,
protocol: Option<String>,
profile: String,
_provider: String,
_model: Option<String>,
mock: bool,
list: bool,
format: OutputFormat,
) -> anyhow::Result<()> {
use reasonkit_core::thinktool::{ProtocolExecutor, ProtocolInput};
let executor = if mock {
ProtocolExecutor::mock()?
} else {
ProtocolExecutor::new()?
};
if list {
println!("Available Protocols:");
for p in executor.list_protocols() {
println!(" - {}", p);
}
println!("\nAvailable Profiles:");
for p in executor.list_profiles() {
println!(" - {}", p);
}
return Ok(());
}
let input = ProtocolInput::query(&query);
let output = if let Some(proto) = protocol {
executor.execute(&proto, input).await?
} else {
executor.execute_profile(&profile, input).await?
};
match format {
OutputFormat::Text => {
println!("Thinking Process:");
for step in &output.steps {
println!("\n[{}] {}", step.step_id, step.as_text().unwrap_or(""));
}
println!("\nConfidence: {:.2}", output.confidence);
}
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&output)?);
}
}
Ok(())
}
#[cfg(feature = "core")]
async fn handle_verify(claim: String, sources: usize) -> anyhow::Result<()> {
println!("Verifying claim: {}", claim);
println!("Minimum sources required: {}", sources);
println!("\n[Not yet implemented - use rk-core verify]");
Ok(())
}
#[cfg(feature = "mem")]
async fn handle_mem(action: MemAction, format: OutputFormat) -> anyhow::Result<()> {
match action {
MemAction::Docs {
action: _docs_action,
} => {
println!("Memory operations via rk-mem (docset management)");
println!("Commands: add, list, query, remove, refresh");
println!("\nFor detailed usage: rk-mem docs --help");
println!(" Example: rk mem docs list");
println!(" rk mem docs add react https://react.dev/reference/");
}
}
let _ = format; Ok(())
}
#[cfg(feature = "mem")]
async fn handle_rag(query: String, top_k: usize, hybrid: bool) -> anyhow::Result<()> {
println!(
"RAG Query: {} (top_k: {}, hybrid: {})",
query, top_k, hybrid
);
println!("\n[Not yet implemented - use rk-core rag]");
Ok(())
}
#[cfg(feature = "web")]
async fn handle_web(action: WebAction) -> anyhow::Result<()> {
match action {
WebAction::Capture { url, screenshot } => {
println!("Capturing URL: {} (screenshot: {})", url, screenshot);
println!("\n[Not yet implemented - use rk-web capture]");
}
WebAction::Extract { url, mode } => {
println!("Extracting from URL: {} (mode: {})", url, mode);
println!("\n[Not yet implemented - use rk-web extract]");
}
}
Ok(())
}
#[allow(unreachable_code)]
async fn handle_serve(host: String, port: u16, mode: ServerMode) -> anyhow::Result<()> {
info!("Starting ReasonKit server on {}:{}", host, port);
info!("Mode: {:?}", mode);
match mode {
#[cfg(all(feature = "core", feature = "mcp-server-pro"))]
ServerMode::Core | ServerMode::Full => {
info!("Starting Core MCP server...");
reasonkit_core::mcp::server::run_server().await?;
}
#[cfg(all(feature = "core", not(feature = "mcp-server-pro")))]
ServerMode::Core | ServerMode::Full => {
anyhow::bail!("MCP server requires mcp-server-pro feature (Pro license). Contact sales@reasonkit.sh");
}
#[cfg(not(feature = "core"))]
ServerMode::Core => {
anyhow::bail!("Core feature not enabled. Rebuild with --features core");
}
#[cfg(feature = "web")]
ServerMode::Web => {
info!("Starting Web MCP server...");
println!("[Web server not yet integrated]");
}
#[cfg(not(feature = "web"))]
ServerMode::Web => {
anyhow::bail!("Web feature not enabled. Rebuild with --features web");
}
#[cfg(not(feature = "core"))]
ServerMode::Full => {
anyhow::bail!("Full mode requires core feature. Rebuild with --features full");
}
}
Ok(())
}
fn handle_version(format: OutputFormat) -> anyhow::Result<()> {
let info = reasonkit::version_info();
match format {
OutputFormat::Text => {
println!("ReasonKit Suite v{}", info.reasonkit);
println!();
println!("Components:");
if let Some(v) = &info.core {
println!(" reasonkit-core: v{}", v);
} else {
println!(" reasonkit-core: not enabled");
}
if let Some(v) = &info.mem {
println!(" reasonkit-mem: v{}", v);
} else {
println!(" reasonkit-mem: not enabled");
}
if let Some(v) = &info.web {
println!(" reasonkit-web: v{}", v);
} else {
println!(" reasonkit-web: not enabled");
}
println!();
println!("Website: https://reasonkit.sh");
}
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&info)?);
}
}
Ok(())
}
fn handle_init(non_interactive: bool, force: bool) -> anyhow::Result<()> {
use std::io::{self, BufRead, Write};
println!();
println!(" ReasonKit Setup Wizard");
println!(" ----------------------");
println!();
let config_dir = dirs::config_dir()
.map(|d| d.join("reasonkit"))
.unwrap_or_else(|| std::path::PathBuf::from(".reasonkit"));
let config_file = config_dir.join("config.toml");
if config_file.exists() && !force {
println!(" ReasonKit is already configured at:");
println!(" {}", config_file.display());
println!();
println!(" Run with --force to reconfigure.");
return Ok(());
}
std::fs::create_dir_all(&config_dir)?;
println!(" Detecting environment...");
println!();
let anthropic_key = std::env::var("ANTHROPIC_API_KEY").ok();
let openai_key = std::env::var("OPENAI_API_KEY").ok();
let gemini_key = std::env::var("GOOGLE_API_KEY")
.or_else(|_| std::env::var("GEMINI_API_KEY"))
.ok();
let mut default_provider = "mock";
if anthropic_key.is_some() {
println!(" [x] ANTHROPIC_API_KEY detected");
default_provider = "anthropic";
} else {
println!(" [ ] ANTHROPIC_API_KEY not set");
}
if openai_key.is_some() {
println!(" [x] OPENAI_API_KEY detected");
if default_provider == "mock" {
default_provider = "openai";
}
} else {
println!(" [ ] OPENAI_API_KEY not set");
}
if gemini_key.is_some() {
println!(" [x] GOOGLE_API_KEY detected");
if default_provider == "mock" {
default_provider = "gemini";
}
} else {
println!(" [ ] GOOGLE_API_KEY not set");
}
println!();
println!(" Components available:");
#[cfg(feature = "core")]
println!(" [x] reasonkit-core (reasoning engine)");
#[cfg(not(feature = "core"))]
println!(" [ ] reasonkit-core (not compiled)");
#[cfg(feature = "mem")]
println!(" [x] reasonkit-mem (memory/RAG)");
#[cfg(not(feature = "mem"))]
println!(" [ ] reasonkit-mem (not compiled)");
#[cfg(feature = "web")]
println!(" [x] reasonkit-web (browser automation)");
#[cfg(not(feature = "web"))]
println!(" [ ] reasonkit-web (not compiled)");
println!();
let provider: String;
let profile: String;
if non_interactive {
provider = default_provider.to_string();
profile = "balanced".to_string();
println!(" Using defaults (non-interactive mode):");
} else {
let stdin = io::stdin();
let mut stdout = io::stdout();
print!(" Default LLM provider [{}]: ", default_provider);
stdout.flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let input_provider = input.trim().to_string();
provider = if input_provider.is_empty() {
default_provider.to_string()
} else {
input_provider
};
print!(" Default reasoning profile [balanced]: ");
stdout.flush()?;
input.clear();
stdin.lock().read_line(&mut input)?;
let input_profile = input.trim().to_string();
profile = if input_profile.is_empty() {
"balanced".to_string()
} else {
input_profile
};
println!();
println!(" Configuration:");
}
println!(" Provider: {}", provider);
println!(" Profile: {}", profile);
println!();
let config_content = format!(
r#"# ReasonKit Configuration
# Generated by `rk init`
[defaults]
provider = "{}"
profile = "{}"
[providers.anthropic]
# model = "claude-opus-4-5"
[providers.openai]
# model = "gpt-5.2"
[providers.gemini]
# model = "gemini-3.0-pro"
"#,
provider, profile
);
std::fs::write(&config_file, config_content)?;
println!(" Configuration saved to:");
println!(" {}", config_file.display());
println!();
println!(" TIP: Enable shell completions with:");
println!(" rk completions bash >> ~/.bashrc");
println!(" rk completions zsh >> ~/.zshrc");
println!(" rk completions fish > ~/.config/fish/completions/rk.fish");
println!();
println!(" Get started:");
println!(" rk demo --mock # Try a demo (no API key needed)");
println!(" rk think \"Your query\" # Run reasoning analysis");
println!(" rk status # Check system health");
println!();
Ok(())
}
fn handle_status(detailed: bool, output: &str) -> anyhow::Result<()> {
use serde::Serialize;
#[derive(Serialize)]
struct StatusReport {
version: String,
components: ComponentStatus,
environment: EnvStatus,
health: HealthStatus,
}
#[derive(Serialize)]
struct ComponentStatus {
core: bool,
mem: bool,
web: bool,
}
#[derive(Serialize)]
struct EnvStatus {
anthropic_api_key: bool,
openai_api_key: bool,
google_api_key: bool,
config_exists: bool,
}
#[derive(Serialize)]
struct HealthStatus {
overall: String,
issues: Vec<String>,
}
let config_dir = dirs::config_dir()
.map(|d| d.join("reasonkit"))
.unwrap_or_else(|| std::path::PathBuf::from(".reasonkit"));
let config_exists = config_dir.join("config.toml").exists();
let anthropic_key = std::env::var("ANTHROPIC_API_KEY").is_ok();
let openai_key = std::env::var("OPENAI_API_KEY").is_ok();
let google_key =
std::env::var("GOOGLE_API_KEY").is_ok() || std::env::var("GEMINI_API_KEY").is_ok();
let mut issues = Vec::new();
#[cfg(feature = "core")]
let core_enabled = true;
#[cfg(not(feature = "core"))]
let core_enabled = false;
#[cfg(feature = "mem")]
let mem_enabled = true;
#[cfg(not(feature = "mem"))]
let mem_enabled = false;
#[cfg(feature = "web")]
let web_enabled = true;
#[cfg(not(feature = "web"))]
let web_enabled = false;
if !core_enabled {
issues.push("Core component not enabled".to_string());
}
if !anthropic_key && !openai_key && !google_key {
issues.push("No LLM API keys configured".to_string());
}
if !config_exists {
issues.push("Not configured (run: rk init)".to_string());
}
let overall = if issues.is_empty() {
"healthy".to_string()
} else if issues.len() <= 2 {
"degraded".to_string()
} else {
"unhealthy".to_string()
};
let report = StatusReport {
version: reasonkit::VERSION.to_string(),
components: ComponentStatus {
core: core_enabled,
mem: mem_enabled,
web: web_enabled,
},
environment: EnvStatus {
anthropic_api_key: anthropic_key,
openai_api_key: openai_key,
google_api_key: google_key,
config_exists,
},
health: HealthStatus {
overall: overall.clone(),
issues: issues.clone(),
},
};
if output == "json" {
println!("{}", serde_json::to_string_pretty(&report)?);
return Ok(());
}
println!();
println!(" ReasonKit Status");
println!(" ----------------");
println!();
let health_icon = match overall.as_str() {
"healthy" => "[OK]",
"degraded" => "[!!]",
_ => "[XX]",
};
println!(" Health: {} {}", health_icon, overall);
println!();
println!(" Version: {}", report.version);
println!();
println!(" Components:");
let icon = |b: bool| if b { "[x]" } else { "[ ]" };
println!(" {} reasonkit-core", icon(report.components.core));
println!(" {} reasonkit-mem", icon(report.components.mem));
println!(" {} reasonkit-web", icon(report.components.web));
println!();
println!(" Environment:");
println!(
" {} ANTHROPIC_API_KEY",
icon(report.environment.anthropic_api_key)
);
println!(
" {} OPENAI_API_KEY",
icon(report.environment.openai_api_key)
);
println!(
" {} GOOGLE_API_KEY",
icon(report.environment.google_api_key)
);
println!(
" {} Configuration file",
icon(report.environment.config_exists)
);
println!();
if detailed {
println!(" Paths:");
println!(" Config: {}", config_dir.display());
if let Some(data_dir) = dirs::data_dir() {
println!(" Data: {}", data_dir.join("reasonkit").display());
}
println!();
}
if !issues.is_empty() {
println!(" Issues:");
for issue in &issues {
println!(" - {}", issue);
}
println!();
}
Ok(())
}
#[cfg(feature = "core")]
async fn handle_demo(demo_type: DemoType, mock: bool) -> anyhow::Result<()> {
use reasonkit_core::thinktool::{ProtocolExecutor, ProtocolInput};
println!();
println!(" ReasonKit Demo");
println!(" --------------");
println!();
match demo_type {
DemoType::Reasoning | DemoType::All => {
println!(" [Reasoning Demo]");
println!();
let executor = if mock {
println!(" Using mock LLM (no API key required)");
ProtocolExecutor::mock()?
} else {
ProtocolExecutor::new()?
};
let query = "Should a startup prioritize growth or profitability in year one?";
println!(" Query: {}", query);
println!();
let input = ProtocolInput::query(query);
let output = executor.execute_profile("quick", input).await?;
println!(" Reasoning Chain:");
for (i, step) in output.steps.iter().enumerate() {
let text = step.as_text().unwrap_or("(no output)");
let display: String = text.chars().take(200).collect();
println!(" Step {}: {}...", i + 1, display);
}
println!();
println!(" Confidence: {:.0}%", output.confidence * 100.0);
println!();
}
DemoType::Memory => {
#[cfg(feature = "mem")]
{
println!(" [Memory Demo]");
println!(" (Memory operations require Qdrant - skipping live demo)");
println!();
println!(" Example commands:");
println!(" rk mem search \"machine learning\"");
println!(" rk mem ingest ./documents/");
println!(" rk rag \"What is gradient descent?\"");
}
#[cfg(not(feature = "mem"))]
{
println!(" [Memory Demo]");
println!(" Memory component not enabled.");
println!(" Rebuild with: cargo install reasonkit --features mem");
}
println!();
}
DemoType::Web => {
#[cfg(feature = "web")]
{
println!(" [Web Demo]");
println!(" (Web operations require Chrome - skipping live demo)");
println!();
println!(" Example commands:");
println!(" rk web capture https://example.com");
println!(" rk web extract https://example.com --mode text");
}
#[cfg(not(feature = "web"))]
{
println!(" [Web Demo]");
println!(" Web component not enabled.");
println!(" Rebuild with: cargo install reasonkit --features web");
}
println!();
}
#[allow(unreachable_patterns)]
_ => {}
}
if matches!(demo_type, DemoType::All) {
if !matches!(demo_type, DemoType::Memory) {
Box::pin(handle_demo(DemoType::Memory, mock)).await?;
}
if !matches!(demo_type, DemoType::Web) {
Box::pin(handle_demo(DemoType::Web, mock)).await?;
}
}
println!(" Learn more: https://reasonkit.sh/docs");
println!();
Ok(())
}
#[cfg(not(feature = "core"))]
async fn handle_demo(_demo_type: DemoType, _mock: bool) -> anyhow::Result<()> {
println!();
println!(" ReasonKit Demo");
println!(" --------------");
println!();
println!(" Demo requires the 'core' feature.");
println!(" Rebuild with: cargo install reasonkit --features core");
println!();
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
setup_logging(cli.verbose);
info!("ReasonKit v{}", reasonkit::VERSION);
match cli.command {
#[cfg(feature = "core")]
Commands::Think {
query,
protocol,
profile,
provider,
model,
mock,
list,
} => {
handle_think(
query, protocol, profile, provider, model, mock, list, cli.format,
)
.await?;
}
#[cfg(feature = "core")]
Commands::Verify { claim, sources } => {
handle_verify(claim, sources).await?;
}
#[cfg(feature = "mem")]
Commands::Mem { action } => {
handle_mem(action, cli.format).await?;
}
#[cfg(feature = "mem")]
Commands::Rag {
query,
top_k,
hybrid,
} => {
handle_rag(query, top_k, hybrid).await?;
}
#[cfg(feature = "web")]
Commands::Web { action } => {
handle_web(action).await?;
}
#[cfg(feature = "org")]
Commands::Task { args } => {
let status = reasonkit_org::integration::taskwarrior::run(&args)?;
if !status.success() {
process::exit(exit_code(status));
}
}
#[cfg(feature = "org")]
Commands::Time { args } => {
let status = reasonkit_org::integration::timewarrior::run(&args)?;
if !status.success() {
process::exit(exit_code(status));
}
}
#[cfg(feature = "org")]
Commands::Org { action } => match action {
OrgAction::Doctor => {
let report = reasonkit_org::integration::doctor::run()?;
print!("{report}");
if !report.ok() {
process::exit(1);
}
}
OrgAction::Status => {
let report = reasonkit_org::integration::status::run()?;
print!("{report}");
if !report.ok() {
process::exit(1);
}
}
},
#[cfg(feature = "org")]
Commands::Job { action } => {
handle_job(action)?;
}
Commands::Serve { host, port, mode } => {
handle_serve(host, port, mode).await?;
}
Commands::Version => {
handle_version(cli.format)?;
}
Commands::Completions { shell } => {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "reasonkit", &mut std::io::stdout());
}
Commands::Init {
non_interactive,
force,
} => {
handle_init(non_interactive, force)?;
}
Commands::Status { detailed, output } => {
handle_status(detailed, &output)?;
}
Commands::Demo { demo_type, mock } => {
handle_demo(demo_type, mock).await?;
}
}
Ok(())
}
#[cfg(feature = "org")]
fn exit_code(status: std::process::ExitStatus) -> i32 {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status
.code()
.or_else(|| status.signal().map(|s| 128 + s))
.unwrap_or(1)
}
#[cfg(not(unix))]
{
status.code().unwrap_or(1)
}
}
#[cfg(feature = "org")]
fn handle_job(action: JobAction) -> anyhow::Result<()> {
use anyhow::Context;
use reasonkit_org::jtbd::{Job, JobStore, Outcome};
use reasonkit_org::storage::Storage;
use reasonkit_org::Uuid;
let storage = Storage::new(Storage::default_location()?)?;
let store = JobStore::open(storage.jobs_db().parent().context("invalid jobs db path")?)?;
match action {
JobAction::Add { statement } => {
let job = Job::new(statement);
store.add_job(&job)?;
println!("Created job: {} - {}", job.uuid, job.statement);
}
JobAction::Outcome {
job_id,
description,
outcome_type,
} => {
let job_uuid =
Uuid::parse_str(&job_id).with_context(|| format!("invalid job id: {job_id}"))?;
let outcome = match outcome_type.as_str() {
"functional" => Outcome::functional(&description),
"emotional" => Outcome::emotional(&description),
"social" => Outcome::social(&description),
other => {
anyhow::bail!("invalid outcome type: {other} (use functional|emotional|social)")
}
};
store.add_outcome(job_uuid, &outcome)?;
println!(
"Added outcome to job {}: {:?} - {}",
job_uuid, outcome.outcome_type, outcome.description
);
}
JobAction::Link { job_id, tasks } => {
let job_uuid =
Uuid::parse_str(&job_id).with_context(|| format!("invalid job id: {job_id}"))?;
let mut resolved = Vec::with_capacity(tasks.len());
for selector in tasks {
resolved.push(resolve_task_uuid(&selector)?);
}
for task_uuid in &resolved {
store.link_task(job_uuid, *task_uuid)?;
}
println!("Linked {} tasks to job {}", resolved.len(), job_uuid);
}
JobAction::List => {
let jobs = store.list_jobs()?;
if jobs.is_empty() {
println!("No jobs.");
return Ok(());
}
println!("Jobs:");
for job in jobs {
println!(
" - {} [{:?}] {:>3.0}% {}",
job.uuid,
job.status,
job.progress * 100.0,
job.statement
);
}
}
JobAction::Progress { job_id } => {
let job_uuid =
Uuid::parse_str(&job_id).with_context(|| format!("invalid job id: {job_id}"))?;
let Some(job) = store.get_job(job_uuid)? else {
anyhow::bail!("job not found: {job_uuid}");
};
println!(
"Job {} [{:?}] {:>3.0}% {}",
job.uuid,
job.status,
job.progress * 100.0,
job.statement
);
if !job.outcomes.is_empty() {
println!("Outcomes:");
for outcome in &job.outcomes {
println!(
" - [{}] {:?}: {}",
if outcome.achieved { "x" } else { " " },
outcome.outcome_type,
outcome.description
);
}
}
if !job.tasks.is_empty() {
println!("Linked tasks:");
for task_uuid in &job.tasks {
println!(" - {}", task_uuid);
}
}
}
}
Ok(())
}
#[cfg(feature = "org")]
fn resolve_task_uuid(selector: &str) -> anyhow::Result<reasonkit_org::Uuid> {
use anyhow::Context;
if let Ok(uuid) = reasonkit_org::Uuid::parse_str(selector) {
return Ok(uuid);
}
let id: u32 = selector.parse().with_context(|| {
format!("invalid task selector '{selector}' (expected uuid or numeric id)")
})?;
let task = reasonkit_org::integration::taskwarrior_export::export_by_id(id)?
.ok_or_else(|| anyhow::anyhow!("task not found for id {}", id))?;
Ok(task.uuid)
}