todo-agent 0.2.4__py3-none-any.whl → 0.2.5__py3-none-any.whl

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.
todo_agent/_version.py CHANGED
@@ -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
34
  __commit_id__ = commit_id = None
@@ -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
 
@@ -1,51 +1,92 @@
1
1
  You are a todo.sh assistant managing tasks in standard todo.txt format.
2
2
 
3
3
  CURRENT DATE/TIME: {current_datetime}
4
+ {calendar_output}
4
5
 
5
6
  CORE PRINCIPLES:
6
- 1. **Strategic Tool Usage**: Batch discovery tools ([list_tasks, list_completed_tasks, list_projects, list_contexts]) to minimize API calls
7
- 2. **Conversational**: Respond naturally without mentioning tools or technical details
8
- 3. **Data Integrity**: Only reference tasks/projects/contexts returned by actual tool calls - NEVER hallucinate
9
- 4. **Safety**: Always verify current state before modifications using list_tasks() and list_completed_tasks()
10
- 5. **Todo.txt Compliance**: Use standard format and ordering
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
11
12
 
12
13
  TODO.TXT FORMAT:
13
14
  - Priority: (A), (B), (C) • Completion: "x YYYY-MM-DD" • Creation: YYYY-MM-DD
14
- - Projects: +project • Contexts: @context • Due dates: due:YYYY-MM-DD
15
+ - Projects: +project (single + symbol) • Contexts: @context (single @ symbol) • Due dates: due:YYYY-MM-DD
15
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
16
18
 
17
19
  WORKFLOW:
18
- **Discovery First**: Gather context with batched tool calls before any action
19
- **Verify Before Action**: Check for duplicates, conflicts, or existing completions
20
- **Sequential Processing**: Tools execute in order within batches
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
21
23
 
22
- CONTEXT INFERENCE:
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:
23
37
  - Extract temporal urgency from due dates and creation dates
24
38
  - Identify task relationships through shared projects/contexts
25
39
  - Determine scope boundaries from natural language (work vs personal tasks)
26
40
  - Recognize priority patterns and dependencies
27
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
+
28
63
  TASK ADVICE:
29
64
  Think deeply and critically to categorize tasks and suggest actions:
30
65
  - Consider real-life implications and importance to my responsibilities regardless of explicit priority
31
66
  - When users request prioritization help, use Eisenhower Matrix:
32
67
  Q1 (Urgent+Important: DO), Q2 (Important: SCHEDULE), Q3 (Urgent: DELEGATE), Q4 (Neither: ELIMINATE) [assign SPARINGLY].
33
68
 
34
- COMPLETED TASKS:
35
- When users mention past accomplishments ("I did XXX today"):
36
- 1. add_task() with description
37
- 2. complete_task() with same ID using "x YYYY-MM-DD" format
38
-
39
69
  ERROR HANDLING:
40
70
  - Empty results: Suggest next steps
41
71
  - Ambiguous requests: Show numbered options
42
72
  - Large lists: Use filtering/summaries for 10+ items
43
73
  - Failed operations: Explain clearly with alternatives
44
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
+
45
81
  CRITICAL RULES:
46
- - **Anti-hallucination**: If no tool data exists, say "I need to check your tasks first"
82
+ - Anti-hallucination: If no tool data exists, say "I need to check your tasks first"
47
83
  - Use appropriate discovery tools extensively
48
84
  - Never assume task existence without verification
49
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.
50
91
 
51
92
  AVAILABLE TOOLS: {tools_section}
@@ -78,8 +78,8 @@ class CLI:
78
78
  self.inference = Inference(self.config, self.tool_handler, self.logger)
79
79
  self.logger.debug("Inference engine initialized")
80
80
 
81
- # Initialize rich console for animations with consistent width
82
- self.console = Console(width=CLI_WIDTH)
81
+ # Initialize rich console for animations with consistent width and color support
82
+ self.console = Console(width=CLI_WIDTH, color_system="auto")
83
83
 
