1use super::{Tool, ToolResult};
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8use std::path::PathBuf;
9
10const TODO_FILE: &str = ".codetether-todos.json";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TodoItem {
14 pub id: String,
15 pub content: String,
16 pub status: TodoStatus,
17 #[serde(default)]
18 pub priority: Priority,
19 #[serde(default)]
20 pub created_at: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
24#[serde(rename_all = "lowercase")]
25pub enum TodoStatus {
26 #[default]
27 Pending,
28 #[serde(alias = "in_progress")]
29 #[serde(alias = "inprogress")]
30 InProgress,
31 Done,
32 Blocked,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
36#[serde(rename_all = "lowercase")]
37pub enum Priority {
38 Low,
39 #[default]
40 Medium,
41 High,
42 Critical,
43}
44
45pub struct TodoReadTool {
46 root: PathBuf,
47}
48
49pub struct TodoWriteTool {
50 root: PathBuf,
51}
52
53impl Default for TodoReadTool {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl Default for TodoWriteTool {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl TodoReadTool {
66 pub fn new() -> Self {
67 Self {
68 root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
69 }
70 }
71
72 pub fn with_root(root: PathBuf) -> Self {
73 Self { root }
74 }
75
76 fn load_todos(&self) -> Result<Vec<TodoItem>> {
77 let path = self.root.join(TODO_FILE);
78 if !path.exists() {
79 return Ok(Vec::new());
80 }
81 let content = std::fs::read_to_string(&path)?;
82 Ok(serde_json::from_str(&content)?)
83 }
84}
85
86impl TodoWriteTool {
87 pub fn new() -> Self {
88 Self {
89 root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
90 }
91 }
92
93 pub fn with_root(root: PathBuf) -> Self {
94 Self { root }
95 }
96
97 fn load_todos(&self) -> Result<Vec<TodoItem>> {
98 let path = self.root.join(TODO_FILE);
99 if !path.exists() {
100 return Ok(Vec::new());
101 }
102 let content = std::fs::read_to_string(&path)?;
103 Ok(serde_json::from_str(&content)?)
104 }
105
106 fn save_todos(&self, todos: &[TodoItem]) -> Result<()> {
107 let path = self.root.join(TODO_FILE);
108 let content = serde_json::to_string_pretty(todos)?;
109 std::fs::write(&path, content)?;
110 Ok(())
111 }
112
113 fn generate_id(&self) -> String {
114 use std::time::{SystemTime, UNIX_EPOCH};
115 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
116 format!("todo_{}", now.as_millis())
117 }
118}
119
120#[derive(Deserialize)]
121struct ReadParams {
122 #[serde(default)]
123 status: Option<String>,
124 #[serde(default)]
125 priority: Option<String>,
126}
127
128#[derive(Deserialize)]
129struct WriteParams {
130 action: String, #[serde(default)]
132 id: Option<String>,
133 #[serde(default)]
134 content: Option<String>,
135 #[serde(default)]
136 status: Option<String>,
137 #[serde(default)]
138 priority: Option<String>,
139}
140
141#[async_trait]
142impl Tool for TodoReadTool {
143 fn id(&self) -> &str {
144 "todoread"
145 }
146 fn name(&self) -> &str {
147 "Todo Read"
148 }
149 fn description(&self) -> &str {
150 "Read todo items. Filter by status (pending/in_progress/done/blocked) or priority (low/medium/high/critical)."
151 }
152 fn parameters(&self) -> Value {
153 json!({
154 "type": "object",
155 "properties": {
156 "status": {"type": "string", "enum": ["pending", "in_progress", "inprogress", "done", "blocked"]},
157 "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]}
158 }
159 })
160 }
161
162 async fn execute(&self, params: Value) -> Result<ToolResult> {
163 let p: ReadParams = serde_json::from_value(params).unwrap_or(ReadParams {
164 status: None,
165 priority: None,
166 });
167
168 let todos = self.load_todos()?;
169
170 let filtered: Vec<&TodoItem> = todos
171 .iter()
172 .filter(|t| {
173 if let Some(ref status) = p.status {
174 let expected = match status.as_str() {
175 "pending" => TodoStatus::Pending,
176 "in_progress" | "inprogress" => TodoStatus::InProgress,
177 "done" => TodoStatus::Done,
178 "blocked" => TodoStatus::Blocked,
179 _ => return true,
180 };
181 if t.status != expected {
182 return false;
183 }
184 }
185 if let Some(ref priority) = p.priority {
186 let expected = match priority.as_str() {
187 "low" => Priority::Low,
188 "medium" => Priority::Medium,
189 "high" => Priority::High,
190 "critical" => Priority::Critical,
191 _ => return true,
192 };
193 if t.priority != expected {
194 return false;
195 }
196 }
197 true
198 })
199 .collect();
200
201 if filtered.is_empty() {
202 return Ok(ToolResult::success("No todos found".to_string()));
203 }
204
205 let output = filtered
206 .iter()
207 .map(|t| {
208 let status_icon = match t.status {
209 TodoStatus::Pending => "○",
210 TodoStatus::InProgress => "◐",
211 TodoStatus::Done => "●",
212 TodoStatus::Blocked => "✗",
213 };
214 let priority_label = match t.priority {
215 Priority::Low => "[low]",
216 Priority::Medium => "",
217 Priority::High => "[HIGH]",
218 Priority::Critical => "[CRITICAL]",
219 };
220 format!("{} {} {} {}", status_icon, t.id, priority_label, t.content)
221 })
222 .collect::<Vec<_>>()
223 .join("\n");
224
225 Ok(ToolResult::success(output).with_metadata("count", json!(filtered.len())))
226 }
227}
228
229#[async_trait]
230impl Tool for TodoWriteTool {
231 fn id(&self) -> &str {
232 "todowrite"
233 }
234 fn name(&self) -> &str {
235 "Todo Write"
236 }
237 fn description(&self) -> &str {
238 "Manage todo items: add, update, delete, or clear todos."
239 }
240 fn parameters(&self) -> Value {
241 json!({
242 "type": "object",
243 "properties": {
244 "action": {"type": "string", "enum": ["add", "update", "delete", "clear"], "description": "Action to perform"},
245 "id": {"type": "string", "description": "Todo ID (for update/delete)"},
246 "content": {"type": "string", "description": "Todo content (for add/update)"},
247 "status": {"type": "string", "enum": ["pending", "in_progress", "inprogress", "done", "blocked"]},
248 "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]}
249 },
250 "required": ["action"]
251 })
252 }
253
254 async fn execute(&self, params: Value) -> Result<ToolResult> {
255 let p: WriteParams = serde_json::from_value(params).context("Invalid params")?;
256 let mut todos = self.load_todos()?;
257
258 match p.action.as_str() {
259 "add" => {
260 let content = p
261 .content
262 .ok_or_else(|| anyhow::anyhow!("content required for add"))?;
263 let status = p
264 .status
265 .map(|s| match s.as_str() {
266 "in_progress" | "inprogress" => TodoStatus::InProgress,
267 "done" => TodoStatus::Done,
268 "blocked" => TodoStatus::Blocked,
269 _ => TodoStatus::Pending,
270 })
271 .unwrap_or_default();
272 let priority = p
273 .priority
274 .map(|s| match s.as_str() {
275 "low" => Priority::Low,
276 "high" => Priority::High,
277 "critical" => Priority::Critical,
278 _ => Priority::Medium,
279 })
280 .unwrap_or_default();
281
282 let id = self.generate_id();
283 todos.push(TodoItem {
284 id: id.clone(),
285 content,
286 status,
287 priority,
288 created_at: Some(chrono::Utc::now().to_rfc3339()),
289 });
290 self.save_todos(&todos)?;
291 Ok(ToolResult::success(format!("Added todo: {}", id)))
292 }
293 "update" => {
294 let id =
295 p.id.ok_or_else(|| anyhow::anyhow!("id required for update"))?;
296 let todo = todos
297 .iter_mut()
298 .find(|t| t.id == id)
299 .ok_or_else(|| anyhow::anyhow!("Todo not found: {}", id))?;
300
301 if let Some(content) = p.content {
302 todo.content = content;
303 }
304 if let Some(status) = p.status {
305 todo.status = match status.as_str() {
306 "in_progress" | "inprogress" => TodoStatus::InProgress,
307 "done" => TodoStatus::Done,
308 "blocked" => TodoStatus::Blocked,
309 _ => TodoStatus::Pending,
310 };
311 }
312 if let Some(priority) = p.priority {
313 todo.priority = match priority.as_str() {
314 "low" => Priority::Low,
315 "high" => Priority::High,
316 "critical" => Priority::Critical,
317 _ => Priority::Medium,
318 };
319 }
320 self.save_todos(&todos)?;
321 Ok(ToolResult::success(format!("Updated todo: {}", id)))
322 }
323 "delete" => {
324 let id =
325 p.id.ok_or_else(|| anyhow::anyhow!("id required for delete"))?;
326 let len_before = todos.len();
327 todos.retain(|t| t.id != id);
328 if todos.len() == len_before {
329 return Ok(ToolResult::error(format!("Todo not found: {}", id)));
330 }
331 self.save_todos(&todos)?;
332 Ok(ToolResult::success(format!("Deleted todo: {}", id)))
333 }
334 "clear" => {
335 let count = todos.len();
336 todos.clear();
337 self.save_todos(&todos)?;
338 Ok(ToolResult::success(format!("Cleared {} todos", count)))
339 }
340 _ => Ok(ToolResult::error(format!("Unknown action: {}", p.action))),
341 }
342 }
343}