todo-agent 0.3.3__py3-none-any.whl → 0.3.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.3.3'
32
- __version_tuple__ = version_tuple = (0, 3, 3)
31
+ __version__ = version = '0.3.5'
32
+ __version_tuple__ = version_tuple = (0, 3, 5)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -12,86 +12,126 @@ class TodoManager:
12
12
  def __init__(self, todo_shell: Any) -> None:
13
13
  self.todo_shell = todo_shell
14
14
 
15
- def add_task(
16
- self,
17
- description: str,
18
- priority: Optional[str] = None,
19
- project: Optional[str] = None,
20
- context: Optional[str] = None,
21
- due: Optional[str] = None,
22
- duration: Optional[str] = None,
23
- ) -> str:
24
- """Add new task with explicit project/context parameters."""
25
- # Validate and sanitize inputs
26
- if priority and not (
27
- len(priority) == 1 and priority.isalpha() and priority.isupper()
28
- ):
15
+ # ============================================================================
16
+ # VALIDATION METHODS
17
+ # ============================================================================
18
+
19
+ def _normalize_empty_to_none(self, value: Optional[str]) -> Optional[str]:
20
+ """Convert empty strings to None for consistent handling."""
21
+ if value is None:
22
+ return None
23
+ if isinstance(value, str) and not value.strip():
24
+ return None
25
+ return value
26
+
27
+ def _validate_date_format(
28
+ self, date_str: Optional[str], field_name: str = "date"
29
+ ) -> None:
30
+ """Validate date format (YYYY-MM-DD)."""
31
+ if date_str is None or not date_str.strip():
32
+ return
33
+ try:
34
+ datetime.strptime(date_str, "%Y-%m-%d")
35
+ except ValueError:
36
+ raise ValueError(
37
+ f"Invalid {field_name} format '{date_str}'. Must be YYYY-MM-DD."
38
+ )
39
+
40
+ def _validate_priority(self, priority: Optional[str]) -> None:
41
+ """Validate priority format (single uppercase letter A-Z)."""
42
+ if priority is None:
43
+ return
44
+ if not (len(priority) == 1 and priority.isalpha() and priority.isupper()):
29
45
  raise ValueError(
30
46
  f"Invalid priority '{priority}'. Must be a single uppercase letter (A-Z)."
31
47
  )
32
48
 
33
- if project:
34
- # Remove any existing + symbols to prevent duplication
35
- project = project.strip().lstrip("+")
36
- if not project:
37
- raise ValueError(
38
- "Project name cannot be empty after removing + symbol."
39
- )
49
+ def _validate_parent_number(self, parent_number: Optional[int]) -> None:
50
+ """Validate parent task number."""
51
+ if parent_number is not None and (
52
+ not isinstance(parent_number, int) or parent_number <= 0
53
+ ):
54
+ raise ValueError(
55
+ f"Invalid parent_number '{parent_number}'. Must be a positive integer."
56
+ )
40
57
 
41
- if context:
42
- # Remove any existing @ symbols to prevent duplication
43
- context = context.strip().lstrip("@")
44
- if not context:
45
- raise ValueError(
46
- "Context name cannot be empty after removing @ symbol."
47
- )
58
+ def _validate_duration(self, duration: Optional[str]) -> None:
59
+ """Validate duration format (e.g., '30m', '2h', '1d')."""
60
+ if duration is None:
61
+ return
48
62
 
49
- if due:
50
- # Basic date format validation
51
- try:
52
- datetime.strptime(due, "%Y-%m-%d")
53
- except ValueError:
54
- raise ValueError(
55
- f"Invalid due date format '{due}'. Must be YYYY-MM-DD."
56
- )
63
+ if not isinstance(duration, str) or not duration.strip():
64
+ raise ValueError("Duration must be a non-empty string.")
57
65
 
58
- if duration is not None:
59
- # Validate duration format (e.g., "30m", "2h", "1d")
60
- if not duration or not isinstance(duration, str):
61
- raise ValueError("Duration must be a non-empty string.")
66
+ if not any(duration.endswith(unit) for unit in ["m", "h", "d"]):
67
+ raise ValueError(
68
+ f"Invalid duration format '{duration}'. Must end with m (minutes), h (hours), or d (days)."
69
+ )
62
70
 
63
- # Check if duration ends with a valid unit
64
- if not any(duration.endswith(unit) for unit in ["m", "h", "d"]):
65
- raise ValueError(
66
- f"Invalid duration format '{duration}'. Must end with m (minutes), h (hours), or d (days)."
67
- )
71
+ value = duration[:-1]
72
+ if not value:
73
+ raise ValueError("Duration value cannot be empty.")
68
74
 
69
- # Extract the numeric part and validate it
70
- value = duration[:-1]
71
- if not value:
72
- raise ValueError("Duration value cannot be empty.")
73
-
74
- try:
75
- # Check if the value is a valid positive number
76
- numeric_value = float(value)
77
- if numeric_value <= 0:
78
- raise ValueError("Duration value must be positive.")
79
- except ValueError:
80
- raise ValueError(
81
- f"Invalid duration value '{value}'. Must be a positive number."
82
- )
75
+ try:
76
+ numeric_value = float(value)
77
+ if numeric_value <= 0:
78
+ raise ValueError("Duration value must be positive.")
79
+ except ValueError:
80
+ raise ValueError(
81
+ f"Invalid duration value '{value}'. Must be a positive number."
82
+ )
83
83
 
84
- # Build the full task description with priority, project, and context
84
+ def _clean_project_name(self, project: Optional[str]) -> Optional[str]:
85
+ """Clean and validate project name."""
86
+ if project is None:
87
+ return None
88
+ if not project.strip():
89
+ return None
90
+ clean_project = project.strip().lstrip("+")
91
+ if not clean_project:
92
+ raise ValueError("Project name cannot be empty after removing + symbol.")
93
+ return clean_project
94
+
95
+ def _clean_context_name(self, context: Optional[str]) -> Optional[str]:
96
+ """Clean and validate context name."""
97
+ if context is None:
98
+ return None
99
+ if not context.strip():
100
+ return "" # Return empty string for empty input (used by set_context for removal)
101
+ clean_context = context.strip().lstrip("@")
102
+ if not clean_context:
103
+ raise ValueError("Context name cannot be empty after removing @ symbol.")
104
+ return clean_context
105
+
106
+ # ============================================================================
107
+ # TASK BUILDING AND UTILITY METHODS
108
+ # ============================================================================
109
+
110
+ def _build_task_description(
111
+ self,
112
+ description: str,
113
+ priority: Optional[str] = None,
114
+ project: Optional[str] = None,
115
+ context: Optional[str] = None,
116
+ due: Optional[str] = None,
117
+ duration: Optional[str] = None,
118
+ parent_number: Optional[int] = None,
119
+ ) -> str:
120
+ """Build full task description with all components."""
85
121
  full_description = description
86
122
 
87
123
  if priority:
88
124
  full_description = f"({priority}) {full_description}"
89
125
 
90
126
  if project:
91
- full_description = f"{full_description} +{project}"
127
+ clean_project = self._clean_project_name(project)
128
+ if clean_project: # Only add if not None/empty after cleaning
129
+ full_description = f"{full_description} +{clean_project}"
92
130
 
93
131
  if context:
94
- full_description = f"{full_description} @{context}"
132
+ clean_context = self._clean_context_name(context)
133
+ if clean_context: # Only add if not None/empty after cleaning
134
+ full_description = f"{full_description} @{clean_context}"
95
135
 
96
136
  if due:
97
137
  full_description = f"{full_description} due:{due}"
@@ -99,6 +139,86 @@ class TodoManager:
99
139
  if duration:
100
140
  full_description = f"{full_description} duration:{duration}"
101
141
 
142
+ if parent_number:
143
+ full_description = f"{full_description} parent:{parent_number}"
144
+
145
+ return full_description
146
+
147
+ def _format_task_operation_response(
148
+ self, operation: str, task_number: int, result: str, additional_info: str = ""
149
+ ) -> str:
150
+ """Format consistent task operation response messages."""
151
+ # Handle special cases for backward compatibility with tests
152
+ if operation == "Set due date" and additional_info:
153
+ return f"Set due date {additional_info} for task {task_number}: {result}"
154
+ elif operation == "Set context" and additional_info:
155
+ return f"Set context {additional_info} for task {task_number}: {result}"
156
+ elif operation == "Set priority" and additional_info:
157
+ return f"Set priority {additional_info} for task {task_number}: {result}"
158
+ else:
159
+ base_message = f"{operation} task {task_number}"
160
+ if additional_info:
161
+ base_message += f" ({additional_info})"
162
+ return f"{base_message}: {result}"
163
+
164
+ def _handle_empty_result(self, result: str, empty_message: str) -> str:
165
+ """Handle empty results with consistent messaging."""
166
+ return empty_message if not result.strip() else result
167
+
168
+ def _find_task_number_by_description(self, description: str) -> int:
169
+ """Find task number by description in the current task list."""
170
+ tasks = self.todo_shell.list_tasks()
171
+ task_lines = [line.strip() for line in tasks.split("\n") if line.strip()]
172
+
173
+ if not task_lines:
174
+ raise RuntimeError("Failed to add task - no tasks found after addition")
175
+
176
+ import re
177
+
178
+ for line in reversed(task_lines):
179
+ if description in line:
180
+ match = re.match(r"^(\d+)", line)
181
+ if match:
182
+ return int(match.group(1))
183
+
184
+ raise RuntimeError(
185
+ f"Could not find task with description '{description}' after adding it. "
186
+ f"This indicates a serious issue with task matching."
187
+ )
188
+
189
+ # ============================================================================
190
+ # CORE CRUD OPERATIONS
191
+ # ============================================================================
192
+
193
+ def add_task(
194
+ self,
195
+ description: str,
196
+ priority: Optional[str] = None,
197
+ project: Optional[str] = None,
198
+ context: Optional[str] = None,
199
+ due: Optional[str] = None,
200
+ duration: Optional[str] = None,
201
+ parent_number: Optional[int] = None,
202
+ ) -> str:
203
+ """Add new task with explicit project/context parameters."""
204
+ # Normalize empty strings to None for optional parameters
205
+ priority = self._normalize_empty_to_none(priority)
206
+ project = self._normalize_empty_to_none(project)
207
+ context = self._normalize_empty_to_none(context)
208
+ due = self._normalize_empty_to_none(due)
209
+ # Note: duration is not normalized - empty strings should raise validation errors
210
+
211
+ # Validate inputs
212
+ self._validate_priority(priority)
213
+ self._validate_date_format(due, "due date")
214
+ self._validate_duration(duration)
215
+ self._validate_parent_number(parent_number)
216
+
217
+ # Build the full task description
218
+ full_description = self._build_task_description(
219
+ description, priority, project, context, due, duration, parent_number
220
+ )
221
+
102
222
  self.todo_shell.add(full_description)
103
223
  return f"Added task: {full_description}"
104
224
 
@@ -107,50 +227,53 @@ class TodoManager:
107
227
  ) -> str:
108
228
  """List tasks with optional filtering."""
109
229
  result = self.todo_shell.list_tasks(filter, suppress_color=suppress_color)
110
- if not result.strip():
111
- return "No tasks found."
112
-
113
- # Return the raw todo.txt format for the LLM to format conversationally
114
- # The LLM will convert this into natural language in its response
115
- return result
230
+ return self._handle_empty_result(result, "No tasks found.")
116
231
 
117
232
  def complete_task(self, task_number: int) -> str:
118
233
  """Mark task complete by line number."""
119
234
  result = self.todo_shell.complete(task_number)
120
- return f"Completed task {task_number}: {result}"
235
+ return self._format_task_operation_response("Completed", task_number, result)
121
236
 
122
237
  def replace_task(self, task_number: int, new_description: str) -> str:
123
238
  """Replace entire task content."""
124
239
  result = self.todo_shell.replace(task_number, new_description)
125
- return f"Replaced task {task_number}: {result}"
240
+ return self._format_task_operation_response("Replaced", task_number, result)
126
241
 
127
242
  def append_to_task(self, task_number: int, text: str) -> str:
128
243
  """Add text to end of existing task."""
129
244
  result = self.todo_shell.append(task_number, text)
130
- return f"Appended to task {task_number}: {result}"
245
+ return self._format_task_operation_response("Appended to", task_number, result)
131
246
 
132
247
  def prepend_to_task(self, task_number: int, text: str) -> str:
133
248
  """Add text to beginning of existing task."""
134
249
  result = self.todo_shell.prepend(task_number, text)
135
- return f"Prepended to task {task_number}: {result}"
250
+ return self._format_task_operation_response("Prepended to", task_number, result)
136
251
 
137
252
  def delete_task(self, task_number: int, term: Optional[str] = None) -> str:
138
253
  """Delete entire task or specific term from task."""
139
254
  result = self.todo_shell.delete(task_number, term)
140
- if term:
141
- return f"Removed '{term}' from task {task_number}: {result}"
255
+ if term is not None:
256
+ return self._format_task_operation_response(
257
+ "Removed", task_number, result, f"'{term}'"
258
+ )
142
259
  else:
143
- return f"Deleted task {task_number}: {result}"
260
+ return self._format_task_operation_response("Deleted", task_number, result)
144
261
 
145
262
  def set_priority(self, task_number: int, priority: str) -> str:
146
263
  """Set or change task priority (A-Z)."""
264
+ priority = self._normalize_empty_to_none(priority)
265
+ self._validate_priority(priority)
147
266
  result = self.todo_shell.set_priority(task_number, priority)
148
- return f"Set priority {priority} for task {task_number}: {result}"
267
+ return self._format_task_operation_response(
268
+ "Set priority", task_number, result, priority or ""
269
+ )
149
270
 
150
271
  def remove_priority(self, task_number: int) -> str:
151
272
  """Remove priority from task."""
152
273
  result = self.todo_shell.remove_priority(task_number)
153
- return f"Removed priority from task {task_number}: {result}"
274
+ return self._format_task_operation_response(
275
+ "Removed priority from", task_number, result
276
+ )
154
277
 
155
278
  def set_due_date(self, task_number: int, due_date: str) -> str:
156
279
  """
@@ -164,19 +287,17 @@ class TodoManager:
164
287
  Confirmation message with the updated task
165
288
  """
166
289
  # Validate due date format only if not empty