84
84
  self.logger.info("CLI initialization completed")
85
85
 
@@ -136,21 +136,21 @@ class CLI:
136
136
  """Get session memory usage as a progress bar."""
137
137
  # Get conversation manager to access memory limits and current usage
138
138
  conversation_manager = self.inference.get_conversation_manager()
139
-
139
+
140
140
  # Get current usage from conversation summary
141
141
  summary = conversation_manager.get_conversation_summary()
142
142
  current_tokens = summary.get("estimated_tokens", 0)
143
143
  current_messages = summary.get("total_messages", 0)
144
-
144
+
145
145
  # Get limits from conversation manager
146
146
  max_tokens = conversation_manager.max_tokens
147
147
  max_messages = conversation_manager.max_messages
148
-
148
+
149
149
  # Create memory usage bar
150
150
  memory_bar = PanelFormatter.create_memory_usage_bar(
151
151
  current_tokens, max_tokens, current_messages, max_messages
152
152
  )
153
-
153
+
154
154
  return memory_bar
155
155
 
156
156
  def run(self) -> None:
@@ -208,7 +208,9 @@ class CLI:
208
208
  try:
209
209
  output = self.todo_shell.list_tasks()
210
210
  formatted_output = TaskFormatter.format_task_list(output)
211
- task_panel = PanelFormatter.create_task_panel(formatted_output)
211
+ task_panel = PanelFormatter.create_task_panel(
212
+ formatted_output
213
+ )
212
214
  self.console.print(task_panel)
213
215
  except Exception as e:
214
216
  self.logger.error(f"Error listing tasks: {e!s}")
@@ -218,6 +220,23 @@ class CLI:
218
220
  self.console.print(error_msg)
219
221
  continue
220
222
 
223
+ if user_input.lower() == "done":
224
+ self.logger.debug("User requested completed task list")
225
+ try:
226
+ output = self.todo_shell.list_completed()
227
+ formatted_output = TaskFormatter.format_completed_tasks(output)
228
+ task_panel = PanelFormatter.create_task_panel(
229
+ formatted_output, title="✅ Completed Tasks"
230
+ )
231
+ self.console.print(task_panel)
232
+ except Exception as e:
233
+ self.logger.error(f"Error listing completed tasks: {e!s}")
234
+ error_msg = ResponseFormatter.format_error(
235
+ f"Failed to list completed tasks: {e!s}"
236
+ )
237
+ self.console.print(error_msg)
238
+ continue
239
+
221
240
  self.logger.info(
222
241
  f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
223
242
  )
@@ -225,10 +244,10 @@ class CLI:
225
244
 
226
245
  # Format the response and create a panel
227
246
  formatted_response = ResponseFormatter.format_response(response)
228
-
247
+
229
248
  # Get memory usage
230
249
  memory_usage = self._get_memory_usage()
231
-
250
+
232
251
  # Create response panel with memory usage
