Skip to main content

hh_cli/tool/
todo.rs

1use crate::tool::{Tool, ToolResult, ToolSchema, parse_tool_args};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5
6pub struct TodoWriteTool;
7pub struct TodoReadTool;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11enum TodoStatus {
12    Pending,
13    InProgress,
14    Completed,
15    Cancelled,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20enum TodoPriority {
21    High,
22    Medium,
23    Low,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27struct TodoItem {
28    content: String,
29    status: TodoStatus,
30    priority: TodoPriority,
31}
32
33#[derive(Debug, Deserialize)]
34struct TodoWriteArgs {
35    todos: Vec<TodoItem>,
36}
37
38#[derive(Debug, Serialize)]
39struct TodoCounts {
40    total: usize,
41    pending: usize,
42    in_progress: usize,
43    completed: usize,
44    cancelled: usize,
45}
46
47#[derive(Debug, Serialize)]
48struct TodoWriteOutput {
49    todos: Vec<TodoItem>,
50    counts: TodoCounts,
51}
52
53impl TodoCounts {
54    fn from_todos(todos: &[TodoItem]) -> Self {
55        let mut counts = Self {
56            total: todos.len(),
57            pending: 0,
58            in_progress: 0,
59            completed: 0,
60            cancelled: 0,
61        };
62
63        for item in todos {
64            match item.status {
65                TodoStatus::Pending => counts.pending += 1,
66                TodoStatus::InProgress => counts.in_progress += 1,
67                TodoStatus::Completed => counts.completed += 1,
68                TodoStatus::Cancelled => counts.cancelled += 1,
69            }
70        }
71
72        counts
73    }
74}
75
76#[async_trait]
77impl Tool for TodoReadTool {
78    fn schema(&self) -> ToolSchema {
79        ToolSchema {
80            name: "todo_read".to_string(),
81            description: "Read canonical todo list state".to_string(),
82            capability: Some("todo_read".to_string()),
83            mutating: Some(false),
84            parameters: json!({
85                "type": "object",
86                "properties": {},
87                "additionalProperties": false
88            }),
89        }
90    }
91
92    async fn execute(&self, args: Value) -> ToolResult {
93        if !args.is_object() {
94            return ToolResult::error("invalid todo_read args: expected object");
95        }
96
97        let output = TodoWriteOutput {
98            todos: Vec::new(),
99            counts: TodoCounts::from_todos(&[]),
100        };
101
102        ToolResult::ok_json_typed_serializable(
103            "todo list snapshot",
104            "application/vnd.hh.todo+json",
105            &output,
106        )
107    }
108}
109
110#[async_trait]
111impl Tool for TodoWriteTool {
112    fn schema(&self) -> ToolSchema {
113        ToolSchema {
114            name: "todo_write".to_string(),
115            description: "Set canonical todo list state".to_string(),
116            capability: Some("todo_write".to_string()),
117            mutating: Some(true),
118            parameters: json!({
119                "type": "object",
120                "properties": {
121                    "todos": {
122                        "type": "array",
123                        "items": {
124                            "type": "object",
125                            "properties": {
126                                "content": {"type": "string"},
127                                "status": {
128                                    "type": "string",
129                                    "enum": ["pending", "in_progress", "completed", "cancelled"]
130                                },
131                                "priority": {
132                                    "type": "string",
133                                    "enum": ["high", "medium", "low"]
134                                }
135                            },
136                            "required": ["content", "status", "priority"]
137                        }
138                    }
139                },
140                "required": ["todos"]
141            }),
142        }
143    }
144
145    async fn execute(&self, args: Value) -> ToolResult {
146        let parsed: TodoWriteArgs = match parse_tool_args(args, "todo_write") {
147            Ok(value) => value,
148            Err(err) => return err,
149        };
150
151        for item in &parsed.todos {
152            if item.content.trim().is_empty() {
153                return ToolResult::error("todo content must not be empty");
154            }
155        }
156
157        let todos = parsed.todos;
158        let output = TodoWriteOutput {
159            counts: TodoCounts::from_todos(&todos),
160            todos,
161        };
162
163        ToolResult::ok_json_typed_serializable(
164            "todo list updated",
165            "application/vnd.hh.todo+json",
166            &output,
167        )
168    }
169}