167
- if due_date.strip():
168
- try:
169
- datetime.strptime(due_date, "%Y-%m-%d")
170
- except ValueError:
171
- raise ValueError(
172
- f"Invalid due date format '{due_date}'. Must be YYYY-MM-DD."
173
- )
290
+ self._validate_date_format(due_date, "due date")
174
291
 
175
292
  result = self.todo_shell.set_due_date(task_number, due_date)
176
293
  if due_date.strip():
177
- return f"Set due date {due_date} for task {task_number}: {result}"
294
+ return self._format_task_operation_response(
295
+ "Set due date", task_number, result, due_date
296
+ )
178
297
  else:
179
- return f"Removed due date from task {task_number}: {result}"
298
+ return self._format_task_operation_response(
299
+ "Removed due date from", task_number, result
300
+ )
180
301
 
181
302
  def set_context(self, task_number: int, context: str) -> str:
182
303
  """
@@ -190,20 +311,17 @@ class TodoManager:
190
311
  Confirmation message with the updated task
191
312
  """
192
313
  # Validate context name if not empty
193
- if context.strip():
194
- # Remove any existing @ symbols to prevent duplication
195
- clean_context = context.strip().lstrip("@")
196
- if not clean_context:
197
- raise ValueError(
198
- "Context name cannot be empty after removing @ symbol."
199
- )
314
+ clean_context = self._clean_context_name(context) if context.strip() else ""
200
315
 
201
316
  result = self.todo_shell.set_context(task_number, context)
202
317
  if context.strip():
203
- clean_context = context.strip().lstrip("@")
204
- return f"Set context @{clean_context} for task {task_number}: {result}"
318
+ return self._format_task_operation_response(
319
+ "Set context", task_number, result, f"@{clean_context}"
320
+ )
205
321
  else:
206
- return f"Removed context from task {task_number}: {result}"
322
+ return self._format_task_operation_response(
323
+ "Removed context from", task_number, result
324
+ )
207
325
 
208
326
  def set_project(self, task_number: int, projects: list) -> str:
209
327
  """
@@ -223,12 +341,7 @@ class TodoManager:
223
341
  if projects:
224
342
  for project in projects:
225
343
  if project.strip() and not project.startswith("-"):
226
- # Remove any existing + symbols to prevent duplication
227
- clean_project = project.strip().lstrip("+")
228
- if not clean_project:
229
- raise ValueError(
230
- "Project name cannot be empty after removing + symbol."
231
- )
344
+ self._clean_project_name(project)
232
345
  elif project.startswith("-"):
233
346
  clean_project = project[1:].strip().lstrip("+")
234
347
  if not clean_project:
@@ -239,7 +352,9 @@ class TodoManager:
239
352
  result = self.todo_shell.set_project(task_number, projects)
240
353
 
241
354
  if not projects:
242
- return f"No project changes made to task {task_number}: {result}"
355
+ return self._format_task_operation_response(
356
+ "No project changes made to", task_number, result
357
+ )
243
358
  else:
244
359
  # Build operation description
245
360
  operations = []
@@ -255,24 +370,52 @@ class TodoManager:
255
370
  operations.append(f"added +{clean_project}")
256
371
 
257
372
  if not operations:
258
- return f"No project changes made to task {task_number}: {result}"
373
+ return self._format_task_operation_response(
374
+ "No project changes made to", task_number, result
375
+ )
259
376
  else:
260
377
  operation_desc = ", ".join(operations)
261
- return f"Updated projects for task {task_number} ({operation_desc}): {result}"
378
+ return self._format_task_operation_response(
379
+ "Updated projects for", task_number, result, operation_desc
380
+ )
381
+
382
+ def set_parent(self, task_number: int, parent_number: Optional[int]) -> str:
383
+ """
384
+ Set or update parent task number for a task by intelligently rewriting it.
385
+
386
+ Args:
387
+ task_number: The task number to modify
388
+ parent_number: Parent task number, or None to remove parent
389
+
390
+ Returns:
391
+ Confirmation message with the updated task
392
+ """
393
+ # Validate parent_number if provided
394
+ self._validate_parent_number(parent_number)
395
+
396
+ result = self.todo_shell.set_parent(task_number, parent_number)
397
+ if parent_number is not None:
398
+ return self._format_task_operation_response(
399
+ "Set parent task", task_number, result, str(parent_number)
400
+ )
401
+ else:
402
+ return self._format_task_operation_response(
403
+ "Removed parent from", task_number, result
404
+ )
405
+
406
+ # ============================================================================
407
+ # LISTING AND QUERY METHODS
408
+ # ============================================================================
262
409
 
263
410
  def list_projects(self, suppress_color: bool = True, **kwargs: Any) -> str:
264
411
  """List all available projects in todo.txt."""
265
412
  result = self.todo_shell.list_projects(suppress_color=suppress_color)
266
- if not result.strip():
267
- return "No projects found."
268
- return result
413
+ return self._handle_empty_result(result, "No projects found.")
269
414
 
270
415
  def list_contexts(self, suppress_color: bool = True, **kwargs: Any) -> str:
271
416
  """List all available contexts in todo.txt."""
272
417
  result = self.todo_shell.list_contexts(suppress_color=suppress_color)
273
- if not result.strip():
274
- return "No contexts found."
275
- return result
418
+ return self._handle_empty_result(result, "No contexts found.")
276
419
 
277
420
  def list_completed_tasks(
278
421
  self,
@@ -335,16 +478,22 @@ class TodoManager:
335
478
  result = self.todo_shell.list_completed(
336
479
  combined_filter, suppress_color=suppress_color
337
480
  )
338
- if not result.strip():
339
- return "No completed tasks found matching the criteria."
340
- return result
481
+ return self._handle_empty_result(
482
+ result, "No completed tasks found matching the criteria."
483
+ )
484
+
485
+ # ============================================================================
486
+ # UTILITY OPERATIONS
487
+ # ============================================================================
341
488
 
342
489
  def move_task(
343
490
  self, task_number: int, destination: str, source: Optional[str] = None
344
491
  ) -> str:
345
492
  """Move task from source to destination file."""
346
493
  result = self.todo_shell.move(task_number, destination, source)
347
- return f"Moved task {task_number} to {destination}: {result}"
494
+ return self._format_task_operation_response(
495
+ "Moved", task_number, result, f"to {destination}"
496
+ )
348
497
 
349
498
  def archive_tasks(self, **kwargs: Any) -> str:
350
499
  """Archive completed tasks."""
@@ -363,12 +512,17 @@ class TodoManager:
363
512
  timezone = now.astimezone().tzinfo
364
513
  return f"Current date and time: {now.strftime('%Y-%m-%d %H:%M:%S')} {timezone} ({now.strftime('%A, %B %d, %Y at %I:%M %p')}) - Week {week_number}"
365
514
 
366
- def created_completed_task(
515
+ # ============================================================================
516
+ # ADVANCED OPERATIONS
517
+ # ============================================================================
518
+
519
+ def create_completed_task(
367
520
  self,
368
521
  description: str,
369
522
  completion_date: Optional[str] = None,
370
523
  project: Optional[str] = None,
371
524
  context: Optional[str] = None,
525
+ parent_number: Optional[int] = None,
372
526
  ) -> str:
373
527
  """
374
528
  Create a task and immediately mark it as completed.
@@ -381,74 +535,48 @@ class TodoManager:
381
535
  completion_date: Completion date in YYYY-MM-DD format (defaults to today)