233
252
  response_panel = PanelFormatter.create_response_panel(
234
253
  formatted_response, memory_usage=memory_usage
@@ -7,7 +7,6 @@ from typing import Any, Dict, Optional
7
7
  from rich.align import Align
8
8
  from rich.box import ROUNDED
9
9
  from rich.panel import Panel
10
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
11
10
  from rich.table import Table
12
11
  from rich.text import Text
13
12
 
@@ -22,14 +21,13 @@ class TaskFormatter:
22
21
  @staticmethod
23
22
  def format_task_list(raw_tasks: str) -> Text:
24
23
  """
25
- Format a raw task list with unicode characters and numbering.
24
+ Format a raw task list while preserving ANSI color codes from todo.sh.
26
25
 
27
26
  Args:
28
- raw_tasks: Raw task output from todo.sh
29
- title: Title for the task list
27
+ raw_tasks: Raw task output from todo.sh with ANSI codes
30
28
 
31
29
  Returns:
32
- Formatted task list as Rich Text object
30
+ Formatted task list as Rich Text object with preserved ANSI codes
33
31
  """
34
32
  if not raw_tasks.strip():
35
33
  return Text("No tasks found.")
@@ -38,19 +36,15 @@ class TaskFormatter:
38
36
  formatted_text = Text()
39
37
  task_count = 0
40
38
 
41
- # Add header
42
- formatted_text.append("Tasks", style="bold blue")
43
- formatted_text.append("\n\n")
39
+
44
40
 
45
41
  for line in lines:
46
42
  line = line.strip()
47
43
  # Skip empty lines, separators, and todo.sh's own summary line
48
44
  if line and line != "--" and not line.startswith("TODO:"):
49
45
  task_count += 1
50
- # Parse todo.txt format and make it more readable
51
- formatted_task = TaskFormatter._format_single_task(line, task_count)
52
- # Create a Text object that respects ANSI codes
53
- task_text = Text.from_ansi(formatted_task)
46
+ # Preserve the original ANSI codes by using Text.from_ansi directly
47
+ task_text = Text.from_ansi(line)
54
48
  formatted_text.append(task_text)
55
49
  formatted_text.append("\n")
56
50
 
@@ -63,6 +57,48 @@ class TaskFormatter:
63
57
 
64
58
  return formatted_text
65
59
 
60
+
61
+
62
+ @staticmethod
63
+ def format_completed_tasks(raw_tasks: str) -> Text:
64
+ """
65
+ Format a raw completed task list while preserving ANSI color codes from todo.sh.
66
+
67
+ Args:
68
+ raw_tasks: Raw completed task output from todo.sh with ANSI codes
69
+
70
+ Returns:
71
+ Formatted completed task list as Rich Text object with preserved ANSI codes
72
+ """
73
+ if not raw_tasks.strip():
74
+ return Text("No completed tasks found.")
75
+
76
+ lines = raw_tasks.strip().split("\n")
77
+ formatted_text = Text()
78
+ task_count = 0
79
+
80
+
81
+
82
+ for line in lines:
83
+ line = line.strip()
84
+ # Skip empty lines, separators, and todo.sh's own summary line
85
+ if line and line != "--" and not line.startswith("TODO:"):
86
+ task_count += 1
87
+ # Preserve the original ANSI codes by using Text.from_ansi directly
88
+ task_text = Text.from_ansi(line)
89
+ formatted_text.append(task_text)
90
+ formatted_text.append("\n")
91
+
92
+ # Add task count at the end
93
+ if task_count > 0:
94
+ formatted_text.append("\n")
95
+ else:
96
+ formatted_text = Text("No completed tasks found.")
97
+
98
+ return formatted_text
99
+
100
+
101
+
66
102
  @staticmethod
67
103
  def _format_single_task(task_line: str, task_number: int) -> str:
68
104
  """
@@ -101,6 +137,33 @@ class TaskFormatter:
101
137
 
102
138
  return formatted_line
103
139
 
140
+ @staticmethod
141
+ def _format_single_completed_task(task_line: str, task_number: int) -> str:
142
+ """
143
+ Format a single completed task line with unicode characters.
144
+
145
+ Args:
146
+ task_line: Raw completed task line from todo.sh
147
+ task_number: Task number for display
148
+
149
+ Returns:
150
+ Formatted completed task string
151
+ """
152
+ # Parse completed task format: "x 2025-08-29 2025-08-28 Clean cat box @home +chores"
153
+ # The format is: "x completion_date creation_date description"
154
+ parts = task_line.split(
155
+ " ", 2
156
+ ) # Split on first two spaces to separate x, dates, and description
157
+ if len(parts) < 3:
158
+ return f" {task_number:2d} │ │ {task_line}"
159
+
160
+ description = parts[2]
161
+
162
+ # Format with unicode characters
163
+ formatted_line = f" {task_number:2d} │ {description}"
164
+
165
+ return formatted_line
166
+
104
167
  @staticmethod
105
168
  def format_projects(raw_projects: str) -> str:
106
169
  """
@@ -279,6 +342,7 @@ class TableFormatter:
279
342
  ("help", "Show this help message"),
280
343
  ("about", "Show application information"),
281
344
  ("list", "List all tasks (no LLM interaction)"),
345
+ ("done", "List completed tasks (no LLM interaction)"),
282
346
  ("quit", "Exit the application"),
283
347
  ]
