todo-agent 0.2.4__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {todo_agent-0.2.4 → todo_agent-0.2.5}/PKG-INFO +1 -1
  2. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_core/test_todo_manager.py +57 -0
  3. todo_agent-0.2.5/tests/test_infrastructure/test_calendar_utils.py +81 -0
  4. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_interface/test_cli.py +45 -0
  5. todo_agent-0.2.5/tests/test_interface/test_formatters.py +75 -0
  6. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_interface/test_tools.py +59 -0
  7. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/_version.py +3 -3
  8. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/core/todo_manager.py +33 -0
  9. todo_agent-0.2.5/todo_agent/infrastructure/calendar_utils.py +64 -0
  10. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/inference.py +13 -2
  11. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/llm_client_factory.py +4 -2
  12. todo_agent-0.2.5/todo_agent/infrastructure/prompts/system_prompt.txt +92 -0
  13. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/interface/cli.py +28 -9
  14. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/interface/formatters.py +144 -48
  15. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/interface/tools.py +80 -8
  16. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent.egg-info/PKG-INFO +1 -1
  17. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent.egg-info/SOURCES.txt +3 -0
  18. todo_agent-0.2.4/todo_agent/infrastructure/prompts/system_prompt.txt +0 -51
  19. {todo_agent-0.2.4 → todo_agent-0.2.5}/.gitignore +0 -0
  20. {todo_agent-0.2.4 → todo_agent-0.2.5}/LICENSE +0 -0
  21. {todo_agent-0.2.4 → todo_agent-0.2.5}/MANIFEST.in +0 -0
  22. {todo_agent-0.2.4 → todo_agent-0.2.5}/Makefile +0 -0
  23. {todo_agent-0.2.4 → todo_agent-0.2.5}/README.md +0 -0
  24. {todo_agent-0.2.4 → todo_agent-0.2.5}/docs/publishing.md +0 -0
  25. {todo_agent-0.2.4 → todo_agent-0.2.5}/pyproject.toml +0 -0
  26. {todo_agent-0.2.4 → todo_agent-0.2.5}/requirements-dev.txt +0 -0
  27. {todo_agent-0.2.4 → todo_agent-0.2.5}/requirements.txt +0 -0
  28. {todo_agent-0.2.4 → todo_agent-0.2.5}/setup.cfg +0 -0
  29. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/__init__.py +0 -0
  30. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_core/__init__.py +0 -0
  31. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_core/test_conversation_manager.py +0 -0
  32. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/__init__.py +0 -0
  33. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/test_config.py +0 -0
  34. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/test_inference.py +0 -0
  35. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  36. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/test_ollama_client.py +0 -0
  37. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/test_openrouter_client.py +0 -0
  38. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/test_todo_shell.py +0 -0
  39. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_infrastructure/test_token_counter.py +0 -0
  40. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_interface/__init__.py +0 -0
  41. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_linting.py +0 -0
  42. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_logger.py +0 -0
  43. {todo_agent-0.2.4 → todo_agent-0.2.5}/tests/test_main.py +0 -0
  44. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/__init__.py +0 -0
  45. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/core/__init__.py +0 -0
  46. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/core/conversation_manager.py +0 -0
  47. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/core/exceptions.py +0 -0
  48. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/__init__.py +0 -0
  49. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/config.py +0 -0
  50. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/llm_client.py +0 -0
  51. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/logger.py +0 -0
  52. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/ollama_client.py +0 -0
  53. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/openrouter_client.py +0 -0
  54. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/todo_shell.py +0 -0
  55. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/infrastructure/token_counter.py +0 -0
  56. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/interface/__init__.py +0 -0
  57. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent/main.py +0 -0
  58. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent.egg-info/dependency_links.txt +0 -0
  59. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent.egg-info/entry_points.txt +0 -0
  60. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent.egg-info/requires.txt +0 -0
  61. {todo_agent-0.2.4 → todo_agent-0.2.5}/todo_agent.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -32,6 +32,63 @@ class TestTodoManager(unittest.TestCase):