382
536
  project: Optional project name (without the + symbol)
383
537
  context: Optional context name (without the @ symbol)
538
+ parent_number: Optional parent task number (required for subtasks)
384
539
 
385
540
  Returns:
386
541
  Confirmation message with the completed task details
387
542
  """
543
+ # Normalize empty strings to None
544
+ project = self._normalize_empty_to_none(project)
545
+ context = self._normalize_empty_to_none(context)
546
+
388
547
  # Set default completion date to today if not provided
389
548
  if not completion_date:
390
549
  completion_date = datetime.now().strftime("%Y-%m-%d")
391
550
 
392
- # Validate completion date format
393
- try:
394
- datetime.strptime(completion_date, "%Y-%m-%d")
395
- except ValueError:
396
- raise ValueError(
397
- f"Invalid completion date format '{completion_date}'. Must be YYYY-MM-DD."
398
- )
399
-
400
- # Build the task description with project and context
401
- full_description = description
551
+ # Validate inputs
552
+ self._validate_date_format(completion_date, "completion date")
553
+ self._validate_parent_number(parent_number)
402
554
 
403
- if project:
404
- # Remove any existing + symbols to prevent duplication
405
- clean_project = project.strip().lstrip("+")
406
- if not clean_project:
407
- raise ValueError(
408
- "Project name cannot be empty after removing + symbol."
409
- )
410
- full_description = f"{full_description} +{clean_project}"
411
-
412
- if context:
413
- # Remove any existing @ symbols to prevent duplication
414
- clean_context = context.strip().lstrip("@")
415
- if not clean_context:
416
- raise ValueError(
417
- "Context name cannot be empty after removing @ symbol."
418
- )
419
- full_description = f"{full_description} @{clean_context}"
420
-
421
- # Add the task first
422
- self.todo_shell.add(full_description)
423
-
424
- # Get the task number by finding the newly added task
425
- tasks = self.todo_shell.list_tasks()
426
- task_lines = [line.strip() for line in tasks.split("\n") if line.strip()]
427
- if not task_lines:
428
- raise RuntimeError("Failed to add task - no tasks found after addition")
429
-
430
- # Find the task that matches our description (it should be the last one added)
431
- # Look for the task that contains our description
432
- task_number = None
433
- for i, line in enumerate(task_lines, 1): # Start from 1 for todo.sh numbering
434
- if description in line:
435
- task_number = i
436
- break
555
+ # Build the task description
556
+ full_description = self._build_task_description(
557
+ description, project=project, context=context, parent_number=parent_number
558
+ )
437
559
 
438
- if task_number is None:
439
- # Fallback: use the last task number if we can't find a match
440
- task_number = len(task_lines)
441
- # Log a warning that we're using fallback logic
442
- import logging
560
+ # Check if we need to use a specific completion date
561
+ current_date = datetime.now().strftime("%Y-%m-%d")
562
+ use_specific_date = completion_date != current_date
443
563
 
444
- logging.warning(
445
- f"Could not find exact match for '{description}', using fallback task number {task_number}"
446
- )
564
+ if use_specific_date:
565
+ # When using a specific completion date, add directly to done.txt
566
+ # Format: "x YYYY-MM-DD [task description]"
567
+ completed_task_line = f"x {completion_date} {full_description}"
568
+ self.todo_shell.addto("done.txt", completed_task_line)
569
+ return f"Created and completed task: {full_description} (completed on {completion_date})"
570
+ else:
571
+ # When using current date, use the standard add + complete workflow
572
+ # Add the task first
573
+ self.todo_shell.add(full_description)
447
574
 
448
- # Mark it as complete
449
- self.todo_shell.complete(task_number)
575
+ # Find the task number and mark it complete
576
+ task_number = self._find_task_number_by_description(description)
577
+ self.todo_shell.complete(task_number)
450
578
 
451
- return f"Created and completed task: {full_description} (completed on {completion_date})"
579
+ return f"Created and completed task: {full_description} (completed on {completion_date})"
452
580
 
453
581
  def restore_completed_task(self, task_number: int) -> str:
454
582
  """
@@ -80,20 +80,20 @@ class Inference:
80
80
  def current_tasks(self) -> str:
81
81
  """
82
82
  Get current tasks from the todo manager.
83
-
83
+
84
84
  Returns:
85
85
  Formatted string of current tasks or error message
86
86
  """
87
87
  try:
88
88
  # Use the todo manager from the tool handler to get current tasks
89
89
  tasks = self.tool_handler.todo_manager.list_tasks(suppress_color=True)
90
-
90
+
91
91
  # If no tasks found, return a clear message
92
92
  if not tasks.strip() or tasks == "No tasks found.":
93
93
  return "No current tasks found."
94
-
94
+
95
95
  return tasks
96
-
96
+
97
97
  except Exception as e:
98
98
  self.logger.warning(f"Failed to get current tasks: {e!s}")
99
99
  return f"Error retrieving current tasks: {e!s}"
@@ -355,7 +355,9 @@ class Inference:
355
355
  self.tool_handler.tools
356
356
  )
357
357
 