284
348
 
@@ -340,28 +404,32 @@ class PanelFormatter:
340
404
  )
341
405
 
342
406
  @staticmethod
343
- def create_task_panel(content: str, title: str = "📋 Current Tasks") -> Panel:
407
+ def create_task_panel(content: str | Text, title: str = "📋 Current Tasks") -> Panel:
344
408
  """Create a panel for displaying task lists."""
345
409
  return Panel(
346
410
  content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
347
411
  )
348
412
 
349
413
  @staticmethod
350
- def create_response_panel(content: str, title: str = "🤖 Assistant", memory_usage: Optional[Text] = None) -> Panel:
414
+ def create_response_panel(
415
+ content: str, title: str = "🤖 Assistant", memory_usage: Optional[Text] = None
416
+ ) -> Panel:
351
417
  """Create a panel for displaying LLM responses."""
352
418
  if memory_usage:
353
419
  # Create the combined content with centered memory usage
420
+ combined_content = Text()
421
+ combined_content.append(content)
422
+ combined_content.append("\n\n")
423
+ combined_content.append("─" * (PANEL_WIDTH - 4)) # Separator line
424
+ combined_content.append("\n")
425
+ combined_content.append(memory_usage)
426
+
354
427
  return Panel(
355
- Align.center(
356
- Text.assemble(
357
- content,
358
- "\n\n",
359
- "─" * (PANEL_WIDTH - 4), # Separator line
360
- "\n",
361
- memory_usage
362
- )
363
- ),
364
- title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
428
+ Align.center(combined_content),
429
+ title=title,
430
+ border_style="dim",
431
+ box=ROUNDED,
432
+ width=PANEL_WIDTH,
365
433
  )
366
434
  else:
367
435
  return Panel(
@@ -415,43 +483,71 @@ class PanelFormatter:
415
483
  )
416
484
 
417
485
  @staticmethod
418
- def create_memory_usage_bar(current_tokens: int, max_tokens: int, current_messages: int, max_messages: int) -> Text:
486
+ def create_memory_usage_bar(
487
+ current_tokens: int, max_tokens: int, current_messages: int, max_messages: int
488
+ ) -> Text:
419
489
  """
420
490
  Create a rich progress bar showing session memory usage.
421
-
491
+
422
492
  Args:
423
493
  current_tokens: Current number of tokens in conversation
424
494
  max_tokens: Maximum allowed tokens
425
495
  current_messages: Current number of messages in conversation
426
496
  max_messages: Maximum allowed messages
427
-
497
+
428
498
  Returns:
429
499
  Rich Text object with memory usage progress bar
430
500
  """
431
501
  # Calculate percentage
432
502
  token_percentage = min(100, (current_tokens / max_tokens) * 100)
433
-
434
- # Determine color based on usage
435
- if token_percentage >= 90:
436
- color = "red"
437
- elif token_percentage >= 75:
438
- color = "yellow"
439
- else:
440
- color = "green"
441
-
442
- # Create the progress bar text
503
+
504
+ # Create the progress bar text with new layout
443
505
  memory_text = Text()
444
- memory_text.append(f"{current_tokens:,}/{max_tokens:,} ", style="dim")
445
-
446
- # Create a simple text-based progress bar
447
- bar_length = 25
506
+
507
+ # Calculate available width (PANEL_WIDTH - 4 for borders = 94 chars)
508
+ # Account for emoji width by using a slightly reduced width
509
+ available_width = 92 # Balance between full width and emoji spacing
510
+
511
+ # Left section: Floppy disk + token count
512
+ left_section = f"💾 {current_tokens:,}/{max_tokens:,}"
513
+ left_width = len(left_section)
514
+
515
+ # Right section: Message count + floppy disk
516
+ right_section = f"{current_messages}/{max_messages} 💾"
517
+ right_width = len(right_section)
518
+
519
+ # Center section: Progress bar (2x wider = 50 chars) + percentage
520
+ bar_length = 50
448
521
  token_filled = int((token_percentage / 100) * bar_length)