32
32
  self.assertEqual(result, "Added task: Test task")
33
33
  self.todo_shell.add.assert_called_once_with("Test task")
34
34
 
35
+ def test_add_task_with_invalid_priority(self):
36
+ """Test adding a task with invalid priority raises ValueError."""
37
+ with self.assertRaises(ValueError) as context:
38
+ self.todo_manager.add_task("Test task", priority="invalid")
39
+ self.assertIn("Invalid priority", str(context.exception))
40
+
41
+ def test_add_task_with_invalid_priority_lowercase(self):
42
+ """Test adding a task with lowercase priority raises ValueError."""
43
+ with self.assertRaises(ValueError) as context:
44
+ self.todo_manager.add_task("Test task", priority="a")
45
+ self.assertIn("Invalid priority", str(context.exception))
46
+
47
+ def test_add_task_with_invalid_priority_multiple_chars(self):
48
+ """Test adding a task with multiple character priority raises ValueError."""
49
+ with self.assertRaises(ValueError) as context:
50
+ self.todo_manager.add_task("Test task", priority="AB")
51
+ self.assertIn("Invalid priority", str(context.exception))
52
+
53
+ def test_add_task_sanitizes_project_with_plus(self):
54
+ """Test that project with existing + symbol is properly sanitized."""
55
+ self.todo_shell.add.return_value = "Test task"
56
+ result = self.todo_manager.add_task("Test task", project="+work")
57
+ self.assertEqual(result, "Added task: Test task +work")
58
+ self.todo_shell.add.assert_called_once_with("Test task +work")
59
+
60
+ def test_add_task_sanitizes_context_with_at(self):
61
+ """Test that context with existing @ symbol is properly sanitized."""
62
+ self.todo_shell.add.return_value = "Test task"
63
+ result = self.todo_manager.add_task("Test task", context="@office")
64
+ self.assertEqual(result, "Added task: Test task @office")
65
+ self.todo_shell.add.assert_called_once_with("Test task @office")
66
+
67
+ def test_add_task_with_empty_project_after_sanitization(self):
68
+ """Test adding a task with empty project after removing + raises ValueError."""
69
+ with self.assertRaises(ValueError) as context:
70
+ self.todo_manager.add_task("Test task", project="+")
71
+ self.assertIn("Project name cannot be empty", str(context.exception))
72
+
73
+ def test_add_task_with_empty_context_after_sanitization(self):
74
+ """Test adding a task with empty context after removing @ raises ValueError."""
75
+ with self.assertRaises(ValueError) as context:
76
+ self.todo_manager.add_task("Test task", context="@")
77
+ self.assertIn("Context name cannot be empty", str(context.exception))
78
+
79
+ def test_add_task_with_invalid_due_date(self):
80
+ """Test adding a task with invalid due date format raises ValueError."""
81
+ with self.assertRaises(ValueError) as context:
82
+ self.todo_manager.add_task("Test task", due="invalid-date")
83
+ self.assertIn("Invalid due date format", str(context.exception))
84
+
85
+ def test_add_task_with_valid_due_date(self):
86
+ """Test adding a task with valid due date format."""
87
+ self.todo_shell.add.return_value = "Test task"
88
+ result = self.todo_manager.add_task("Test task", due="2024-01-15")
89
+ self.assertEqual(result, "Added task: Test task due:2024-01-15")
90
+ self.todo_shell.add.assert_called_once_with("Test task due:2024-01-15")
91
+
35
92
  def test_list_tasks(self):
36
93
  """Test listing tasks."""
37
94
  self.todo_shell.list_tasks.return_value = "1. Task 1\n2. Task 2"
