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