358
- def _get_tool_progress_description(self, tool_name: str, tool_call: Dict[str, Any]) -> str:
358
+ def _get_tool_progress_description(
359
+ self, tool_name: str, tool_call: Dict[str, Any]
360
+ ) -> str:
359
361
  """
360
362
  Get user-friendly progress description for a tool with parameter interpolation.
361
363
 
@@ -377,25 +379,30 @@ class Inference:
377
379
 
378
380
  if tool_def and "progress_description" in tool_def:
379
381
  template = tool_def["progress_description"]
380
-
382
+
381
383
  # Extract arguments from tool call
382
384
  arguments = tool_call.get("function", {}).get("arguments", {})
383
385
  if isinstance(arguments, str):
384
386
  import json
387
+
385
388
  try:
386
389
  arguments = json.loads(arguments)
387
390
  except json.JSONDecodeError:
388
391
  arguments = {}
389
-
392
+
390
393
  # Use .format() like the system prompt does
391
394
  try:
392
395
  return template.format(**arguments)
393
396
  except KeyError as e:
394
397
  # If a required parameter is missing, fall back to template
395
- self.logger.warning(f"Missing parameter {e} for progress description of {tool_name}")
398
+ self.logger.warning(
399
+ f"Missing parameter {e} for progress description of {tool_name}"
400
+ )
396
401
  return template
397
402
  except Exception as e:
398
- self.logger.warning(f"Failed to interpolate progress description for {tool_name}: {e}")
403
+ self.logger.warning(
404
+ f"Failed to interpolate progress description for {tool_name}: {e}"
405
+ )
399
406
  return template
400
407
 
401
408
  # Fallback to generic description
@@ -12,48 +12,130 @@ You are an AI interface to the user's todo.sh task management system with direct
12
12
 
13
13
  ## Decision Flow
14
14
  1. **Data Discovery** → `list_tasks()` and `list_completed_tasks()` to fetch current and completed tasks
15
+ 2. **Date Discovery**: → `parse_date()` to fetch the calendar dates of any user-specified days
15
16
  2. **Planning Phase** → Analyze tasks and plan operations in logical order:
16
17
  - Multiple distinct goals may be indicated by the user
17
- - Identify dependencies and blocking relationships
18
+ - "I did X" Search existing tasks first, then complete or create_completed_task()
19
+ - Identify dependencies/parents and blocking relationships
20
+ - Identify the due date of recurring tasks using parse_date()
18
21
  - Determine priority sequence (overdue → due today → due soon → others)
19
22
  - Plan context-specific operations if needed
20
23
  - Map out required tool calls in execution order
21
24
  - Detail the execution plan in the response content
22
25
  3. **Execution Phase** → Execute planned operations in sequence:
23
26
  - Task operations: discover → analyze → execute
24
- - "I did X" → Search existing tasks first, then complete or create_completed_task()
25
- - Context filtering → Use exact matching: `list_tasks("@context")` only returns tasks with that specific context
26
27
  4. **Validation** → Verify all planned operations completed successfully
27
- 5. **Respond**: Generate a conversational, context-aware reply that summarizes the actions taken, explains reasoning (especially for due dates, priorities, or suggestions), and presents results in a natural, engaging tone. Always reference real data and operations performed. If no action was taken, clearly state why. Ensure the response is logically consistent, matches the user's style, and highlights any important next steps or recommendations.
28
+ 5. **Respond**: Generate a conversational, context-aware reply that summarizes the actions taken, explains
29
+ reasoning (especially for due dates, priorities, or suggestions), and presents results in a natural, engaging tone.
30
+ Always reference real data and operations performed. If no action was taken, clearly state why. Ensure the response
31
+ is logically consistent, matches the user's style, and highlights any important next steps or recommendations.
28
32
 
29
33
  ## Todo.txt Format
30
34
  ```
31
- (A) Task description +project @context due:YYYY-MM-DD duration:1h
35
+ (A) Task description +project @context due:YYYY-MM-DD duration:2h parent:12
36
+ (B) Put out trash and recycling +weekly +thursday @home duration:5m
37
+ (C) Standup meeting at 9:00AM +daily +weekdays @office duration:10m
38
+ (D) Another task (weekly on Friday) +project @context duration:10m
39
+ (E) Yet another task (daily) +project @context duration:2h
32
40
  x YYYY-MM-DD Completed task description
33
41
  ```
34
42
 
43
+ **Key Format Rules:**
44
+ - **Recurrence Tags**: Use `+weekly`, `+daily`, `+monthly` for recurrence frequency
45
+ - **Day Specificity**: Recurring tasks use `+monday`, `+tuesday`, `+wednesday`, `+thursday`, `+friday`, `+saturday`, `+sunday` for specific days
46
+ - **Weekday Patterns**: Recurring tasks use `+weekdays` for Monday-Friday, `+weekends` for Saturday-Sunday
47
+ - **Multiple Recurrence**: Combine tags like `+weekly +thursday` or `+daily +weekdays`
48
+ - **Time Specification**: Include time directly in description (e.g., "at 9:00AM", "at 2:00PM")
49
+ - **Duration**: Always specify `duration:Xm` or `duration:Xh` for time estimates
50
+
51
+ Example: add_task(description='Put out trash and recycling', project='weekly', context='home', due='YYYY-MM-DD', duration='5m')
52
+
35
53
  ## Key Intelligence Engines
36
54
 
55
+ ### Recurring Tasks
56
+ - **Daily Tasks**: If the task is indicated as 'daily', it is considered due today, regardless if a due date is specified.
57
+ - **Daily Weekday Tasks**: If the task is indicated as 'daily +weekdays', it is due on the next weekday (Monday-Friday).
58
+ - **Weekly Tasks**: If the task is indicated as 'weekly', it is due on the day of the week mentioned in the task description THIS WEEK.
59
+ - **Weekly Day-Specific Tasks**: If the task contains both '+weekly' and a specific day (e.g., '+thursday'), it is due on that specific day THIS WEEK.
60
+ - **Time-Specific Tasks**: If the task contains a specific time (e.g., 'at 9:00AM'), use parse_date() to determine the next occurrence of that time.
61
+ - **Work in Progress Tasks**: If the task has a 'wip' project tag (work in progress), it is considered due today, regardless if a due date is specified.
62
+ - **Due Date Inference**: Issue parse_date() to determine the due date of recurring tasks, especially for complex patterns like "weekly on Thursday" or "daily on weekdays at 9:00AM".
63
+
64
+ ### Complex Recurrence Pattern Examples
65
+ - `+weekly +thursday` → Due on the next Thursday
66
+ - `+daily +weekdays` → Due on the next weekday (Monday-Friday)
67
+ - `at 9:00AM +daily +weekdays` → Due on the next weekday at 9:00AM
68
+ - `+weekly +friday duration:2h` → Due on the next Friday with 2-hour duration
69
+ - `+monthly +first` → Due on the first day of next month
70
+
37
71
  ### Task Creation Protocol
38
72
  1. Get current + completed tasks to check duplicates
39
73
  2. Infer project/context/duration from description and patterns
40
- 3. Apply completion date intelligence (work tasks by week end, bills 3-5 days early, etc.)
41
- 4. Create with full metadata
74
+ 3. **Parse complex recurrence patterns**: Handle multiple recurrence indicators (e.g., '+weekly +thursday', '+daily +weekdays')
75
+ 4. **Handle time specifications**: Extract and process time-based scheduling (e.g., 'at 9:00AM')
76
+ 5. **Parse parent task relationships**: Identify when user indicates task dependencies or hierarchies
77
+ 6. Apply completion date intelligence (work tasks by week end, bills 3-5 days early, etc.)
78
+ 7. Create with full metadata including proper due date calculation and parent relationships
79
+
80
+ ### Parent Task Relationships
81
+ **Natural Language Indicators for Parent Tasks**:
82
+ - **Explicit Dependencies**: "after [task]", "once [task] is done", "following [task]", "depends on [task]"
83
+ - **Hierarchical Language**: "subtask of [task]", "part of [task]", "under [task]", "child of [task]"
84
+ - **Sequential Language**: "next step after [task]", "continue with [task]", "build on [task]"
85
+ - **Conditional Language**: "if [task] then [new task]", "when [task] is complete, do [new task]"
86
+ - **Update Commands**: "make [task] depend on [task]", "update [task] to depend on [task]", "put [task] under [task]", "move [task] under [task]"
87
+
88
+ **Parent Task Resolution Protocol**:
89
+ 1. **Identify Parent Indicators**: Look for natural language cues indicating task relationships (creation or updates)
90
+ 2. **Find Tasks**: Use `list_tasks()` to locate both the child task and parent task
91
+ 3. **Validate Relationship**: Ensure the parent-child relationship makes logical sense
92
+ 4. **Establish Relationship**:
93
+ - **For new tasks**: Create child task with `parent:XX` reference
94
+ - **For existing tasks**: Use `set_parent()` to update target task with parent relationship
95
+ 5. **Maintain Hierarchy**: Preserve task dependencies in the todo.txt format with `parent:XX`
96
+
97
+ **Parent Task Examples**:
98
+ - **Creating new tasks**: "Add a subtask to review the quarterly report" → Find parent task "Review quarterly report" and create child task with `parent:XX`
99
+ - **Creating new tasks**: "After I finish the presentation, I need to schedule a follow-up meeting" → Find "presentation" task and create "schedule follow-up meeting" with `parent:XX`
100
+ - **Creating new tasks**: "Once the code review is done, I'll deploy to staging" → Find "code review" task and create "deploy to staging" with `parent:XX`
101
+ - **Updating existing tasks**: "Update the listing tasks to depend on the pictures task" → Find "listing tasks" and "pictures task", then use `set_parent()` to update listing tasks
102
+ - **Updating existing tasks**: "Make the deployment depend on the testing task" → Find "deployment" and "testing" tasks, then use `set_parent()` to update deployment
103
+ - **Updating existing tasks**: "Put the documentation under the code review task" → Find "documentation" and "code review" tasks, then use `set_parent()` to update documentation
104
+
105
+ **Parent Task Format**: `(A) Child task description +project @context due:YYYY-MM-DD duration:2h parent:12`
42
106
 
43
107
  ### Task Completion Protocol
44
- 1. Search semantically in active tasks
45
- 2. Single match complete immediately
46
- 3. Multiple/fuzzy show options
47
- 4. No match suggest alternatives
108
+ 1. **Discovery**: Use `list_tasks()` to search semantically in active tasks
109
+ 2. **For Recurring Tasks** (containing +daily, +weekly, +monthly, +weekdays, etc.):
110
+ - **USE** `create_completed_task()` with the original task number as parent_number
111
+ - **PRESERVE** the original recurring task for future occurrences. **IMPORTANT** DO NOT MARK COMPLETE! DO NOT MODIFY!
112
+ - **TOOL CALL**: `create_completed_task(description='Task description', parent_number='XX', completion_date='YYYY-MM-DD', context='context', project='project')`
113
+ 3. **For Non-Recurring Tasks**:
114
+ - Single match → use `complete_task(task_number='XX')`
115
+ - Multiple/fuzzy → show options
116
+ - No match → suggest alternatives
117
+
118
+ ### Recurring Task Completion Examples
119
+ - **User says**: "I put out the trash" → Find `(B) Put out trash and recycling +weekly +thursday @home duration:5m`
120
+ - **Tool Call**: `create_completed_task(description='Put out trash and recycling', parent_number='B', completion_date='YYYY-MM-DD', context='home', project='weekly')`
121
+ - **Result**: Original task remains active for next Thursday
122
+ - **User says**: "Done with standup" → Find `Standup meeting at 9:00AM +daily +weekdays @office duration:10m`
123
+ - **Tool Call**: `create_completed_task(description='Standup meeting at 9:00AM', parent_number='XX', completion_date='YYYY-MM-DD', context='office', project='daily')`
124
+ - **Result**: Original task remains active for next weekday
48
125
 
49
126
  ### Task Suggestions
50
127
  **Trigger**: User asks, seems stuck, or after completions
51
- **Method**:
128
+ **Method**:
129
+ - Identify tasks within the user's implied temporal scope
130
+ - Consider that today is {current_datetime}, and match any recurring tasks
131
+ - Pay careful attention to due dates and their relation to the current date
132
+ - @office and work tasks are always the highest priority
133
+ - +wip tasks are considered high priority and due today
52
134
  - Balance urgency and priority. Use your best judgment.
53
- - Logical dependencies trend first (tasks that unblock others get priority)
135
+ - Logical and explicit dependencies should be suggested first (tasks that unblock others get priority)
54
136
  - Then urgency (overdue → due today → due soon)
55
- - Pay careful attention to due dates and their relation to the current date {{current_datetime}}
56
- - Mention days of week when dates are mentioned for clarity. Minimize repetition.
137
+ - Be exhaustive in your search and mention everything relevant
138
+ - Always state days of the week
57
139
 
58
140
  ### Context Patterns
59
141
  - `@phone`: calls, appointments
@@ -64,6 +146,23 @@ x YYYY-MM-DD Completed task description
64
146
 
65
147
  ### Project Patterns
66
148
  - Health → `+health`, Work → `+work`, Bills → `+bills`, etc.
149
+ - Recurring tasks:
150
+ - `+daily`: ALWAYS consider due today
151
+ - `+weekly`: ALWAYS consider due on the specific day THIS WEEK
152
+
153
+ ## Notes Protocol
154
+ **When the user wants to create a note**:
155
+ **Triggers**: 'note:', or 'Create a note', or 'progress:', etc
156
+ **Action**: create_completed_task()
157
+ **Method**:
158
+ 1. ALWAYS Identify semantically similar tasks that are the likely parent task
159
+ 2. IF there is LIKELY match, that is the parent and parent_number
160
+ 4. Create a completed task with create_completed_task():
161
+ - **IMPORTANT**: ALWAYS assign the parent_number if a match was found
162
+ - with inferred parent_number, completion_date, context, and project
163
+ - Task description should never include date of completion
164
+ - NO priority
165
+ **Response**: ALWAYS refer to your note actions as creating a note, NOT creating a task
67
166
 
68
167
  ## Critical Rules
69
168
  - **Overdue definition**: A task is overdue IF AND _ONLY IF_ due < {current_datetime}. None is an acceptable answer!
@@ -71,11 +170,14 @@ x YYYY-MM-DD Completed task description
71
170
  - **Task ordering**: Always dependencies first, then urgency
72
171
  - **Data integrity**: Only use real tool data, never fabricate
73
172
  - **Completion date reasoning**: Always explain date suggestions briefly
173
+ - **Parent Tasks**: Task dependencies are explicitly indicated by `parent:XX` tags
174
+ - **Parent Task Detection**: Always identify and establish parent-child relationships when users indicate task dependencies through natural language
74
175
 
75
176
  ## Tool Selection Strategy
76
177
  - Project tags: use `set_project()`
77
178
  - Context tags: use `set_context()`
78
179
  - Due dates: use `set_due_date()`
180
+ - Parent relationships: use `set_parent()`
79
181
  - Discovery: `list_tasks()` once for full context
80
182
  - Completion: `list_tasks()` + `complete_task()`
81
183
  - Addition: `list_tasks()` + `add_task()` with full metadata
@@ -108,6 +108,10 @@ class TodoShell:
108
108
  """Add new task."""
109
109
  return self.execute(["todo.sh", "add", description])
110
110
 
111
+ def addto(self, destination: str, text: str) -> str:
112
+ """Add text to a specific file in the todo.txt directory."""
113
+ return self.execute(["todo.sh", "addto", destination, text])
114
+
111
115
  def list_tasks(
112
116
  self, filter_str: Optional[str] = None, suppress_color: bool = True
113
117
  ) -> str:
@@ -276,6 +280,61 @@ class TodoShell:
276
280
  # Replace the task with the new description
277
281
  return self.replace(task_number, new_description)
278
282
 
283
+ def set_parent(self, task_number: int, parent_number: Optional[int]) -> str:
284
+ """
285
+ Set or update parent task number for a task by intelligently rewriting it.
286
+
287
+ Args:
288
+ task_number: The task number to modify
289
+ parent_number: Parent task number, or None to remove parent
290
+
291
+ Returns:
292
+ The updated task description
293
+ """
294
+ # First, get the current task to parse its components
295
+ tasks_output = self.list_tasks()
296
+ task_lines = tasks_output.strip().split("\n")
297
+
298
+ # Find the task by its actual number (not array index)
299
+ current_task = None
300
+ for line in task_lines:
301
+ if line.strip():
302
+ # Extract task number from the beginning of the line (handling ANSI codes)
303
+ extracted_number = self._extract_task_number(line)
304
+ if extracted_number == task_number:
305
+ current_task = line
306
+ break
307
+
308
+ if not current_task:
309
+ raise TodoShellError(f"Task number {task_number} not found")
310
+
311
+ # Parse the current task components
312
+ components = self._parse_task_components(current_task)
313
+
314
+ # Update the parent (None removes it)
315
+ if parent_number is not None:
316
+ if not isinstance(parent_number, int) or parent_number <= 0:
317
+ raise TodoShellError(
318
+ f"Invalid parent_number '{parent_number}'. Must be a positive integer."
319
+ )
320
+ parent_tag = f"parent:{parent_number}"
321
+ # Remove any existing parent tag and add the new one
322
+ components["other_tags"] = [
323
+ tag for tag in components["other_tags"] if not tag.startswith("parent:")
324
+ ]
325
+ components["other_tags"].append(parent_tag)
326
+ else:
327
+ # Remove parent tag
328
+ components["other_tags"] = [
329
+ tag for tag in components["other_tags"] if not tag.startswith("parent:")
330
+ ]
331
+
332
+ # Reconstruct the task
333
+ new_description = self._reconstruct_task(components)
334
+
335
+ # Replace the task with the new description
336
+ return self.replace(task_number, new_description)
337
+
279
338
  def _extract_task_number(self, line: str) -> Optional[int]:
280
339
  """