@@ -0,0 +1,81 @@
1
+ """
2
+ Tests for calendar utilities.
3
+ """
4
+
5
+ from unittest.mock import Mock, patch
6
+
7
+ import pytest
8
+
9
+ from todo_agent.infrastructure.calendar_utils import (
10
+ get_calendar_output,
11
+ get_current_month_calendar,
12
+ )
13
+
14
+
15
+ class TestCalendarUtils:
16
+ """Test calendar utility functions."""
17
+
18
+ def test_get_calendar_output_success(self):
19
+ """Test successful calendar output generation."""
20
+ mock_output = "Calendar output from cal -3"
21
+
22
+ with patch("subprocess.run") as mock_run:
23
+ mock_result = Mock()
24
+ mock_result.stdout = mock_output
25
+ mock_run.return_value = mock_result
26
+
27
+ result = get_calendar_output()
28
+
29
+ assert result == mock_output
30
+ assert len(result) > 0
31
+ mock_run.assert_called_once_with(
32
+ ["cal", "-3"], capture_output=True, text=True, check=True
33
+ )
34
+
35
+ def test_get_calendar_output_fallback(self):
36
+ """Test calendar output fallback to Python calendar."""
37
+ with patch("subprocess.run", side_effect=FileNotFoundError("cal not found")):
38
+ result = get_calendar_output()
39
+
40
+ # Should return a string with calendar data
41
+ assert isinstance(result, str)
42
+ assert len(result) > 0
43
+
44
+ def test_get_current_month_calendar_success(self):
45
+ """Test successful current month calendar generation."""
46
+ mock_output = "Current month calendar"
47
+
48
+ with patch("subprocess.run") as mock_run:
49
+ mock_result = Mock()
50
+ mock_result.stdout = mock_output
51
+ mock_run.return_value = mock_result
52
+
53
+ result = get_current_month_calendar()
54
+
55
+ assert result == mock_output
56
+ assert len(result) > 0
57
+ mock_run.assert_called_once_with(
58
+ ["cal"], capture_output=True, text=True, check=True
59
+ )
60
+
61
+ def test_get_current_month_calendar_fallback(self):
62
+ """Test current month calendar fallback to Python calendar."""
63
+ with patch("subprocess.run", side_effect=FileNotFoundError("cal not found")):
64
+ result = get_current_month_calendar()
65
+
66
+ # Should return a string with calendar data
67
+ assert isinstance(result, str)
68
+ assert len(result) > 0
69
+
70
+ def test_calendar_output_contains_expected_content(self):
71
+ """Test that calendar output contains expected content."""
72
+ with patch("subprocess.run") as mock_run:
73
+ mock_result = Mock()
74
+ mock_result.stdout = "Calendar with July August September 2025"
75
+ mock_run.return_value = mock_result
76
+
77
+ result = get_calendar_output()
78
+
79
+ # Should contain some calendar content
80
+ assert len(result) > 0
81
+ assert isinstance(result, str)
@@ -295,6 +295,49 @@ class TestCLI:
295
295
  f"Error: Failed to list tasks: {error_message}"
296
296
  )
297
297
 
298
+ def test_done_command_success(self):
299
+ """Test successful done command execution."""
300
+ expected_output = (
301
+ "x 2025-08-29 2025-08-28 Buy groceries\nx 2025-08-28 2025-08-27 Call mom"
302
+ )
303
+
304
+ # Mock the todo_shell.list_completed method
305
+ self.cli.todo_shell.list_completed.return_value = expected_output
306
+
307
+ with patch("builtins.print") as mock_print:
308
+ # Simulate the done command logic
309
+ try:
310
+ output = self.cli.todo_shell.list_completed()
311
+ print(output)
312
+ except Exception as e:
313
+ print(f"Error: Failed to list completed tasks: {e!s}")
314
+
315
+ # Verify todo_shell.list_completed was called
316
+ self.cli.todo_shell.list_completed.assert_called_once()
317
+
318
+ # Verify print was called with the expected output
319
+ mock_print.assert_called_once_with(expected_output)
320
+
321
+ def test_done_command_exception_handling(self):
322
+ """Test done command handles exceptions properly."""
323
+ error_message = "Database connection failed"
324
+
325
+ # Mock exception in todo_shell.list_completed
326
+ self.cli.todo_shell.list_completed.side_effect = Exception(error_message)
327
+
328
+ with patch("builtins.print") as mock_print:
329
+ # Simulate the done command logic with error
330
+ try:
331
+ output = self.cli.todo_shell.list_completed()
332
+ print(output)
333
+ except Exception as e:
334
+ print(f"Error: Failed to list completed tasks: {e!s}")
335
+
336
+ # Verify error was handled and formatted correctly
337
+ mock_print.assert_called_once_with(
338
+ f"Error: Failed to list completed tasks: {error_message}"
339
+ )
340
+
298
341
  def test_help_command_displays_available_commands(self):