449
522
  token_bar = "█" * token_filled + "░" * (bar_length - token_filled)
450
- memory_text.append(f"[{token_bar}] ", style="dim")
451
- memory_text.append(f"{token_percentage:.1f}%", style="dim")
452
-
453
- # Add message count without progress bar
454
- memory_text.append(" | ", style="dim")
455
- memory_text.append(f"{current_messages}/{max_messages}", style="dim")
456
-
523
+ center_section = f"[{token_bar}] {token_percentage:.1f}%"
524
+ center_width = len(center_section)
525
+
526
+ # Calculate spacing to center the progress bar
527
+ total_content_width = left_width + center_width + right_width
528
+ remaining_space = available_width - total_content_width
529
+
530
+ # Ensure we have enough space, if not, reduce the progress bar length
531
+ if remaining_space < 0:
532
+ # Reduce bar length to fit everything
533
+ excess = abs(remaining_space)
534
+ bar_length = max(30, 50 - excess) # Minimum 30 chars
535
+ token_filled = int((token_percentage / 100) * bar_length)
536
+ token_bar = "█" * token_filled + "░" * (bar_length - token_filled)
537
+ center_section = f"[{token_bar}] {token_percentage:.1f}%"
538
+ center_width = len(center_section)
539
+ total_content_width = left_width + center_width + right_width
540
+ remaining_space = available_width - total_content_width
541
+
542
+ # Distribute remaining space equally for symmetrical layout
543
+ left_spacing = remaining_space // 2
544
+ right_spacing = remaining_space - left_spacing
545
+
546
+ # Build the final layout
547
+ memory_text.append(left_section, style="dim")
548
+ memory_text.append(" " * left_spacing)
549
+ memory_text.append(center_section, style="dim")
550
+ memory_text.append(" " * right_spacing)
551
+ memory_text.append(right_section, style="dim")
552
+
457
553
  return memory_text
@@ -2,6 +2,7 @@
2
2
  Tool definitions and schemas for LLM function calling.