281
340
  Extract task number from a line that may contain ANSI color codes.
@@ -16,7 +16,7 @@ Task Management Tools:
16
16
  - append_to_task(task_number, text) - Add text to end of existing task
17
17
  - prepend_to_task(task_number, text) - Add text to beginning of existing task
18
18
  - delete_task(task_number, term?) - Delete entire task or remove specific term
19
- - created_completed_task(description, completion_date?, project?, context?) - Create task and immediately mark as completed
19
+ - create_completed_task(description, completion_date?, project?, context?) - Create task and immediately mark as completed
20
20
 
21
21
  Priority Management Tools:
22
22
  - set_priority(task_number, priority) - Set or change task priority (A-Z)
@@ -26,6 +26,7 @@ Task Modification Tools:
26
26
  - set_due_date(task_number, due_date) - Set or update due date for a task by intelligently rewriting it (use empty string to remove due date)
27
27
  - set_context(task_number, context) - Set or update context for a task by intelligently rewriting it (use empty string to remove context)
28
28
  - set_project(task_number, projects) - Set or update projects for a task by intelligently rewriting it (handles array of projects with add/remove operations)
29
+ - set_parent(task_number, parent_number) - Set or update parent task number for a task by intelligently rewriting it (use None to remove parent)
29
30
 