299
342
  """Test that help command displays all available commands."""
300
343
  with patch("builtins.print") as mock_print:
@@ -304,6 +347,7 @@ class TestCLI:
304
347
  print(" history - Show conversation statistics")
305
348
  print(" help - Show this help message")
306
349
  print(" list - List all tasks (no LLM interaction)")
350
+ print(" done - List completed tasks (no LLM interaction)")
307
351
  print(" quit - Exit the application")
308
352
  print(" Or just type your request naturally!")
309
353
 
@@ -314,6 +358,7 @@ class TestCLI:
314
358
  " history - Show conversation statistics",
315
359
  " help - Show this help message",
316
360
  " list - List all tasks (no LLM interaction)",
361
+ " done - List completed tasks (no LLM interaction)",
317
362
  " quit - Exit the application",
318
363
  " Or just type your request naturally!",
319
364
  ]
@@ -0,0 +1,75 @@
1
+ """
2
+ Tests for formatters module.
3
+ """
4
+
5
+ import pytest
6
+ from rich.text import Text
7
+
8
+ from todo_agent.interface.formatters import TaskFormatter
9
+
10
+
11
+ class TestTaskFormatter:
12
+ """Test TaskFormatter functionality."""
13
+
14
+ def test_format_task_list_preserves_ansi_codes(self):
15
+ """Test that format_task_list preserves ANSI color codes."""
16
+ # Sample output with ANSI color codes (simulating todo.sh output)
17
+ raw_tasks = "\033[1;33m1\033[0m (A) \033[1;32m2025-08-29\033[0m Clean cat box \033[1;34m@home\033[0m \033[1;35m+chores\033[0m \033[1;31mdue:2025-08-29\033[0m"
18
+
19
+ result = TaskFormatter.format_task_list(raw_tasks)
20
+
21
+ # The result should be a Rich Text object
22
+ assert isinstance(result, Text)
23
+
24
+ # Check that the Rich Text object contains the original ANSI codes
25
+ # We can check this by looking at the raw text content
26
+ result_str = result.plain
27
+ assert "1" in result_str # Task number should be present
28
+ assert "(A)" in result_str # Priority should be present
29
+ assert "2025-08-29" in result_str # Date should be present
30
+ assert "@home" in result_str # Context should be present
31
+ assert "+chores" in result_str # Project should be present
32
+ assert "due:2025-08-29" in result_str # Due date should be present
33
+
34
+ def test_format_completed_tasks_preserves_ansi_codes(self):
35
+ """Test that format_completed_tasks preserves ANSI color codes."""
36
+ # Sample completed task output with ANSI color codes
37
+ raw_tasks = "\033[1;32mx\033[0m \033[1;32m2025-08-29\033[0m \033[1;32m2025-08-28\033[0m Clean cat box \033[1;34m@home\033[0m \033[1;35m+chores\033[0m"
38
+
39
+ result = TaskFormatter.format_completed_tasks(raw_tasks)
40
+
41
+ # The result should be a Rich Text object
42
+ assert isinstance(result, Text)
43
+
44
+ # Check that the Rich Text object contains the original content
45
+ # We can check this by looking at the raw text content
46
+ result_str = result.plain
47
+ assert "x" in result_str # Completion marker should be present
48
+ assert "2025-08-29" in result_str # Completion date should be present
49
+ assert "2025-08-28" in result_str # Creation date should be present
50
+ assert "@home" in result_str # Context should be present
51
+ assert "+chores" in result_str # Project should be present
52
+
53
+ def test_format_task_list_handles_empty_input(self):
54
+ """Test that format_task_list handles empty input gracefully."""
55
+ result = TaskFormatter.format_task_list("")
56
+ assert isinstance(result, Text)
57
+ assert "No tasks found" in str(result)
58
+
59
+ def test_format_completed_tasks_handles_empty_input(self):
60
+ """Test that format_completed_tasks handles empty input gracefully."""
61
+ result = TaskFormatter.format_completed_tasks("")
62
+ assert isinstance(result, Text)
63
+ assert "No completed tasks found" in str(result)
64
+
65
+ def test_format_task_list_handles_whitespace_only(self):
66
+ """Test that format_task_list handles whitespace-only input."""
67
+ result = TaskFormatter.format_task_list(" \n \t ")
68
+ assert isinstance(result, Text)
69
+ assert "No tasks found" in str(result)
70
+
71
+ def test_format_completed_tasks_handles_whitespace_only(self):
72
+ """Test that format_completed_tasks handles whitespace-only input."""
73
+ result = TaskFormatter.format_completed_tasks(" \n \t ")
74
+ assert isinstance(result, Text)
75
+ assert "No completed tasks found" in str(result)
@@ -178,3 +178,62 @@ class TestToolErrorHandling:
178
178
 