3
3
  """
4
4
 
5
+ import subprocess
5
6
  from typing import Any, Callable, Dict, List, Optional
6
7
 
7
8
  try:
@@ -62,8 +63,12 @@ class ToolCallHandler:
62
63
  "1) User wants to see their tasks, "
63
64
  "2) You need to find a specific task by description, "
64
65
  "3) You need to check for potential duplicates before adding new tasks, "
65
- "4) You need to understand the current state before making changes. "
66
+ "4) You need to understand the current state before making changes, "
67
+ "5) User says they finished/completed a task and you need to find the matching task. "
66
68
  "CRITICAL: ALWAYS use this before add_task() to check for similar existing tasks. "
69
+ "CRITICAL: ALWAYS use this when user mentions task completion to find the correct task number. "
70
+ "COMPLETION INTELLIGENCE: When searching for completion matches, if exactly one task clearly "
71
+ "matches the user's description, proceed to complete it immediately without asking for confirmation. "
67
72
  "IMPORTANT: When presenting the results to the user, convert the raw todo.txt format "
68
73
  "into conversational language. Do not show the raw format like '(A) task +project @context'. "
69
74
  "STRATEGIC CONTEXT: This is the primary discovery tool - call this FIRST when you need to "
@@ -76,7 +81,7 @@ class ToolCallHandler:
76
81
  "type": "string",
77
82
  "description": (
78
83
  "Optional filter string (e.g., '+work', '@office', '(A)') - "
79
- "use when you want to see only specific tasks"
84
+ "use when you want to see only specific tasks or when searching for completion matches"
80
85
  ),
81
86
  }
82
87
  },
@@ -166,7 +171,10 @@ class ToolCallHandler:
166
171
  "use list_tasks() and list_completed_tasks() to check for potential duplicates. Look for tasks with "
167
172
  "similar descriptions, keywords, or intent. If you find similar tasks, "
168
173
  "ask the user if they want to add a new task or modify an existing one. "
169
- "If project or context is ambiguous, use discovery tools first. "
174
+ "AUTOMATIC INFERENCE: When project, context, or due date is not specified, automatically infer appropriate tags "
175
+ "and due dates based on the task content, natural language expressions, task nature, calendar context, and existing patterns. "
176
+ "DUE DATE INFERENCE: Extract temporal expressions and use common sense to infer appropriate due dates based on task type, "
177
+ "work patterns, personal schedules, and existing task due date patterns. Only ask for clarification when genuinely ambiguous. "
170
178
  "Always provide a complete, natural response to the user. "
171
179
  "STRATEGIC CONTEXT: This is a modification tool - call this LAST after using "
172
180
  "discovery tools (list_tasks, list_projects, list_contexts list_completed_tasks) "
@@ -193,7 +201,7 @@ class ToolCallHandler:
193
201
  },
194
202
  "due": {
195
203
  "type": "string",
196
- "description": "Optional due date in YYYY-MM-DD format",
204
+ "description": "Optional due date in YYYY-MM-DD format. Automatically inferred from natural language expressions like 'tomorrow', 'next week', 'by Friday', 'urgent', 'asap'",
197
205
  },
198
206
  },
199
207
  "required": ["description"],
@@ -206,11 +214,13 @@ class ToolCallHandler:
206
214
  "name": "complete_task",
207
215
  "description": (
208
216
  "Mark a specific task as complete by its line number. IMPORTANT: "
209
- "Before completing, use list_completed_tasks() to check if it's already done. "
210
- "If multiple tasks match the description, ask the user to clarify which one. "
217
+ "When user says they finished/completed a task, FIRST use list_tasks() to find matching tasks, "
218
+ "then list_completed_tasks() to verify it's not already done. "
219
+ "COMPLETION LOGIC: If user's statement clearly matches exactly one task, complete it immediately. "
220
+ "If multiple tasks match, show numbered options and ask for clarification. "
221
+ "If the match is ambiguous, ask for confirmation. "
211
222
  "STRATEGIC CONTEXT: This is a modification tool - call this LAST after using "
212
- "discovery tools (list_tasks, list_completed_tasks) "
213
- "to verify the task exists and hasn't already been completed."
223
+ "discovery tools (list_tasks, list_completed_tasks) to verify the task exists and status."
214
224
  ),
215
225
  "parameters": {
216
226
  "type": "object",
@@ -434,8 +444,69 @@ class ToolCallHandler:
434
444
  "parameters": {"type": "object", "properties": {}, "required": []},
435
445
  },
436
446
  },
447
+ {
448
+ "type": "function",
449
+ "function": {
450
+ "name": "get_calendar",
451
+ "description": (
452
+ "Get a calendar for a specific month and year using the system 'cal' command. "
453
+ "Use this when: "
454
+ "1) User asks to see a calendar for a specific month/year, "
455
+ "2) User wants to plan tasks around specific dates, "
456
+ "3) User needs to see what day of the week a date falls on, "
457
+ "4) User wants to visualize the current month or upcoming months. "
458
+ "The calendar will show the month in a traditional calendar format with days of the week. "
459
+ "IMPORTANT: When displaying the calendar output, present it directly without wrapping in backticks or code blocks. "
460
+ "The calendar should be displayed as plain text in the conversation."
461
+ ),
462
+ "parameters": {
463
+ "type": "object",
464
+ "properties": {
465
+ "month": {
466
+ "type": "integer",
467
+ "description": "Month number (1-12, where 1=January, 12=December)",
468
+ "minimum": 1,
469
+ "maximum": 12,
470
+ },
471
+ "year": {
472
+ "type": "integer",
473
+ "description": "Year (4-digit format, e.g., 2025)",
474
+ "minimum": 1900,
475
+ "maximum": 2100,
476
+ },
477
+ },
478
+ "required": ["month", "year"],
479
+ },
480
+ },
481
+ },
437
482
  ]
438
483
 
484
+ def _get_calendar(self, month: int, year: int) -> str:
485
+ """
486
+ Get a calendar for the specified month and year using the system 'cal' command.
487
+
488
+ Args:
489
+ month: Month number (1-12)
490
+ year: Year (4-digit format)
491
+
492
+ Returns:
493
+ Calendar output as a string
494
+ """
495
+ try:
496
+ # Use the cal command with specific month and year
497
+ result = subprocess.run(
498
+ ["cal", str(month), str(year)],
499
+ capture_output=True,
500
+ text=True,
501
+ check=True,
502
+ )
503
+ return result.stdout.strip()
504
+ except (subprocess.SubprocessError, FileNotFoundError):
505
+ # Fallback to Python calendar module
506
+ import calendar
507
+
508
+ return calendar.month(year, month).strip()
509
+
439
510
  def _format_tool_signature(self, tool_name: str, arguments: Dict[str, Any]) -> str:
440
511
  """Format tool signature with parameters for logging."""
441
512
  if not arguments:
@@ -503,6 +574,7 @@ class ToolCallHandler:
503
574
  "move_task": self.todo_manager.move_task,
504
575
  "archive_tasks": self.todo_manager.archive_tasks,
505
576
  "deduplicate_tasks": self.todo_manager.deduplicate_tasks,
577
+ "get_calendar": self._get_calendar,
506
578
  }
507
579
 
508
580
  if tool_name not in method_map:
@@ -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
@@ -1,28 +1,29 @@
1
1
  todo_agent/__init__.py,sha256=RUowhd14r3tqB_7rl83unGV8oBjra3UOIl7jix-33fk,254
2
- todo_agent/_version.py,sha256=NRw4Jle4n9v_DD2wtplRqflGCvX8OU5eAjycYY0vY3Y,704
2
+ todo_agent/_version.py,sha256=9wrJ_4Dlc0arUzKiaIqvTY85rMJma3eb1nNlF3uHAxU,704
3
3
  todo_agent/main.py,sha256=-ryhMm4c4sz4e4anXI8B-CYnpEh5HIkmnYcnGxcWHDk,1628
4
4
  todo_agent/core/__init__.py,sha256=QAZ4it63pXv5-DxtNcuSAmg7ZnCY5ackI5yycvKHr9I,365
5
5
  todo_agent/core/conversation_manager.py,sha256=gSCcX356UJ0T3FCTS1q0fOud0ytFKXptf9RyKjzpTYI,11640
6
6
  todo_agent/core/exceptions.py,sha256=cPvvkIbKdI7l51wC7cE-ZxUi54P3nf2m7x2lMNMRFYM,399
7
- todo_agent/core/todo_manager.py,sha256=wV-J_E_aK7yRNT-WbILKJgqBFhbcXDZa6Pb4IjbgGkU,7974
7
+ todo_agent/core/todo_manager.py,sha256=Dyc5NbDd_u21nJlS-C8KxefGJpEcHOLtL21qqQEit2Q,9142
8
8
  todo_agent/infrastructure/__init__.py,sha256=SGbHXgzq6U1DMgOfWPMsWEK99zjPSF-6gzy7xqc5fsI,284
9
+ todo_agent/infrastructure/calendar_utils.py,sha256=HmF0ykXF_6GbdoJvZLIv6fKwT6ipixoywdTMkIXmkGU,1871
9
10
  todo_agent/infrastructure/config.py,sha256=zyp6qOlg1nN_awphivlgGNBE6fL0Hf66YgvWxR8ldyQ,2117
10
- todo_agent/infrastructure/inference.py,sha256=XCM18hpHwWxDLV-31yO_UOYbqQ7lRjGz-PQduyp6Xio,10289
11
+ todo_agent/infrastructure/inference.py,sha256=J6i9jtzOVo2Yy3e6yAUBH22ik3OqHs1I0zrADhs4IRk,10676
11
12
  todo_agent/infrastructure/llm_client.py,sha256=ZoObyqaRP6i_eqGYGfJWGeWTJ-VNxpY70ay04vt2v_E,1390
12
- todo_agent/infrastructure/llm_client_factory.py,sha256=SmPHNS4QifFI8CmsAz7uTcYjTdFzq8x9jVNybOmyerk,1884
13
+ todo_agent/infrastructure/llm_client_factory.py,sha256=-tktnVOIF7B45WR7AuLoi7MKnEyuM8lgg1jjc4T1FhM,1929
13
14
  todo_agent/infrastructure/logger.py,sha256=2ykG_0lyzmEGxDF6ZRl1qiTUGDuFeQgzv4Na6vRmXcM,4110
14
15
  todo_agent/infrastructure/ollama_client.py,sha256=6WsjSftsHNt-CeScK6GSrJ_CMe80OiATT3idcJgPCBk,5654
15
16
  todo_agent/infrastructure/openrouter_client.py,sha256=GVOJTzPDOKNdHEu-y-HQygMcLvrFw1KB6NZU_rJR3-c,6859
16
17
  todo_agent/infrastructure/todo_shell.py,sha256=z6kqUKDX-i4DfYJKoOLiPLCp8y6m1HdTDLHTvmLpzMc,5801
17
18
  todo_agent/infrastructure/token_counter.py,sha256=PCKheOVJbp1s89yhh_i6iKgURMt9mVoYkwjQJCc2xCE,4958
18
- todo_agent/infrastructure/prompts/system_prompt.txt,sha256=uCb6yz3uDQdwcB8HJcF0y1_1b75oRtRnCMMHQLHI3NI,2415
19
+ todo_agent/infrastructure/prompts/system_prompt.txt,sha256=S4oJXNaIhyzCydVOF7VRbg9Be_l51DHiMXEUHBzhOwY,5830
19
20
  todo_agent/interface/__init__.py,sha256=vDD3rQu4qDkpvVwGVtkDzE1M4IiSHYzTif4GbYSFWaI,457
20
- todo_agent/interface/cli.py,sha256=FacwTDjLP60qAQ_BsM_7_jM9liYJpZpRdXpBNwNuO2Y,11060
21
- todo_agent/interface/formatters.py,sha256=QT6IgBVX6ghL8UWH54KFtJY9n9GbrPvOizjCN8emMGg,15503
22
- todo_agent/interface/tools.py,sha256=mlPPLVwECYrTtOX8ysIORRfErIOJl43qlTvfdpy2Vbs,28559
23
- todo_agent-0.2.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
24
- todo_agent-0.2.4.dist-info/METADATA,sha256=fYnY58TYzNuQtiu2WBr3fRJwJEIh6bbt4gA2lrNC_9A,10047
25
- todo_agent-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- todo_agent-0.2.4.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
27
- todo_agent-0.2.4.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
28
- todo_agent-0.2.4.dist-info/RECORD,,
21
+ todo_agent/interface/cli.py,sha256=eEtYXrizgdN00jA5Po7JOFi0um0Lje3YpslzQXs9XO4,11992
22
+ todo_agent/interface/formatters.py,sha256=Oc7ynL7vpb4i8f-XQM38gJlqTOVZfzyTBwWceeMHV_Y,18912
23
+ todo_agent/interface/tools.py,sha256=RkxsyqYbp0QYwtnJMXixBf727Atcp8DY6dm6949DTMY,32739
24
+ todo_agent-0.2.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
+ todo_agent-0.2.5.dist-info/METADATA,sha256=Hz4s2k3T2fNMmyscFirCqfr7U5WkWnTVVkxgxzWtr9M,10047
26
+ todo_agent-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ todo_agent-0.2.5.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
28
+ todo_agent-0.2.5.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
29
+ todo_agent-0.2.5.dist-info/RECORD,,