30
31
  Utility Tools:
31
32
  - move_task(task_number, destination, source?) - Move task between files
@@ -176,6 +177,10 @@ class ToolCallHandler:
176
177
  "type": "string",
177
178
  "description": "Optional duration estimate in format: minutes (e.g., '30m'), hours (e.g., '2h'), or days (e.g., '1d'). Use for time planning and task prioritization.",
178
179
  },
180
+ "parent_number": {
181
+ "type": "integer",
182
+ "description": "Optional parent task number (required for subtasks)",
183
+ },
179
184
  },
180
185
  "required": ["description"],
181
186
  },
@@ -254,12 +259,12 @@ class ToolCallHandler:
254
259
  "type": "integer",
255
260
  "description": "The line number of the task to modify (required)",
256
261
  },
257
- "text_to_append": {
262
+ "text": {
258
263
  "type": "string",
259
264
  "description": "Text to add to the end of the task (required)",
260
265
  },
261
266
  },
262
- "required": ["task_number", "text_to_append"],
267
+ "required": ["task_number", "text"],
263
268
  },
264
269
  },
265
270
  "progress_description": "📝 Adding notes to task #{task_number}...",
@@ -474,6 +479,39 @@ class ToolCallHandler:
474
479
  },
475
480
  "progress_description": "🏷️ Setting project tags for task #{task_number}...",
476
481
  },
