use anyhow::{bail, Context, Result};
use serde_json::{json, Value};
use crate::setup::{find_tool, tool_ctx};
use crate::{setup, GlobalOpts};
#[derive(clap::Args, Debug)]
pub struct Args {
#[command(subcommand)]
pub cmd: Option<Cmd>,
}
#[derive(clap::Subcommand, Debug)]
pub enum Cmd {
List {
#[arg(long)]
closed: bool,
#[arg(long)]
json: bool,
},
Add {
#[arg(required = true, num_args = 1..)]
name: Vec<String>,
#[arg(long)]
due: Option<String>,
#[arg(long)]
project: Option<String>,
#[arg(long)]
context: Option<String>,
},
Set {
task: String,
#[arg(long)]
status: Option<String>,
#[arg(long)]
due: Option<String>,
#[arg(long)]
defer: Option<String>,
#[arg(long)]
context: Option<String>,
},
}
pub async fn run(global: &GlobalOpts, args: Args) -> Result<()> {
match args.cmd.unwrap_or(Cmd::List {
closed: false,
json: false,
}) {
Cmd::List { closed, json } => list(global, closed, json).await,
Cmd::Add {
name,
due,
project,
context,
} => add(global, &name.join(" "), due, project, context).await,
Cmd::Set {
task,
status,
due,
defer,
context,
} => set(global, &task, status, due, defer, context).await,
}
}
async fn call(global: &GlobalOpts, tool: &str, args: Value) -> Result<Value> {
let prepared = setup::prepare_tools(global, false).await?;
let found = find_tool(&prepared.registry, tool).with_context(|| {
format!("no knowledge-graph server in this configuration — `{tool}` is not on the tool surface. Is `[[mcp]]` enabled?")
})?;
let out = found.call(args, &tool_ctx(&prepared)).await?;
if out.is_error {
bail!("{}: {}", tool, out.content.trim());
}
serde_json::from_str(&out.content)
.with_context(|| format!("{tool} did not answer with JSON: {}", out.content))
}
async fn list(global: &GlobalOpts, closed: bool, as_json: bool) -> Result<()> {
let board = call(global, "kg_task_list", json!({ "include_closed": closed })).await?;
if as_json {
println!("{board}");
return Ok(());
}
let today = board["today"].as_str().unwrap_or_default();
let items = board["items"].as_array().map(Vec::as_slice).unwrap_or(&[]);
if items.is_empty() {
println!("nothing on the board — `mecha tasks add <what>` captures one");
return Ok(());
}
for t in items {
let due = match t["due_at"].as_str() {
Some(d) if t["overdue"].as_bool().unwrap_or(false) => format!("{d} overdue"),
Some(d) => d.to_string(),
None => "—".into(),
};
println!(
"{:<10} {:<18} {:<52} {}",
t["status"].as_str().unwrap_or("?"),
due,
t["name"].as_str().unwrap_or(""),
t["id"].as_str().unwrap_or(""),
);
let tail: Vec<String> = [
("project", "project"),
("context", "context"),
("waiting_on", "waiting on"),
]
.iter()
.filter_map(|(key, label)| {
t[*key]
.as_str()
.filter(|v| !v.is_empty())
.map(|v| format!("{label} {v}"))
})
.collect();
if !tail.is_empty() {
println!("{:<10} {}", "", tail.join(" · "));
}
}
println!("\n{} task(s) · today is {today}", items.len());
Ok(())
}
async fn add(
global: &GlobalOpts,
name: &str,
due: Option<String>,
project: Option<String>,
context: Option<String>,
) -> Result<()> {
let mut args = json!({ "name": name });
for (key, value) in [("due", due), ("project", project), ("context", context)] {
if let Some(v) = value {
args[key] = json!(v);
}
}
let out = call(global, "kg_task_create", args).await?;
println!(
"{} {}",
out["id"].as_str().unwrap_or("created"),
match out["due_at"].as_str() {
Some(d) => format!("due {d}"),
None => "no due date".into(),
}
);
println!("{name}");
Ok(())
}
async fn set(
global: &GlobalOpts,
task: &str,
status: Option<String>,
due: Option<String>,
defer: Option<String>,
context: Option<String>,
) -> Result<()> {
let mut args = json!({ "task": task });
for (key, value) in [
("status", status),
("due", due),
("defer", defer),
("context", context),
] {
if let Some(v) = value {
args[key] = json!(v);
}
}
if args.as_object().is_some_and(|o| o.len() == 1) {
bail!("nothing to change — pass at least one of --status, --due, --defer, --context");
}
let out = call(global, "kg_task_update", args).await?;
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}