Skip to main content

codetether_agent/tool/
todo.rs

1//! Todo Tool - Read and write todo items for task tracking.
2
3use 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, // add, update, delete, clear
131    #[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}