179
179
  assert result["error"] is False
180
180
  assert result["output"] == "1. Test task"
181
+
182
+
183
+ class TestCalendarTool:
184
+ """Test calendar tool functionality."""
185
+
186
+ def setup_method(self):
187
+ """Set up test fixtures."""
188
+ self.mock_todo_manager = Mock(spec=TodoManager)
189
+ self.mock_logger = Mock()
190
+ self.tool_handler = ToolCallHandler(self.mock_todo_manager, self.mock_logger)
191
+
192
+ @patch("subprocess.run")
193
+ def test_get_calendar_success(self, mock_run):
194
+ """Test successful calendar retrieval using system cal command."""
195
+ # Mock the subprocess.run to return a calendar
196
+ mock_run.return_value.stdout = " January 2025\nSu Mo Tu We Th Fr Sa\n 1 2 3 4\n 5 6 7 8 9 10 11\n12 13 14 15 16 17 18\n19 20 21 22 23 24 25\n26 27 28 29 30 31\n"
197
+ mock_run.return_value.returncode = 0
198
+
199
+ tool_call = {
200
+ "function": {
201
+ "name": "get_calendar",
202
+ "arguments": '{"month": 1, "year": 2025}',
203
+ },
204
+ "id": "test_id",
205
+ }
206
+
207
+ result = self.tool_handler.execute_tool(tool_call)
208
+
209
+ assert result["error"] is False
210
+ assert "January 2025" in result["output"]
211
+ assert result["tool_call_id"] == "test_id"
212
+ assert result["name"] == "get_calendar"
213
+
214
+ # Verify subprocess.run was called with correct arguments
215
+ mock_run.assert_called_once_with(
216
+ ["cal", "1", "2025"], capture_output=True, text=True, check=True
217
+ )
218
+
219
+ @patch("subprocess.run")
220
+ def test_get_calendar_fallback_to_python(self, mock_run):
221
+ """Test calendar fallback to Python calendar module when cal command fails."""
222
+ # Mock subprocess.run to raise FileNotFoundError
223
+ mock_run.side_effect = FileNotFoundError("cal command not found")
224
+
225
+ tool_call = {
226
+ "function": {
227
+ "name": "get_calendar",
228
+ "arguments": '{"month": 1, "year": 2025}',
229
+ },
230
+ "id": "test_id",
231
+ }
232
+
233
+ result = self.tool_handler.execute_tool(tool_call)
234
+
235
+ assert result["error"] is False
236
+ assert "January" in result["output"]
237
+ assert "2025" in result["output"]
238
+ assert result["tool_call_id"] == "test_id"
239
+ assert result["name"] == "get_calendar"
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.4'
32
- __version_tuple__ = version_tuple = (0, 2, 4)
31
+ __version__ = version = '0.2.5'
32
+ __version_tuple__ = version_tuple = (0, 2, 5)
33
33
 