482
+ {
483
+ "type": "function",
484
+ "function": {
485
+ "name": "set_parent",
486
+ "description": (
487
+ "Set or update the parent task number for an EXISTING task by intelligently rewriting it. "
488
+ "USE CASE: Call this when user wants to add, change, or remove a parent task relationship on an existing task. "
489
+ "NOT FOR: Creating new tasks, completing tasks, or any other task operations. "
490
+ "This preserves all existing task components (priority, projects, contexts, due date, etc.) "
491
+ "while updating or adding the parent relationship. Use None to remove the parent. "
492
+ "PREFERRED METHOD: Use this instead of append_to_task() when adding parent tags (parent:XX). "
493
+ "This tool properly manages parent relationships and prevents formatting issues. "
494
+ "IMPORTANT: Use list_tasks() first "
495
+ "to find the correct task number if user doesn't specify it. "
496
+ "Parent task numbers should be provided as integers (e.g., 12 not 'parent:12')."
497
+ ),
498
+ "parameters": {
499
+ "type": "object",
500
+ "properties": {
501
+ "task_number": {
502
+ "type": "integer",
503
+ "description": "The line number of the task to modify (required)",
504
+ },
505
+ "parent_number": {
506
+ "type": "integer",
507
+ "description": "Parent task number to set, or null to remove parent (required)",
508
+ },
509
+ },
510
+ "required": ["task_number", "parent_number"],
511
+ },
512
+ },
513
+ "progress_description": "🔗 Setting parent task {parent_number} for task #{task_number}...",
514
+ },
477
515
  {
478
516
  "type": "function",
479
517
  "function": {
@@ -585,7 +623,7 @@ class ToolCallHandler:
585
623
  {
586
624
  "type": "function",
587
625
  "function": {
588
- "name": "created_completed_task",
626
+ "name": "create_completed_task",
589
627
  "description": (
590
628
  "Create a task and immediately mark it as completed. "
591
629
  "USE CASE: Call this when user says they completed something on a specific date (e.g., 'I did the laundry today', 'I finished the report yesterday', 'I cleaned the garage last week') "
@@ -615,6 +653,10 @@ class ToolCallHandler:
615
653
  "type": "string",
616
654
  "description": "Optional context name (without the @ symbol) for new task creation",
617
655
  },
656
+ "parent_number": {
657
+ "type": "integer",
658
+ "description": "Optional parent task number (required for subtasks)",
659
+ },
618
660
  },
619
661
  "required": ["description"],
620
662
  },
@@ -897,11 +939,12 @@ class ToolCallHandler:
897
939
  "set_due_date": self.todo_manager.set_due_date,
898
940
  "set_context": self.todo_manager.set_context,
899
941
  "set_project": self.todo_manager.set_project,
942
+ "set_parent": self.todo_manager.set_parent,
900
943
  "move_task": self.todo_manager.move_task,
901
944
  "archive_tasks": self.todo_manager.archive_tasks,
902
945
  "parse_date": self._parse_date,
903
946
  "get_calendar": self._get_calendar,
904
- "created_completed_task": self.todo_manager.created_completed_task,
947
+ "create_completed_task": self.todo_manager.create_completed_task,
905
948
  "restore_completed_task": self.todo_manager.restore_completed_task,
906
949
  }
907
950
 
todo_agent/main.py CHANGED
@@ -11,9 +11,12 @@ from .interface.cli import CLI
11
11
 
12
12
  def main() -> None:
13
13
  """Main application entry point."""
14
+ from ._version import __version__
15
+
14
16
  parser = argparse.ArgumentParser(
15
- description="Todo.sh LLM Agent - Natural language task management",
17
+ description=f"Todo.sh LLM Agent - Natural language task management (v{__version__})",
16
18
  formatter_class=argparse.RawDescriptionHelpFormatter,
19
+ add_help=False,
17
20
  epilog="""
18
21
  Examples:
19
22
  todo-agent # Interactive mode
@@ -23,6 +26,19 @@ Examples:
23
26
  """,
24
27
  )
25
28
 
29
+ parser.add_argument(
30
+ "--version", "-v",
31
+ action="version",
32
+ version=f"%(prog)s {__version__}",
33
+ help="Show version information and exit",
34
+ )
35
+
36
+ parser.add_argument(
37
+ "--help", "-h",
38
+ action="help",
39
+ help="Show this help message and exit",
40
+ )
41
+
26
42
  parser.add_argument(
27
43
  "command",
28
44
  nargs="?",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -1,30 +1,30 @@
1
1
  todo_agent/__init__.py,sha256=RUowhd14r3tqB_7rl83unGV8oBjra3UOIl7jix-33fk,254
2
- todo_agent/_version.py,sha256=lemL_4Kl75FgrO6lVuFrrtw6-Dcf9wtXBalKkXuzkO4,704
3
- todo_agent/main.py,sha256=-ryhMm4c4sz4e4anXI8B-CYnpEh5HIkmnYcnGxcWHDk,1628
2
+ todo_agent/_version.py,sha256=UAb2Toi6SAdScDfq1uKRRv5QpMUuRtJqqwNxTMGe5Q4,704
3
+ todo_agent/main.py,sha256=DfcZuTsHkmpm6kw0coB0-B677K3hyRwutWIuO4noQ3w,2026
4
4
  todo_agent/core/__init__.py,sha256=QAZ4it63pXv5-DxtNcuSAmg7ZnCY5ackI5yycvKHr9I,365
5
5
  todo_agent/core/conversation_manager.py,sha256=9aAWogswZe9Cs7wKT47RG-cLh1LQ5D9RbhUHJVUyTS8,13549
6
6
  todo_agent/core/exceptions.py,sha256=ilVL5hyPHGYQXsLm0pYivMbhbamupG77r8TbiQr2tAU,2034
7
- todo_agent/core/todo_manager.py,sha256=nLQbri-hhbqtV-zJNpdnBHPiUYd7tAcqpFFLX-xxeW4,19263
7
+ todo_agent/core/todo_manager.py,sha256=qnMT6TOhxvh-e_2IkC8rA87T56iJQ9mHT1AyffrAFr8,25134
8
8
  todo_agent/infrastructure/__init__.py,sha256=SGbHXgzq6U1DMgOfWPMsWEK99zjPSF-6gzy7xqc5fsI,284
9
9
  todo_agent/infrastructure/calendar_utils.py,sha256=8tOZHN5CNrRHpTQHYzskmsZNJKWuUFjrjyvHQ76AhzU,1837
10
10
  todo_agent/infrastructure/config.py,sha256=zyp6qOlg1nN_awphivlgGNBE6fL0Hf66YgvWxR8ldyQ,2117
11
- todo_agent/infrastructure/inference.py,sha256=tAAv6kou9ucgLaX2f3ZHahjAn2ofB0WQFVUgFVRQ-KM,16193
11
+ todo_agent/infrastructure/inference.py,sha256=nv0ZbYHiJtWOwsN1VoXY1f0Zlf9SOIOVQZDhnAeC1YU,16212
12
12
  todo_agent/infrastructure/llm_client.py,sha256=Knb6yvQt0q8mobnwVH08mGCd8xBHK_g1S66DOdp7cO8,9807
13
13
  todo_agent/infrastructure/llm_client_factory.py,sha256=-tktnVOIF7B45WR7AuLoi7MKnEyuM8lgg1jjc4T1FhM,1929
14
14
  todo_agent/infrastructure/logger.py,sha256=2ykG_0lyzmEGxDF6ZRl1qiTUGDuFeQgzv4Na6vRmXcM,4110
15
15
  todo_agent/infrastructure/ollama_client.py,sha256=V_zAeBjIEzB8PZXyzFeiLMLA5qf3y4WV2_6Vqxn1ujc,5629
16
16
  todo_agent/infrastructure/openrouter_client.py,sha256=0pxIKvHItwVijFz8l4loOeCa4HUpMvTYROcwYJh3iyI,6748
17
- todo_agent/infrastructure/todo_shell.py,sha256=EySpPNZUZ0Dl_-snieKk6TcMx8NHkZvxXz6E-m6RKhI,18201
17
+ todo_agent/infrastructure/todo_shell.py,sha256=2iPPhsLsTcjVx8Wv2mJJBu11kLJOK9Tk5KBAyVQ3Yjs,20596
18
18
  todo_agent/infrastructure/token_counter.py,sha256=PCKheOVJbp1s89yhh_i6iKgURMt9mVoYkwjQJCc2xCE,4958
19
- todo_agent/infrastructure/prompts/system_prompt.txt,sha256=APyGy3sjc2LP4YgcxMuC7_9c4BVYzSEb9R8pHobB5H8,4453
19
+ todo_agent/infrastructure/prompts/system_prompt.txt,sha256=ZHGYgd7lCuWaSX5io3IYWFhdjcGbdqKXf2uhEWdbzQ8,12380
20
20
  todo_agent/interface/__init__.py,sha256=vDD3rQu4qDkpvVwGVtkDzE1M4IiSHYzTif4GbYSFWaI,457
21
21
  todo_agent/interface/cli.py,sha256=Ikh5AYoOZnNUVXzHjfv2bsE33yK3VhCdzza83HOzFp8,16460
22
22
  todo_agent/interface/formatters.py,sha256=a9PW-5DbY8K5QYZjBXFZSdzlCmy263kwBI9nXgP8LXI,20081
23
23
  todo_agent/interface/progress.py,sha256=EpPa20Hrnjv_TBIp0tzViVciThqsOvAHuYO4v2rc8eI,1751
24
- todo_agent/interface/tools.py,sha256=-pMPSNKyb5M5CGie6-ZxDYdl82YAt0v9ie3TCILOfQg,51714
25
- todo_agent-0.3.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
26
- todo_agent-0.3.3.dist-info/METADATA,sha256=VceLOqM8POJIJxGuRsAalzTMgnilLCuL7vmdJgbNOlU,10056
27
- todo_agent-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- todo_agent-0.3.3.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
29
- todo_agent-0.3.3.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
30
- todo_agent-0.3.3.dist-info/RECORD,,
24
+ todo_agent/interface/tools.py,sha256=Q6h2zpe1yRUwJOZZFArjXgBN6WPDI56CDQQJcdF1I5M,54448
25
+ todo_agent-0.3.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
26
+ todo_agent-0.3.5.dist-info/METADATA,sha256=eP-pVo55MQvK1G9b9vaoOhir6fQnjOr4CD-D8n29Ha0,10056
27
+ todo_agent-0.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ todo_agent-0.3.5.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
29
+ todo_agent-0.3.5.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
30
+ todo_agent-0.3.5.dist-info/RECORD,,