class Clacky::Tools::TodoManager
def add_todos(task: nil, tasks: nil)
def add_todos(task: nil, tasks: nil) # Determine which tasks to add tasks_to_add = [] if tasks && tasks.is_a?(Array) && !tasks.empty? tasks_to_add = tasks.map(&:strip).reject(&:empty?) elsif task && !task.strip.empty? tasks_to_add = [task.strip] end return { error: "At least one task description is required" } if tasks_to_add.empty? existing_todos = load_todos next_id = existing_todos.empty? ? 1 : existing_todos.map { |t| t[:id] }.max + 1 added_todos = [] tasks_to_add.each_with_index do |task_desc, index| new_todo = { id: next_id + index, task: task_desc, status: "pending", created_at: Time.now.iso8601 } existing_todos << new_todo added_todos << new_todo end save_todos(existing_todos) { message: added_todos.size == 1 ? "TODO added successfully" : "#{added_todos.size} TODOs added successfully", todos: added_todos, total: existing_todos.size, reminder: "⚠️ IMPORTANT: You have added TODO(s) but have NOT started working yet! You MUST now use other tools (write, edit, shell, etc.) to actually complete these tasks. DO NOT stop here!" } end
def clear_todos
def clear_todos todos = load_todos count = todos.size # Clear the in-memory storage save_todos([]) { message: "All TODOs cleared", cleared_count: count } end
def complete_todo(id)
def complete_todo(id) return { error: "Task ID is required" } if id.nil? todos = load_todos todo = todos.find { |t| t[:id] == id } return { error: "Task not found: #{id}" } unless todo if todo[:status] == "completed" return { message: "Task already completed", todo: todo } end todo[:status] = "completed" todo[:completed_at] = Time.now.iso8601 save_todos(todos) # Find the next pending task next_pending = todos.find { |t| t[:status] == "pending" } # Count statistics completed_count = todos.count { |t| t[:status] == "completed" } total_count = todos.size result = { message: "Task marked as completed", todo: todo, progress: "#{completed_count}/#{total_count}", reminder: "⚠️ REMINDER: Check the PROJECT-SPECIFIC RULES section in your system prompt before continuing to the next task" } if next_pending result[:next_task] = next_pending result[:next_task_info] = "✅ Progress: #{completed_count}/#{total_count}. Next task: ##{next_pending[:id]} - #{next_pending[:task]}" else result[:all_completed] = true result[:completion_message] = "🎉 All tasks completed! (#{completed_count}/#{total_count})" end result end
def execute(action:, task: nil, tasks: nil, id: nil, ids: nil, todos_storage: nil, working_dir: nil)
def execute(action:, task: nil, tasks: nil, id: nil, ids: nil, todos_storage: nil, working_dir: nil) # todos_storage is injected by Agent, stores todos in memory @todos = todos_storage || [] case action when "add" add_todos(task: task, tasks: tasks) when "list" list_todos when "complete" complete_todo(id) when "remove" # Support both single ID and batch IDs if ids && ids.is_a?(Array) remove_todos(ids) else remove_todo(id) end when "clear" clear_todos else { error: "Unknown action: #{action}" } end end
def format_call(args)
def format_call(args) action = args[:action] || args['action'] case action when 'add' count = (args[:tasks] || args['tasks'])&.size || 1 "TodoManager(add #{count} task#{count > 1 ? 's' : ''})" when 'complete' "TodoManager(complete ##{args[:id] || args['id']})" when 'list' "TodoManager(list)" when 'remove' ids = args[:ids] || args['ids'] if ids && ids.is_a?(Array) && !ids.empty? "TodoManager(remove #{ids.size} tasks: #{ids.join(', ')})" else "TodoManager(remove ##{args[:id] || args['id']})" end when 'clear' "TodoManager(clear all)" else "TodoManager(#{action})" end end
def format_result(result)
def format_result(result) return result[:error] if result[:error] if result[:message] result[:message] else "Done" end end
def list_todos
def list_todos todos = load_todos if todos.empty? return { message: "No TODO items", todos: [], total: 0 } end { message: "TODO list", todos: todos, total: todos.size, pending: todos.count { |t| t[:status] == "pending" }, completed: todos.count { |t| t[:status] == "completed" } } end
def load_todos
def load_todos @todos end
def remove_todo(id)
def remove_todo(id) return { error: "Task ID is required" } if id.nil? todos = load_todos todo = todos.find { |t| t[:id] == id } return { error: "Task not found: #{id}" } unless todo todos.reject! { |t| t[:id] == id } save_todos(todos) { message: "Task removed", todo: todo, remaining: todos.size } end
def remove_todos(ids)
def remove_todos(ids) return { error: "Task IDs array is required" } if ids.nil? || ids.empty? todos = load_todos removed_todos = [] not_found_ids = [] ids.each do |id| todo = todos.find { |t| t[:id] == id } if todo removed_todos << todo else not_found_ids << id end end # Remove all found todos todos.reject! { |t| ids.include?(t[:id]) } save_todos(todos) result = { message: "#{removed_todos.size} task(s) removed", removed: removed_todos, remaining: todos.size } # Add warning about not found IDs result[:not_found] = not_found_ids unless not_found_ids.empty? result end
def save_todos(todos)
def save_todos(todos) # Modify the array in-place so Agent's @todos is updated # Important: Don't use @todos.clear first because todos might be @todos itself! @todos.replace(todos) end