34
- __commit_id__ = commit_id = 'gc2b1a1313'
34
+ __commit_id__ = commit_id = 'gcb2b8bbf9'
@@ -21,6 +21,39 @@ class TodoManager:
21
21
  due: Optional[str] = None,
22
22
  ) -> str:
23
23
  """Add new task with explicit project/context parameters."""
24
+ # Validate and sanitize inputs
25
+ if priority and not (
26
+ len(priority) == 1 and priority.isalpha() and priority.isupper()
27
+ ):
28
+ raise ValueError(
29
+ f"Invalid priority '{priority}'. Must be a single uppercase letter (A-Z)."
30
+ )
31
+
32
+ if project:
33
+ # Remove any existing + symbols to prevent duplication
34
+ project = project.strip().lstrip("+")
35
+ if not project:
36
+ raise ValueError(
37
+ "Project name cannot be empty after removing + symbol."
38
+ )
39
+
40
+ if context:
41
+ # Remove any existing @ symbols to prevent duplication
42
+ context = context.strip().lstrip("@")
43
+ if not context:
44
+ raise ValueError(
45
+ "Context name cannot be empty after removing @ symbol."
46
+ )
47
+
48
+ if due:
49
+ # Basic date format validation
50
+ try:
51
+ datetime.strptime(due, "%Y-%m-%d")
52
+ except ValueError:
53
+ raise ValueError(
54
+ f"Invalid due date format '{due}'. Must be YYYY-MM-DD."
55
+ )
56
+
24
57
  # Build the full task description with priority, project, and context
25
58
  full_description = description
26
59
 
@@ -0,0 +1,64 @@
1
+ """
2
+ Calendar utilities for generating calendar output in system prompts.
3
+ """
4
+
5
+ import calendar
6
+ import subprocess
7
+ from datetime import datetime, timedelta
8
+
9
+
10
+ def get_calendar_output() -> str:
11
+ """
12
+ Generate calendar output for previous, current, and next month.
13
+
14
+ Returns:
15
+ Formatted calendar string showing three months side by side
16
+ """
17
+ try:
18
+ # Use cal -3 to get three months side by side
19
+ result = subprocess.run(
20
+ ["cal", "-3"], capture_output=True, text=True, check=True
21
+ )
22
+ return result.stdout.strip()
23
+ except (subprocess.SubprocessError, FileNotFoundError):
24
+ # Fallback to Python calendar module
25
+ return _get_python_cal_output()
26
+
27
+
28
+ def _get_python_cal_output() -> str:
29
+ """
30
+ Generate calendar output using Python calendar module as fallback.
31
+
32
+ Returns:
33
+ Calendar output formatted similar to cal command
34
+ """
35
+ current_date = datetime.now()
36
+
37
+ # Calculate previous, current, and next month
38
+ prev_month = current_date - timedelta(days=current_date.day)
39
+ next_month = current_date.replace(day=1) + timedelta(days=32)
40
+ next_month = next_month.replace(day=1)
41
+
42
+ calendars = []
43
+
44
+ for date in [prev_month, current_date, next_month]:
45
+ cal = calendar.month(date.year, date.month)
46
+ calendars.append(cal.strip())
47
+
48
+ return "\n\n".join(calendars)
49
+
50
+
51
+ def get_current_month_calendar() -> str:
52
+ """
53
+ Get calendar for current month only.
54
+
55
+ Returns:
56
+ Calendar output for current month
57
+ """
58
+ try:
59
+ result = subprocess.run(["cal"], capture_output=True, text=True, check=True)
60
+ return result.stdout.strip()
61
+ except (subprocess.SubprocessError, FileNotFoundError):
62
+ # Fallback to Python calendar
63
+ current_date = datetime.now()
64
+ return calendar.month(current_date.year, current_date.month).strip()
@@ -75,6 +75,15 @@ class Inference:
75
75
 
76
76
  current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
77
77
 
