use incurs::cli::Cli;
use incurs::command::{CommandDef, Example, McpAnnotations, McpCommandOptions, TypedContext};
use incurs::output::CommandResult;
use crate::worklog::{
DoneInput, NoteInput, StartInput, UpdateInput, WorkEvent, WorkTask, Worklog,
};
use super::typed_from_result;
fn open_worklog() -> Result<Worklog, CommandResult> {
Worklog::open().map_err(|e| CommandResult::Error {
code: "WORKLOG_ERROR".into(),
message: format!("Failed to open worklog: {e}"),
retryable: false,
exit_code: Some(1),
cta: None,
})
}
fn tool_err(code: &str, message: String) -> CommandResult {
CommandResult::Error {
code: code.into(),
message,
retryable: false,
exit_code: Some(1),
cta: None,
}
}
fn write_mcp() -> McpCommandOptions {
McpCommandOptions {
annotations: Some(McpAnnotations {
read_only_hint: Some(false),
destructive_hint: Some(false),
idempotent_hint: Some(false),
open_world_hint: Some(false),
..Default::default()
}),
..Default::default()
}
}
fn read_mcp() -> McpCommandOptions {
McpCommandOptions {
annotations: Some(McpAnnotations {
read_only_hint: Some(true),
destructive_hint: Some(false),
idempotent_hint: Some(true),
open_world_hint: Some(false),
..Default::default()
}),
..Default::default()
}
}
#[derive(incurs::Args, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct StartArgs {
title: String,
}
#[derive(incurs::Options, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct StartOptions {
body: Option<String>,
project: Option<String>,
cwd: Option<String>,
agent: Option<String>,
session_id: Option<String>,
}
#[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
struct StartOutput {
task: WorkTask,
event: WorkEvent,
}
async fn run_start(ctx: TypedContext<StartArgs, StartOptions, ()>) -> incurs::command::TypedResult<StartOutput> {
let wl = match open_worklog() {
Ok(w) => w,
Err(e) => return typed_from_result(e),
};
match wl.start(StartInput {
title: ctx.args.title,
body: ctx.options.body,
project: ctx.options.project,
cwd: ctx.options.cwd,
agent: ctx.options.agent,
session_id: ctx.options.session_id,
}) {
Ok((task, event)) => incurs::command::TypedResult::ok(StartOutput { task, event }),
Err(e) => typed_from_result(tool_err("WORKLOG_ERROR", e.to_string())),
}
}
fn start_cmd() -> CommandDef {
CommandDef::typed::<StartArgs, StartOptions, (), StartOutput, _, _>("start", run_start)
.description("Start a work task and emit a start event (agents: call when beginning non-trivial work)")
.examples(vec![Example {
command: r#""Fix auth token refresh" --project velo --agent codex --body "Investigating 401s""#
.into(),
description: Some("Start a task with project and body".into()),
}])
.mcp(write_mcp())
.done()
}
#[derive(incurs::Args, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct UpdateArgs {
task: String,
}
#[derive(incurs::Options, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct UpdateOptions {
title: Option<String>,
body: Option<String>,
status: Option<String>,
}
#[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
struct UpdateOutput {
task: WorkTask,
event: WorkEvent,
}
async fn run_update(
ctx: TypedContext<UpdateArgs, UpdateOptions, ()>,
) -> incurs::command::TypedResult<UpdateOutput> {
let wl = match open_worklog() {
Ok(w) => w,
Err(e) => return typed_from_result(e),
};
match wl.update(UpdateInput {
task_id: ctx.args.task,
title: ctx.options.title,
body: ctx.options.body,
status: ctx.options.status,
}) {
Ok((task, event)) => incurs::command::TypedResult::ok(UpdateOutput { task, event }),
Err(e) => typed_from_result(tool_err("WORKLOG_ERROR", e.to_string())),
}
}
fn update_cmd() -> CommandDef {
CommandDef::typed::<UpdateArgs, UpdateOptions, (), UpdateOutput, _, _>("update", run_update)
.description("Record progress on a work task")
.examples(vec![Example {
command: r#"abc123 --body "Found the bad middleware" --status doing"#.into(),
description: Some("Update a task with a progress note".into()),
}])
.mcp(write_mcp())
.done()
}
#[derive(incurs::Args, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct DoneArgs {
task: String,
}
#[derive(incurs::Options, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct DoneOptions {
title: Option<String>,
body: Option<String>,
}
#[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
struct DoneOutput {
task: WorkTask,
event: WorkEvent,
}
async fn run_done(ctx: TypedContext<DoneArgs, DoneOptions, ()>) -> incurs::command::TypedResult<DoneOutput> {
let wl = match open_worklog() {
Ok(w) => w,
Err(e) => return typed_from_result(e),
};
match wl.done(DoneInput {
task_id: ctx.args.task,
title: ctx.options.title,
body: ctx.options.body,
}) {
Ok((task, event)) => incurs::command::TypedResult::ok(DoneOutput { task, event }),
Err(e) => typed_from_result(tool_err("WORKLOG_ERROR", e.to_string())),
}
}
fn done_cmd() -> CommandDef {
CommandDef::typed::<DoneArgs, DoneOptions, (), DoneOutput, _, _>("done", run_done)
.description("Mark a work task done and emit a done event")
.examples(vec![Example {
command: r#"abc123 --body "Shipped fix; tests green""#.into(),
description: Some("Complete a task".into()),
}])
.mcp(write_mcp())
.done()
}
#[derive(incurs::Args, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct NoteArgs {
title: String,
}
#[derive(incurs::Options, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct NoteOptions {
body: Option<String>,
task: Option<String>,
project: Option<String>,
cwd: Option<String>,
agent: Option<String>,
session_id: Option<String>,
}
#[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
struct NoteOutput {
event: WorkEvent,
}
async fn run_note(ctx: TypedContext<NoteArgs, NoteOptions, ()>) -> incurs::command::TypedResult<NoteOutput> {
let wl = match open_worklog() {
Ok(w) => w,
Err(e) => return typed_from_result(e),
};
match wl.note(NoteInput {
title: ctx.args.title,
body: ctx.options.body,
task_id: ctx.options.task,
project: ctx.options.project,
cwd: ctx.options.cwd,
agent: ctx.options.agent,
session_id: ctx.options.session_id,
}) {
Ok(event) => incurs::command::TypedResult::ok(NoteOutput { event }),
Err(e) => typed_from_result(tool_err("WORKLOG_ERROR", e.to_string())),
}
}
fn note_cmd() -> CommandDef {
CommandDef::typed::<NoteArgs, NoteOptions, (), NoteOutput, _, _>("note", run_note)
.description("Add a freeform worklog note (optionally attached to a task)")
.examples(vec![Example {
command: r#""Standup prep" --body "Three PRs ready for review" --project tmppr"#.into(),
description: Some("Standalone note".into()),
}])
.mcp(write_mcp())
.done()
}
#[derive(incurs::Args, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct ListArgs {}
#[derive(incurs::Options, serde::Deserialize, serde::Serialize)]
#[allow(dead_code)]
struct ListOptions {
status: Option<String>,
project: Option<String>,
#[incurs(alias = "n", default = 20)]
limit: i64,
}
#[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
struct ListOutput {
total: usize,
tasks: Vec<WorkTask>,
}
async fn run_list(ctx: TypedContext<ListArgs, ListOptions, ()>) -> incurs::command::TypedResult<ListOutput> {
let wl = match open_worklog() {
Ok(w) => w,
Err(e) => return typed_from_result(e),
};
match wl.list_tasks(
ctx.options.status.as_deref(),
ctx.options.project.as_deref(),
ctx.options.limit,
) {
Ok(tasks) => {
let total = tasks.len();
incurs::command::TypedResult::ok(ListOutput { total, tasks })
}
Err(e) => typed_from_result(tool_err("WORKLOG_ERROR", e.to_string())),
}
}
fn list_cmd() -> CommandDef {
CommandDef::typed::<ListArgs, ListOptions, (), ListOutput, _, _>("list", run_list)
.description("List work tasks (default: most recently updated)")
.examples(vec![Example {
command: "--status doing".into(),
description: Some("List open tasks".into()),
}])
.mcp(read_mcp())
.done()
}
pub fn build_group() -> Cli {
Cli::create("work")
.description(
"Write structured work events for the day timeline.\n\
Agents: start non-trivial work, update on progress, done on completion.",
)
.command("start", start_cmd())
.command("update", update_cmd())
.command("done", done_cmd())
.command("note", note_cmd())
.command("list", list_cmd())
}