Skip to main content

hematite/tools/
tasks.rs

1use crate::tools::file_ops::hematite_dir;
2use serde_json::{json, Value};
3use std::fs;
4use std::path::PathBuf;
5
6/// Manages a persistent TODO list for the agent in `.hematite/TASK.md`.
7/// Actions: list, add, update, remove
8pub async fn manage_tasks(args: &Value) -> Result<String, String> {
9    let action = args
10        .get("action")
11        .and_then(|v| v.as_str())
12        .unwrap_or("list");
13    let task_path = hematite_dir().join("TASK.md");
14
15    match action {
16        "list" => list_tasks(&task_path),
17        "add" => {
18            let title = args
19                .get("title")
20                .and_then(|v| v.as_str())
21                .ok_or("manage_tasks: 'title' required for 'add'")?;
22            add_task(&task_path, title)
23        }
24        "update" => {
25            let id = args
26                .get("id")
27                .and_then(|v| v.as_u64())
28                .ok_or("manage_tasks: 'id' required for 'update'")? as usize;
29            let status = args
30                .get("status")
31                .and_then(|v| v.as_str())
32                .ok_or("manage_tasks: 'status' ([ ], [/], [x]) required for 'update'")?;
33            update_task(&task_path, id, status)
34        }
35        "remove" => {
36            let id = args
37                .get("id")
38                .and_then(|v| v.as_u64())
39                .ok_or("manage_tasks: 'id' required for 'remove'")? as usize;
40            remove_task(&task_path, id)
41        }
42        _ => Err(format!("manage_tasks: unknown action '{action}'")),
43    }
44}
45
46fn list_tasks(path: &PathBuf) -> Result<String, String> {
47    if !path.exists() {
48        return Ok("No task ledger found. Use 'add' to start tracking mission goals.".into());
49    }
50    let content = fs::read_to_string(path).map_err(|e| format!("Failed to read tasks: {e}"))?;
51    Ok(format!(
52        "--- TASK LEDGER (.hematite/TASK.md) ---\n\n{}",
53        content
54    ))
55}
56
57fn add_task(path: &PathBuf, title: &str) -> Result<String, String> {
58    let mut tasks = if path.exists() {
59        fs::read_to_string(path).unwrap_or_default()
60    } else {
61        String::new()
62    };
63
64    if !tasks.is_empty() && !tasks.ends_with('\n') {
65        tasks.push('\n');
66    }
67    tasks.push_str(&format!("- [ ] {}\n", title));
68
69    fs::create_dir_all(path.parent().expect("Invalid task path")).map_err(|e| e.to_string())?;
70    fs::write(path, &tasks).map_err(|e| format!("Failed to write task: {e}"))?;
71
72    Ok(format!("Added task: [ ] {}", title))
73}
74
75fn update_task(path: &PathBuf, id: usize, status: &str) -> Result<String, String> {
76    if !path.exists() {
77        return Err("Task ledger not found".into());
78    }
79    let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
80    let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
81
82    if id < 1 || id > lines.len() {
83        return Err(format!(
84            "Invalid task ID {id}. Ledger has {} items.",
85            lines.len()
86        ));
87    }
88
89    let line = &mut lines[id - 1];
90    if line.starts_with("- [") && line.len() >= 5 {
91        // Update the [ ] status (index 3)
92        let new_line = format!(
93            "- [{}] {}",
94            status.trim_matches(|c| c == '[' || c == ']' || c == ' '),
95            &line[6..]
96        );
97        *line = new_line;
98    } else {
99        return Err("Target line is not a valid task format".into());
100    }
101
102    fs::write(path, lines.join("\n") + "\n").map_err(|e| e.to_string())?;
103    Ok(format!("Updated task {id} to status [{}]", status))
104}
105
106fn remove_task(path: &PathBuf, id: usize) -> Result<String, String> {
107    if !path.exists() {
108        return Err("Task ledger not found".into());
109    }
110    let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
111    let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
112
113    if id < 1 || id > lines.len() {
114        return Err(format!("Invalid task ID {id}."));
115    }
116
117    let removed = lines.remove(id - 1);
118    fs::write(path, lines.join("\n") + "\n").map_err(|e| e.to_string())?;
119    Ok(format!("Removed task: {}", removed))
120}
121
122pub fn get_tasks_params() -> Value {
123    json!({
124        "type": "object",
125        "properties": {
126            "action": {
127                "type": "string",
128                "description": "The action to perform: 'list', 'add', 'update', 'remove'.",
129                "enum": ["list", "add", "update", "remove"]
130            },
131            "id": {
132                "type": "integer",
133                "description": "The 1-based ID of the task to update or remove."
134            },
135            "title": {
136                "type": "string",
137                "description": "The description of the task (required for 'add')."
138            },
139            "status": {
140                "type": "string",
141                "description": "The status to set: '[ ]' (todo), '[/]' (in-progress), '[x]' (done).",
142                "enum": [" ", "/", "x", "[ ]", "[/]", "[x]"]
143            }
144        },
145        "required": ["action"]
146    })
147}