78
+ # Get calendar output
79
+ from .calendar_utils import get_calendar_output
80
+
81
+ try:
82
+ calendar_output = get_calendar_output()
83
+ except Exception as e:
84
+ self.logger.warning(f"Failed to get calendar output: {e!s}")
85
+ calendar_output = "Calendar unavailable"
86
+
78
87
  # Load system prompt from file
79
88
  prompt_file_path = os.path.join(
80
89
  os.path.dirname(__file__), "prompts", "system_prompt.txt"
@@ -84,9 +93,11 @@ class Inference:
84
93
  with open(prompt_file_path, encoding="utf-8") as f:
85
94
  system_prompt_template = f.read()
86
95
 
87
- # Format the template with the tools section and current datetime
96
+ # Format the template with the tools section, current datetime, and calendar
88
97
  return system_prompt_template.format(
89
- tools_section=tools_section, current_datetime=current_datetime
98
+ tools_section=tools_section,
99
+ current_datetime=current_datetime,
100
+ calendar_output=calendar_output,
90
101
  )
91
102
 
92
103
  except FileNotFoundError:
@@ -2,6 +2,8 @@
2
2
  Factory for creating LLM clients based on configuration.
3
3
  """
4
4
 
5
+ # mypy: disable-error-code="no-redef"
6
+
5
7
  from typing import Optional
6
8
 
7
9
  try:
@@ -15,8 +17,8 @@ except ImportError:
15
17
  from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
16
18
  from infrastructure.logger import Logger # type: ignore[no-redef]
17
19
  from infrastructure.ollama_client import OllamaClient # type: ignore[no-redef]
18
- from infrastructure.openrouter_client import ( # type: ignore[no-redef]
19
- OpenRouterClient,
20
+ from infrastructure.openrouter_client import (
21
+ OpenRouterClient, # type: ignore[no-redef, misc]
20
22
  )
21
23
 
22
24
 
@@ -0,0 +1,92 @@
1
+ You are a todo.sh assistant managing tasks in standard todo.txt format.
2
+
3
+ CURRENT DATE/TIME: {current_datetime}
4
+ {calendar_output}
5
+
6
+ CORE PRINCIPLES:
7
+ 1. Strategic Tool Usage: Batch discovery tools ([list_tasks, list_completed_tasks, list_projects, list_contexts]) to minimize API calls
8
+ 2. Conversational: Respond naturally without mentioning tools or technical details
9
+ 3. Data Integrity: Only reference tasks/projects/contexts returned by actual tool calls - NEVER hallucinate
10
+ 4. Safety: Always verify current state before modifications using list_tasks() and list_completed_tasks()
11
+ 5. Todo.txt Compliance: Use standard format and ordering
12
+
13
+ TODO.TXT FORMAT:
14
+ - Priority: (A), (B), (C) • Completion: "x YYYY-MM-DD" • Creation: YYYY-MM-DD
15
+ - Projects: +project (single + symbol) • Contexts: @context (single @ symbol) • Due dates: due:YYYY-MM-DD
16
+ - Example: "(A) 2024-01-15 Call dentist +health @phone due:2024-01-20"
17
+ - CRITICAL: Never use double symbols like ++project or @@context - always use single + and @ symbols
18
+
19
+ WORKFLOW:
20
+ Discovery First: Gather context with batched tool calls before any action
21
+ Verify Before Action: Check for duplicates, conflicts, or existing completions
22
+ Sequential Processing: Tools execute in order within batches
23
+
24
+ TASK COMPLETION:
25
+ When users say something like "I finished X" or "I'm done with Y", search for matching tasks
26
+ using list_tasks() and handle ambiguity by showing numbered options. Always verify task
27
+ hasn't already been completed with list_completed_tasks().
28
+
29
+ COMPLETION INTELLIGENCE:
30
+ - If user's statement clearly matches exactly one task (e.g., "I finished mowing the lawn"
31
+ when there's only one task with "yard work" in the description), complete it immediately
32
+ - If user's statement matches multiple tasks, show numbered options and ask for clarification
33
+ - If user's statement is ambiguous or could match many tasks, ask for clarification
34
+ - When in doubt about ambiguity, ask for more information to clarify intent before taking any action
35
+
36
+ CONTEXT AND PROJECT INFERENCE:
37
+ - Extract temporal urgency from due dates and creation dates
38
+ - Identify task relationships through shared projects/contexts
39
+ - Determine scope boundaries from natural language (work vs personal tasks)
40
+ - Recognize priority patterns and dependencies
41
+
42
+ TASK CREATION INTELLIGENCE:
43
+ - When users request to add a task, automatically infer appropriate projects, contexts, and due dates based on the task content
44
+ - When intent is clear, create the task immediately without asking for confirmation
45
+ - Only ask for clarification when project/context/due date is genuinely ambiguous
46
+ - Use priority C for new tasks unless urgency is indicated
47
+ - DUE DATE INFERENCE: Automatically infer due dates using multiple intelligence sources:
48
+ * Explicit expressions: "tomorrow", "next week", "next Monday", "by Friday" → Convert to YYYY-MM-DD format
49
+ * Relative expressions: "in 3 days", "next month", "end of month" → Calculate appropriate date
50
+ * Urgency indicators: "urgent", "asap", "today" → Set to today's date
51
+ * Vague expressions: "sometime this week" → Set to end of current week
52
+ * Task nature inference: Use common sense based on task type and existing patterns:
53
+ - Work tasks → Consider work week patterns and existing work task due dates
54
+ - Personal tasks → Consider weekend availability and personal schedule patterns
55
+ - Health/medical → Consider urgency and typical scheduling patterns
56
+ - Shopping/errands → Consider when items are needed and store hours
57
+ - Bills/payments → Consider due dates and late fees
58
+ - Maintenance tasks → Consider frequency patterns and current state
59
+ * Calendar context: Use current date/time and calendar output to inform timing decisions
60
+ * Existing task patterns: Look at similar tasks and their due dates for consistency
61
+ * Always infer: Every task should have a reasonable due date based on available context
62
+
63
+ TASK ADVICE:
64
+ Think deeply and critically to categorize tasks and suggest actions:
65
+ - Consider real-life implications and importance to my responsibilities regardless of explicit priority
66
+ - When users request prioritization help, use Eisenhower Matrix:
67
+ Q1 (Urgent+Important: DO), Q2 (Important: SCHEDULE), Q3 (Urgent: DELEGATE), Q4 (Neither: ELIMINATE) [assign SPARINGLY].
68
+
69
+ ERROR HANDLING:
70
+ - Empty results: Suggest next steps
71
+ - Ambiguous requests: Show numbered options
72
+ - Large lists: Use filtering/summaries for 10+ items
73
+ - Failed operations: Explain clearly with alternatives
74
+
75
+ OUTPUT FORMATTING:
76
+ - Calendar Display: Show calendar output as plain text without backticks, code blocks, or markdown formatting
77
+ - Task Lists: Present tasks in conversational language, not raw todo.txt format
78
+ - Natural Language: Use conversational responses that feel natural and helpful
79
+ - No Technical Details: Avoid mentioning tools, API calls, or technical implementation details
80
+
81
+ CRITICAL RULES:
82
+ - Anti-hallucination: If no tool data exists, say "I need to check your tasks first"
83
+ - Use appropriate discovery tools extensively
84
+ - Never assume task existence without verification
85
+ - Maintain todo.txt standard compliance
86
+ - Format Compliance: Always use single + for projects and single @ for contexts (never ++ or @@)
87
+ - Display Formatting: When showing calendar output, display it as plain text without backticks or code blocks
88
+ - Proactive Task Creation: When users request to add a task, create it immediately with inferred tags and due dates unless genuinely ambiguous
89
+ - No Unnecessary Confirmation: Don't ask for confirmation when the task intent is clear and appropriate tags/due dates can be inferred
90
+ - Due Date Intelligence: Always infer reasonable due dates using task nature, calendar context, existing patterns, and common sense. Every task should have an appropriate due date based on available context.
91
+
92
+ AVAILABLE TOOLS: {tools_section}