Skip to main content

codetether_agent/tool/
todo.rs

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