1use async_trait::async_trait;
2use serde_json::json;
3use std::sync::Mutex;
4
5use super::{Tool, ToolCtx, ToolResult};
6use crate::event::RiskLevel;
7
8pub struct Todo {
9 db: Mutex<Option<rusqlite::Connection>>,
10}
11
12impl Todo {
13 pub fn new() -> Self {
14 Self {
15 db: Mutex::new(None),
16 }
17 }
18
19 fn get_conn(&self) -> anyhow::Result<std::sync::MutexGuard<'_, Option<rusqlite::Connection>>> {
20 let mut guard = self
21 .db
22 .lock()
23 .map_err(|_| anyhow::anyhow!("Todo DB lock poisoned"))?;
24 if guard.is_none() {
25 let state_dir = dirs::state_dir().unwrap_or_default().join("sparrow");
26 std::fs::create_dir_all(&state_dir)?;
27 let conn = rusqlite::Connection::open(state_dir.join("sparrow.db"))?;
28 conn.execute_batch(
29 "CREATE TABLE IF NOT EXISTS todos (
30 id TEXT PRIMARY KEY,
31 content TEXT NOT NULL,
32 status TEXT NOT NULL DEFAULT 'pending',
33 created_at INTEGER NOT NULL DEFAULT (unixepoch()),
34 updated_at INTEGER NOT NULL DEFAULT (unixepoch())
35 );",
36 )?;
37 *guard = Some(conn);
38 }
39 Ok(guard)
40 }
41}
42
43#[async_trait]
44impl Tool for Todo {
45 fn name(&self) -> &str {
46 "todo"
47 }
48 fn description(&self) -> &str {
49 "Track tasks with persistent state across calls"
50 }
51 fn schema(&self) -> serde_json::Value {
52 json!({
53 "type": "object",
54 "properties": {
55 "action": { "type": "string", "enum": ["create", "update", "list", "complete", "delete", "clear_completed"] },
56 "id": { "type": "string" },
57 "content": { "type": "string" },
58 "status": { "type": "string", "enum": ["pending", "in_progress", "completed", "cancelled"] }
59 },
60 "required": ["action"]
61 })
62 }
63 fn risk(&self) -> RiskLevel {
64 RiskLevel::ReadOnly
65 }
66
67 async fn call(&self, args: serde_json::Value, _ctx: &ToolCtx) -> anyhow::Result<ToolResult> {
68 let action = args["action"].as_str().unwrap_or("list");
69 let content = args["content"].as_str().unwrap_or("");
70 let status = args["status"].as_str().unwrap_or("pending");
71 let id = args["id"].as_str().unwrap_or("");
72
73 let guard = self.get_conn()?;
74 let conn = guard
75 .as_ref()
76 .ok_or_else(|| anyhow::anyhow!("Todo DB not initialized"))?;
77
78 match action {
79 "create" => {
80 let new_id = uuid::Uuid::new_v4().to_string();
81 conn.execute(
82 "INSERT INTO todos (id, content, status) VALUES (?1, ?2, ?3)",
83 rusqlite::params![new_id, content, status],
84 )?;
85 Ok(ToolResult::text(format!(
86 "Created task {}: {} ({})",
87 new_id, content, status
88 )))
89 }
90 "update" => {
91 let rows = conn.execute(
92 "UPDATE todos SET content = ?1, status = ?2, updated_at = unixepoch() WHERE id = ?3",
93 rusqlite::params![content, status, id],
94 )?;
95 if rows == 0 {
96 Ok(ToolResult::error(format!("Task {} not found", id)))
97 } else {
98 Ok(ToolResult::text(format!(
99 "Updated task {}: {} ({})",
100 id, content, status
101 )))
102 }
103 }
104 "complete" => {
105 let rows = conn.execute(
106 "UPDATE todos SET status = 'completed', updated_at = unixepoch() WHERE id = ?1",
107 rusqlite::params![id],
108 )?;
109 if rows == 0 {
110 Ok(ToolResult::error(format!("Task {} not found", id)))
111 } else {
112 Ok(ToolResult::text(format!("Completed task: {}", id)))
113 }
114 }
115 "delete" => {
116 conn.execute("DELETE FROM todos WHERE id = ?1", rusqlite::params![id])?;
117 Ok(ToolResult::text(format!("Deleted task: {}", id)))
118 }
119 "clear_completed" => {
120 let count = conn.execute("DELETE FROM todos WHERE status = 'completed'", [])?;
121 Ok(ToolResult::text(format!(
122 "Cleared {} completed tasks",
123 count
124 )))
125 }
126 _ => {
127 let mut stmt = if !status.is_empty() && status != "pending" {
129 conn.prepare("SELECT id, content, status FROM todos WHERE status = ?1 ORDER BY created_at")?
130 } else {
131 conn.prepare("SELECT id, content, status FROM todos ORDER BY created_at")?
132 };
133 let rows: Vec<String> = if !status.is_empty() && status != "pending" {
134 stmt.query_map(rusqlite::params![status], |row| {
135 Ok(format!(
136 " {} [{}] {}",
137 row.get::<_, String>(0)?,
138 row.get::<_, String>(2)?,
139 row.get::<_, String>(1)?
140 ))
141 })?
142 .filter_map(|r| r.ok())
143 .collect()
144 } else {
145 stmt.query_map([], |row| {
146 Ok(format!(
147 " {} [{}] {}",
148 row.get::<_, String>(0)?,
149 row.get::<_, String>(2)?,
150 row.get::<_, String>(1)?
151 ))
152 })?
153 .filter_map(|r| r.ok())
154 .collect()
155 };
156 if rows.is_empty() {
157 Ok(ToolResult::text("No tasks."))
158 } else {
159 Ok(ToolResult::text(rows.join("\n")))
160 }
161 }
162 }
163 }
164}
165
166impl Default for Todo {
167 fn default() -> Self {
168 Self::new()
169 }
170}