1use 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, #[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}