todo-agent 0.2.8__py3-none-any.whl → 0.2.9__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.8'
32
- __version_tuple__ = version_tuple = (0, 2, 8)
31
+ __version__ = version = '0.2.9'
32
+ __version_tuple__ = version_tuple = (0, 2, 9)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -78,7 +78,9 @@ class TodoManager:
78
78
  if interval < 1:
79
79
  raise ValueError("Interval must be at least 1.")
80
80
  except ValueError:
81
- raise ValueError(f"Invalid interval '{parts[2]}'. Must be a positive integer.")
81
+ raise ValueError(
82
+ f"Invalid interval '{parts[2]}'. Must be a positive integer."
83
+ )
82
84
 
83
85
  # Build the full task description with priority, project, and context
84
86
  full_description = description
@@ -167,6 +169,114 @@ class TodoManager:
167
169
  result = self.todo_shell.remove_priority(task_number)
168
170
  return f"Removed priority from task {task_number}: {result}"
169
171
 
172
+ def set_due_date(self, task_number: int, due_date: str) -> str:
173
+ """
174
+ Set or update due date for a task by intelligently rewriting it.
175
+
176
+ Args:
177
+ task_number: The task number to modify
178
+ due_date: Due date in YYYY-MM-DD format, or empty string to remove due date
179
+
180
+ Returns:
181
+ Confirmation message with the updated task
182
+ """
183
+ # Validate due date format only if not empty
184
+ if due_date.strip():
185
+ try:
186
+ datetime.strptime(due_date, "%Y-%m-%d")
187
+ except ValueError:
188
+ raise ValueError(
189
+ f"Invalid due date format '{due_date}'. Must be YYYY-MM-DD."
190
+ )
191
+
192
+ result = self.todo_shell.set_due_date(task_number, due_date)
193
+ if due_date.strip():
194
+ return f"Set due date {due_date} for task {task_number}: {result}"
195
+ else:
196
+ return f"Removed due date from task {task_number}: {result}"
197
+
198
+ def set_context(self, task_number: int, context: str) -> str:
199
+ """
200
+ Set or update context for a task by intelligently rewriting it.
201
+
202
+ Args:
203
+ task_number: The task number to modify
204
+ context: Context name (without @ symbol), or empty string to remove context
205
+
206
+ Returns:
207
+ Confirmation message with the updated task
208
+ """
209
+ # Validate context name if not empty
210
+ if context.strip():
211
+ # Remove any existing @ symbols to prevent duplication
212
+ clean_context = context.strip().lstrip("@")
213
+ if not clean_context:
214
+ raise ValueError(
215
+ "Context name cannot be empty after removing @ symbol."
216
+ )
217
+
218
+ result = self.todo_shell.set_context(task_number, context)
219
+ if context.strip():
220
+ clean_context = context.strip().lstrip("@")
221
+ return f"Set context @{clean_context} for task {task_number}: {result}"
222
+ else:
223
+ return f"Removed context from task {task_number}: {result}"
224
+
225
+ def set_project(self, task_number: int, projects: list) -> str:
226
+ """
227
+ Set or update projects for a task by intelligently rewriting it.
228
+
229
+ Args:
230
+ task_number: The task number to modify
231
+ projects: List of project operations. Each item can be:
232
+ - "project" (add project)
233
+ - "-project" (remove project)
234
+ - Empty string removes all projects
235
+
236
+ Returns:
237
+ Confirmation message with the updated task
238
+ """
239
+ # Validate project names if not empty
240
+ if projects:
241
+ for project in projects:
242
+ if project.strip() and not project.startswith("-"):
243
+ # Remove any existing + symbols to prevent duplication
244
+ clean_project = project.strip().lstrip("+")
245
+ if not clean_project:
246
+ raise ValueError(
247
+ "Project name cannot be empty after removing + symbol."
248
+ )
249
+ elif project.startswith("-"):
250
+ clean_project = project[1:].strip().lstrip("+")
251
+ if not clean_project:
252
+ raise ValueError(
253
+ "Project name cannot be empty after removing - and + symbols."
254
+ )
255
+
256
+ result = self.todo_shell.set_project(task_number, projects)
257
+
258
+ if not projects:
259
+ return f"No project changes made to task {task_number}: {result}"
260
+ else:
261
+ # Build operation description
262
+ operations = []
263
+ for project in projects:
264
+ if not project.strip():
265
+ # Empty string is a NOOP - skip
266
+ continue
267
+ elif project.startswith("-"):
268
+ clean_project = project[1:].strip().lstrip("+")
269
+ operations.append(f"removed +{clean_project}")
270
+ else:
271
+ clean_project = project.strip().lstrip("+")
272
+ operations.append(f"added +{clean_project}")
273
+
274
+ if not operations:
275
+ return f"No project changes made to task {task_number}: {result}"
276
+ else:
277
+ operation_desc = ", ".join(operations)
278
+ return f"Updated projects for task {task_number} ({operation_desc}): {result}"
279
+
170
280
  def list_projects(self, **kwargs: Any) -> str:
171
281
  """List all available projects in todo.txt."""
172
282
  result = self.todo_shell.list_projects()
@@ -72,26 +72,32 @@ class OpenRouterClient(LLMClient):
72
72
  f"Token usage - Prompt: {prompt_tokens}, Completion: {completion_tokens}, Total: {total_tokens}"
73
73
  )
74
74
 
75
- # Log tool call details if present
76
- if response.get("choices"):
77
- choice = response["choices"][0]
78
- if "message" in choice and "tool_calls" in choice["message"]:
79
- tool_calls = choice["message"]["tool_calls"]
80
- self.logger.info(f"Response contains {len(tool_calls)} tool calls")
81
-
82
- # Log thinking content (response body) if present
83
- content = choice["message"].get("content", "")
84
- if content and content.strip():
85
- self.logger.info(f"LLM thinking before tool calls: {content}")
86
-
87
- for i, tool_call in enumerate(tool_calls):
88
- tool_name = tool_call.get("function", {}).get("name", "unknown")
89
- self.logger.info(f" Tool call {i + 1}: {tool_name}")
90
- elif "message" in choice and "content" in choice["message"]:
91
- content = choice["message"]["content"]
92
- self.logger.debug(
93
- f"Response contains content: {content[:100]}{'...' if len(content) > 100 else ''}"
94
- )
75
+ # Extract and log choice details
76
+ choices = response.get("choices", [])
77
+ if not choices:
78
+ return
79
+
80
+ choice = choices[0]
81
+ message = choice.get("message", {})
82
+
83
+ # Always log reasoning and content if present
84
+ reasoning = message.get("reasoning", "")
85
+ if reasoning:
86
+ self.logger.info(f"LLM reasoning: {reasoning}")
87
+
88
+ content = message.get("content", "")
89
+ if content:
90
+ self.logger.info(f"LLM content: {content}")
91
+
92
+ # Handle tool calls
93
+ tool_calls = message.get("tool_calls", [])
94
+ if tool_calls:
95
+ self.logger.info(f"Response contains {len(tool_calls)} tool calls")
96
+
97
+ # Log each tool call
98
+ for i, tool_call in enumerate(tool_calls, 1):
99
+ tool_name = tool_call.get("function", {}).get("name", "unknown")
100
+ self.logger.info(f" Tool call {i}: {tool_name}")
95
101
 
96
102
  self.logger.debug(f"Raw response: {json.dumps(response, indent=2)}")
97
103
 
@@ -10,7 +10,7 @@ CORE PRINCIPLES
10
10
  - Present in natural language with strategic insights, never raw todo.txt format
11
11
  - Include concise reasoning for all decisions and suggestions
12
12
  - Make confident and specific recommendations
13
- - Lead with the answer then follow with minimal supporting information afterward
13
+ - Lead with the answer or description of actions then follow with minimal supporting information afterward
14
14
  - Prose is always preferred to formatted lists or formatting in general
15
15
 
16
16
  OUTPUT FORMATTING
@@ -66,17 +66,20 @@ Gate 3: Execution Protocols
66
66
  Task Creation Protocol:
67
67
  DISCOVER: Current tasks + completed tasks
68
68
  ANALYZE: Semantic duplicates? (similar intent/keywords)
69
- INFER: High confidence context/timing from:
69
+ INFER: Context/timing from:
70
70
  - Explicit temporal: "tomorrow", "by Friday"
71
71
  - Task nature: bills->payment cycles, work->business hours
72
72
  - Existing patterns: match similar task contexts
73
73
  - Calendar context: work days, weekends, holidays
74
- - ACTIVATE Completion Date Intelligence Engine for date inference
74
+ - Project inference: ALWAYS infer and add unambiguous +project tags
75
+ - Context inference: ALWAYS infer and add unambiguous @context tag
76
+ - Duration inference: ALWAYS infer and add appropriate duration: tag
77
+ - ACTIVATE Completion Date Intelligence Engine for due date inference
75
78
  DECIDE:
76
79
  - Clear intent + high confidence -> Create immediately
77
80
  - Semantic duplicate found -> "Similar task exists: [task]. Add anyway or modify existing?"
78
81
  - Ambiguous context -> Ask specific clarification
79
- - Low confidence inference -> Verify before acting
82
+ - Low confidence inference -> Ask before acting
80
83
 
81
84
  Completion Date Intelligence Engine
82
85
  When inferring convenient and realistic completion dates:
@@ -124,7 +127,7 @@ MATCH:
124
127
  - Single clear match -> Complete immediately + suggest next steps
125
128
  - Multiple candidates -> Show numbered options with context
126
129
  - Fuzzy match -> "Did you mean: [closest match]?"
127
- - No match -> "No matching active tasks found. Recent completions?"
130
+ - No match -> "No matching active tasks found. Relax filters? Recent completions?"
128
131
 
129
132
  STRATEGIC INTELLIGENCE MODULES
130
133
 
@@ -269,6 +272,56 @@ When encountering "rec:" syntax in task descriptions or the user indicates repea
269
272
  - Preserve all task metadata (priority, project, context) in new instances
270
273
  - Handle edge cases: leap years, month boundaries, weekday patterns
271
274
 
275
+ DURATION INTELLIGENCE ENGINE
276
+ When encountering duration: syntax in task descriptions:
277
+ - Parse duration patterns: duration:15m, duration:2h, duration:1d
278
+ - Apply reasonable defaults for tasks without duration tags
279
+ - Validate duration ranges (1m to 8h for most tasks)
280
+ - Use duration for task organization and prioritization
281
+
282
+ AUTOMATIC PROJECT AND CONTEXT INFERENCE:
283
+ When creating tasks, ALWAYS infer and add unambiguous project and context tags based on:
284
+ - Task description keywords and semantic patterns
285
+ - Existing project and context usage patterns in current and completed tasks
286
+ - Task nature and typical locations/contexts
287
+ - User's historical task organization
288
+
289
+ Project Inference Patterns:
290
+ - Health/medical: +health (dentist, doctor, pharmacy, exercise, vitamins)
291
+ - Work/business: +work (meetings, reports, presentations, emails, calls)
292
+ - Financial: +bills (payments, invoices, banking, taxes, expenses)
293
+ - Home maintenance: +chores (cleaning, repairs, maintenance, organization)
294
+ - Personal development: +learning (reading, courses, skills, education)
295
+ - Social: +social (calls, meetings, events, family, friends)
296
+ - Errands: +errands (shopping, appointments, deliveries, services)
297
+ - Work in Progress: +wip (work tasks, projects, tasks pending dependencies)
298
+
299
+ Context Inference Patterns:
300
+ - @phone: calls, appointments, customer service, scheduling
301
+ - @computer: work, research, writing, online tasks, emails
302
+ - @office: work meetings, in-person work, office tasks
303
+ - @home: chores, maintenance, personal tasks, relaxation
304
+ - @errands: shopping, appointments, deliveries, services
305
+ - @grocery: food shopping, household supplies
306
+ - @post-office: mail, packages, shipping, government services
307
+
308
+ AUTOMATIC DURATION INFERENCE:
309
+ When creating tasks, ALWAYS infer and add appropriate duration tags based on:
310
+ - Task description keywords and context
311
+ - Project and context patterns
312
+ - Task complexity and scope
313
+ - User's existing task patterns
314
+
315
+ Default Duration Patterns:
316
+ - Quick tasks: duration:15m (calls, emails, simple errands, quick checks)
317
+ - Medium tasks: duration:1h (meetings, focused work, moderate chores)
318
+ - Long tasks: duration:2h (deep work, complex projects, major errands)
319
+ - Context-specific defaults:
320
+ - @phone tasks: duration:15m (calls, appointments)
321
+ - @computer tasks: duration:1h (work, research, writing)
322
+ - @errands tasks: duration:45m (shopping, appointments)
323
+ - @home tasks: duration:30m (chores, maintenance)
324
+
272
325
  Recurring Task Completion Protocol:
273
326
  1. Detect when user completes a recurring task
274
327
  2. Find the original recurring task in active tasks
@@ -299,6 +352,7 @@ CORE BEHAVIORAL RULES (Hierarchical)
299
352
  TODO.TXT FORMAT COMPLIANCE
300
353
  Priority: (A) (B) (C) | Projects: +name | Contexts: @location
301
354
  Due dates: due:YYYY-MM-DD | Completion: x YYYY-MM-DD description
355
+ Duration: duration:XX (e.g., duration:30m, duration:2h, duration:1d)
302
356
  Recurring tasks: rec:frequency[:interval] (e.g., rec:daily, rec:weekly:2, rec:monthly)
303
357
  Single symbols only (never ++project or @@context)
304
358
  Do not duplicate elements such as +context or (PRIORITY) within a single task description.
@@ -306,6 +360,31 @@ Do not duplicate elements such as +context or (PRIORITY) within a single task de
306
360
  Example recurring task format:
307
361
  (A) Take vitamins +health @home rec:daily due:2025-01-15
308
362
 
363
+ Example task with duration:
364
+ (A) Review quarterly report +work @office duration:2h due:2025-01-20
365
+
366
+ TOOL SELECTION STRATEGY
367
+
368
+ CRITICAL TOOL CHOICE GUIDELINES:
369
+ 1. **For Adding Project Tags (+project)**: ALWAYS use set_project()
370
+ 2. **For Adding Context Tags (@context)**: ALWAYS use set_context()
371
+ 3. **For Adding Due Dates** (due:YYYY-MM-DD): ALWAYS use set_due_date()
372
+ 4. **For Task Discovery**: Use list_tasks() once with appropriate filters, not multiple calls
373
+ 5. **For Task Completion**: Use list_tasks() + list_completed_tasks() + complete_task() sequence
374
+ 6. **For Task Addition**: Use list_tasks() + list_completed_tasks() + add_task() sequence
375
+
376
+ EXAMPLE CORRECT WORKFLOW:
377
+ User: "Add exercise and outdoors projects to mow lawn"
378
+ CORRECT: list_tasks() → set_project(task_number=15, projects=["exercise", "outdoors"])
379
+ INCORRECT: list_tasks(filter="+exercise") → list_tasks(filter="+outdoors") → append_to_task()
380
+
381
+ EFFICIENT DISCOVERY PRINCIPLES:
382
+ - Use list_tasks() once with NO filters to understand the full context
383
+ - Use list_completed_tasks() once with NO filters to understand historical patterns
384
+ - Avoid multiple discovery calls unless absolutely necessary for disambiguation
385
+ - When adding tags/projects/contexts, first discover the target task, then use the appropriate set_* tool
386
+ - Prefer single comprehensive discovery over multiple targeted searches
387
+
309
388
  TOOL CALL FORMAT
310
389
 
311
390
  IMPORTANT: Include concise reasoning in the content field when using tools.
@@ -142,12 +142,335 @@ class TodoShell:
142
142
  """Archive completed tasks."""
143
143
  return self.execute(["todo.sh", "-f", "archive"])
144
144
 
145
- def deduplicate(self) -> str:
146
- """Remove duplicate tasks."""
147
- try:
148
- return self.execute(["todo.sh", "-f", "deduplicate"])
149
- except TodoShellError as e:
150
- # Handle the case where no duplicates are found (not really an error)
151
- if "No duplicate tasks found" in str(e):
152
- return "No duplicate tasks found"
153
- raise
145
+ def set_due_date(self, task_number: int, due_date: str) -> str:
146
+ """
147
+ Set or update due date for a task by intelligently rewriting it.
148
+
149
+ Args:
150
+ task_number: The task number to modify
151
+ due_date: Due date in YYYY-MM-DD format, or empty string to remove due date
152
+
153
+ Returns:
154
+ The updated task description
155
+ """
156
+ # First, get the current task to parse its components
157
+ tasks_output = self.list_tasks()
158
+ task_lines = tasks_output.strip().split("\n")
159
+
160
+ # Find the task by its actual number (not array index)
161
+ current_task = None
162
+ for line in task_lines:
163
+ if line.strip():
164
+ # Extract task number from the beginning of the line (handling ANSI codes)
165
+ extracted_number = self._extract_task_number(line)
166
+ if extracted_number == task_number:
167
+ current_task = line
168
+ break
169
+
170
+ if not current_task:
171
+ raise TodoShellError(f"Task number {task_number} not found")
172
+
173
+ # Parse the current task components
174
+ components = self._parse_task_components(current_task)
175
+
176
+ # Update the due date (empty string removes it)
177
+ if due_date.strip():
178
+ components["due"] = due_date
179
+ else:
180
+ components["due"] = None
181
+
182
+ # Reconstruct the task
183
+ new_description = self._reconstruct_task(components)
184
+
185
+ # Replace the task with the new description
186
+ return self.replace(task_number, new_description)
187
+
188
+ def set_context(self, task_number: int, context: str) -> str:
189
+ """
190
+ Set or update context for a task by intelligently rewriting it.
191
+
192
+ Args:
193
+ task_number: The task number to modify
194
+ context: Context name (without @ symbol), or empty string to remove context
195
+
196
+ Returns:
197
+ The updated task description
198
+ """
199
+ # First, get the current task to parse its components
200
+ tasks_output = self.list_tasks()
201
+ task_lines = tasks_output.strip().split("\n")
202
+
203
+ # Find the task by its actual number (not array index)
204
+ current_task = None
205
+ for line in task_lines:
206
+ if line.strip():
207
+ # Extract task number from the beginning of the line (handling ANSI codes)
208
+ extracted_number = self._extract_task_number(line)
209
+ if extracted_number == task_number:
210
+ current_task = line
211
+ break
212
+
213
+ if not current_task:
214
+ raise TodoShellError(f"Task number {task_number} not found")
215
+
216
+ # Parse the current task components
217
+ components = self._parse_task_components(current_task)
218
+
219
+ # Update the context (empty string removes it)
220
+ if context.strip():
221
+ # Remove any existing @ symbols to prevent duplication
222
+ clean_context = context.strip().lstrip("@")
223
+ if not clean_context:
224
+ raise TodoShellError(
225
+ "Context name cannot be empty after removing @ symbol."
226
+ )
227
+ context_tag = f"@{clean_context}"
228
+ # Only add if not already present (deduplication)
229
+ if context_tag not in components["contexts"]:
230
+ components["contexts"] = [context_tag]
231
+ else:
232
+ # Context already exists, no change needed
233
+ return self._reconstruct_task(components)
234
+ else:
235
+ components["contexts"] = []
236
+
237
+ # Reconstruct the task
238
+ new_description = self._reconstruct_task(components)
239
+
240
+ # Replace the task with the new description
241
+ return self.replace(task_number, new_description)
242
+
243
+ def _extract_task_number(self, line: str) -> Optional[int]:
244
+ """
245
+ Extract task number from a line that may contain ANSI color codes.
246
+
247
+ Args:
248
+ line: Task line that may contain ANSI color codes
249
+
250
+ Returns:
251
+ Task number if found, None otherwise
252
+ """
253
+ from rich.text import Text
254
+
255
+ # Use rich to properly handle ANSI color codes
256
+ text = Text.from_ansi(line)
257
+ clean_line = text.plain
258
+
259
+ # Split on first space and check if first part is a number
260
+ parts = clean_line.split(" ", 1)
261
+ if parts and parts[0].isdigit():
262
+ return int(parts[0])
263
+ return None
264
+
265
+ def set_project(self, task_number: int, projects: list) -> str:
266
+ """
267
+ Set or update projects for a task by intelligently rewriting it.
268
+
269
+ Args:
270
+ task_number: The task number to modify
271
+ projects: List of project operations. Each item can be:
272
+ - "project" (add project)
273
+ - "-project" (remove project)
274
+ - Empty string is a NOOP
275
+
276
+ Returns:
277
+ The updated task description
278
+ """
279
+ # First, get the current task to parse its components
280
+ tasks_output = self.list_tasks()
281
+ task_lines = tasks_output.strip().split("\n")
282
+
283
+ # Find the task by its actual number (not array index)
284
+ current_task = None
285
+ for line in task_lines:
286
+ if line.strip():
287
+ # Extract task number from the beginning of the line (handling ANSI codes)
288
+ extracted_number = self._extract_task_number(line)
289
+ if extracted_number == task_number:
290
+ current_task = line
291
+ break
292
+
293
+ if not current_task:
294
+ raise TodoShellError(f"Task number {task_number} not found")
295
+
296
+ # Parse the current task components
297
+ components = self._parse_task_components(current_task)
298
+
299
+ # Store original projects to check if any changes were made
300
+ original_projects = components["projects"].copy()
301
+
302
+ # Handle project operations
303
+ if not projects:
304
+ # Empty list is a NOOP - return original task unchanged
305
+ return self._reconstruct_task(components)
306
+ else:
307
+ # Process each project operation
308
+ for project in projects:
309
+ if not project.strip():
310
+ # Empty string is a NOOP - skip this operation
311
+ continue
312
+ elif project.startswith("-"):
313
+ # Remove project
314
+ clean_project = project[1:].strip().lstrip("+")
315
+ if not clean_project:
316
+ raise TodoShellError(
317
+ "Project name cannot be empty after removing - and + symbols."
318
+ )
319
+ # Remove the project if it exists (with or without + prefix)
320
+ project_to_remove = f"+{clean_project}"
321
+ components["projects"] = [
322
+ p for p in components["projects"]
323
+ if p != project_to_remove and p != clean_project
324
+ ]
325
+ else:
326
+ # Add project
327
+ clean_project = project.strip().lstrip("+")
328
+ if not clean_project:
329
+ raise TodoShellError(
330
+ "Project name cannot be empty after removing + symbol."
331
+ )
332
+ project_tag = f"+{clean_project}"
333
+ # Only add if not already present (deduplication)
334
+ if project_tag not in components["projects"]:
335
+ components["projects"].append(project_tag)
336
+
337
+ # Check if any changes were actually made
338
+ if components["projects"] == original_projects:
339
+ # No changes made - return original task unchanged
340
+ return self._reconstruct_task(components)
341
+
342
+ # Reconstruct the task
343
+ new_description = self._reconstruct_task(components)
344
+
345
+ # Replace the task with the new description
346
+ return self.replace(task_number, new_description)
347
+
348
+ def _parse_task_components(self, task_line: str) -> dict:
349
+ """
350
+ Parse a todo.txt task line into its components.
351
+
352
+ Args:
353
+ task_line: Raw task line from todo.txt
354
+
355
+ Returns:
356
+ Dictionary with parsed components
357
+ """
358
+ # Remove ANSI color codes first using rich
359
+ from rich.text import Text
360
+ text = Text.from_ansi(task_line)
361
+ task_line = text.plain
362
+
363
+ # Remove task number prefix if present (e.g., "1 " or "1. ")
364
+ # First try the format without dot (standard todo.sh format)
365
+ if " " in task_line and task_line.split(" ")[0].isdigit():
366
+ task_line = task_line.split(" ", 1)[1]
367
+ # Fallback to dot format if present
368
+ elif ". " in task_line:
369
+ task_line = task_line.split(". ", 1)[1]
370
+
371
+ components = {
372
+ "priority": None,
373
+ "description": "",
374
+ "projects": [],
375
+ "contexts": [],
376
+ "due": None,
377
+ "recurring": None,
378
+ "other_tags": [],
379
+ }
380
+
381
+ # Use sets to automatically deduplicate projects and contexts
382
+ projects_set = set()
383
+ contexts_set = set()
384
+ other_tags_set = set()
385
+
386
+ # Split by spaces to process each word
387
+ words = task_line.split()
388
+
389
+ for word in words:
390
+ # Priority: (A), (B), etc.
391
+ if word.startswith("(") and word.endswith(")") and len(word) == 3:
392
+ priority = word[1]
393
+ if priority.isalpha() and priority.isupper():
394
+ components["priority"] = priority
395
+ continue
396
+
397
+ # Projects: +project
398
+ if word.startswith("+"):
399
+ projects_set.add(word)
400
+ continue
401
+
402
+ # Contexts: @context
403
+ if word.startswith("@"):
404
+ contexts_set.add(word)
405
+ continue
406
+
407
+ # Due date: due:YYYY-MM-DD
408
+ if word.startswith("due:"):
409
+ components["due"] = word[4:] # Remove 'due:' prefix
410
+ continue
411
+
412
+ # Recurring: rec:frequency[:interval]
413
+ if word.startswith("rec:"):
414
+ components["recurring"] = word
415
+ continue
416
+
417
+ # Other tags (like custom tags)
418
+ if (
419
+ ":" in word
420
+ and not word.startswith("due:")
421
+ and not word.startswith("rec:")
422
+ ):
423
+ other_tags_set.add(word)
424
+ continue
425
+
426
+ # Regular description text
427
+ if components["description"]:
428
+ components["description"] += " " + word
429
+ else:
430
+ components["description"] = word
431
+
432
+ # Convert sets back to sorted lists for consistent ordering
433
+ components["projects"] = sorted(list(projects_set))
434
+ components["contexts"] = sorted(list(contexts_set))
435
+ components["other_tags"] = sorted(list(other_tags_set))
436
+
437
+ return components
438
+
439
+ def _reconstruct_task(self, components: dict) -> str:
440
+ """
441
+ Reconstruct a task description from parsed components.
442
+
443
+ Args:
444
+ components: Dictionary with task components
445
+
446
+ Returns:
447
+ Reconstructed task description
448
+ """
449
+ parts = []
450
+
451
+ # Add priority if present
452
+ if components["priority"]:
453
+ parts.append(f"({components['priority']})")
454
+
455
+ # Add description
456
+ if components["description"]:
457
+ parts.append(components["description"])
458
+
459
+ # Add projects
460
+ parts.extend(components["projects"])
461
+
462
+ # Add contexts
463
+ parts.extend(components["contexts"])
464
+
465
+ # Add due date
466
+ if components["due"]:
467
+ parts.append(f"due:{components['due']}")
468
+
469
+ # Add recurring pattern
470
+ if components["recurring"]:
471
+ parts.append(components["recurring"])
472
+
473
+ # Add other tags
474
+ parts.extend(components["other_tags"])
475
+
476
+ return " ".join(parts)
@@ -166,7 +166,7 @@ class CLI:
166
166
  while True:
167
167
  try:
168
168
  # Print prompt character on separate line to prevent deletion
169
- self.console.print("\n[bold cyan]▶[/bold cyan]", end="\n")
169
+ self.console.print("\n[bold cyan]▶[/bold cyan]", end=" ")
170
170
  user_input = self.console.input().strip()
171
171
 
172
172
  if user_input.lower() in ["quit", "exit", "q"]:
@@ -244,8 +244,10 @@ class CLI:
244
244
  # Format the response and create a panel
245
245
  formatted_response = ResponseFormatter.format_response(response)
246
246
 
247
- # Get memory usage
248
- memory_usage = self._get_memory_usage()
247
+ # Get memory usage
248
+ # DISABLED FOR NOW
249
+ # memory_usage = self._get_memory_usage()
250
+ memory_usage = None
249
251
 
250
252
  # Create response panel with memory usage
251
253
  response_panel = PanelFormatter.create_response_panel(
@@ -10,7 +10,7 @@ Discovery Tools (Call FIRST):
10
10
  - list_completed_tasks(filter?, project?, context?, text_search?, date_from?, date_to?) - List completed tasks with optional filtering
11
11
 
12
12
  Task Management Tools:
13
- - add_task(description, priority?, project?, context?, due?) - Add new task to todo.txt
13
+ - add_task(description, priority?, project?, context?, due?, recurring?) - Add new task to todo.txt
14
14
  - complete_task(task_number) - Mark task as complete by line number
15
15
  - replace_task(task_number, new_description) - Replace entire task content
16
16
  - append_to_task(task_number, text) - Add text to end of existing task
@@ -20,12 +20,16 @@ Task Management Tools:
20
20
  Priority Management Tools:
21
21
  - set_priority(task_number, priority) - Set or change task priority (A-Z)
22
22
  - remove_priority(task_number) - Remove priority from task
23
+ - 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)
24
+ - set_context(task_number, context) - Set or update context for a task by intelligently rewriting it (use empty string to remove context)
25
+ - set_project(task_number, projects) - Set or update projects for a task by intelligently rewriting it (handles array of projects with add/remove operations)
23
26
 
24
27
  Utility Tools:
25
28
  - get_overview() - Show task statistics and summary
26
29
  - move_task(task_number, destination, source?) - Move task between files
27
30
  - archive_tasks() - Archive completed tasks from todo.txt to done.txt
28
31
  - get_calendar(month, year) - Get calendar for specific month and year
32
+ - parse_date(date_expression) - Convert natural language date expressions to YYYY-MM-DD format
29
33
  """
30
34
 
31
35
  import subprocess
@@ -296,7 +300,11 @@ class ToolCallHandler:
296
300
  "name": "append_to_task",
297
301
  "description": (
298
302
  "Add text to the end of an existing task. Use this when user wants "
299
- "to add additional information to a task without replacing it entirely."
303
+ "to add additional information to a task without replacing it entirely. "
304
+ "CRITICAL: DO NOT use this for adding project tags (+project) or context tags (@context) - "
305
+ "use set_project() or set_context() instead. "
306
+ "DO NOT use this for adding due dates - use set_due_date() instead. "
307
+ "This tool is for adding descriptive text, notes, or comments to tasks."
300
308
  ),
301
309
  "parameters": {
302
310
  "type": "object",
@@ -411,6 +419,97 @@ class ToolCallHandler:
411
419
  },
412
420
  },
413
421
  },
422
+ {
423
+ "type": "function",
424
+ "function": {
425
+ "name": "set_due_date",
426
+ "description": (
427
+ "Set or update the due date for a task by intelligently rewriting it. "
428
+ "This preserves all existing task components (priority, projects, contexts, etc.) "
429
+ "while updating or adding the due date. Use empty string to remove the due date. "
430
+ "IMPORTANT: Use list_tasks() first "
431
+ "to find the correct task number if user doesn't specify it. "
432
+ "Use parse_date() tool to convert natural language expressions like 'tomorrow', "
433
+ "'next week', 'by Friday' to YYYY-MM-DD format."
434
+ ),
435
+ "parameters": {
436
+ "type": "object",
437
+ "properties": {
438
+ "task_number": {
439
+ "type": "integer",
440
+ "description": "The line number of the task to modify (required)",
441
+ },
442
+ "due_date": {
443
+ "type": "string",
444
+ "description": "Due date in YYYY-MM-DD format, or empty string to remove due date (required). Use parse_date() tool to convert natural language expressions.",
445
+ },
446
+ },
447
+ "required": ["task_number", "due_date"],
448
+ },
449
+ },
450
+ },
451
+ {
452
+ "type": "function",
453
+ "function": {
454
+ "name": "set_context",
455
+ "description": (
456
+ "Set or update the context for a task by intelligently rewriting it. "
457
+ "This preserves all existing task components (priority, projects, due date, etc.) "
458
+ "while updating or adding the context. Use empty string to remove the context. "
459
+ "PREFERRED METHOD: Use this instead of append_to_task() when adding context tags (@context). "
460
+ "This tool properly manages context organization and prevents formatting issues. "
461
+ "IMPORTANT: Use list_tasks() first "
462
+ "to find the correct task number if user doesn't specify it."
463
+ ),
464
+ "parameters": {
465
+ "type": "object",
466
+ "properties": {
467
+ "task_number": {
468
+ "type": "integer",
469
+ "description": "The line number of the task to modify (required)",
470
+ },
471
+ "context": {
472
+ "type": "string",
473
+ "description": "Context to set for the task (required). Use empty string to remove context.",
474
+ },
475
+ },
476
+ "required": ["task_number", "context"],
477
+ },
478
+ },
479
+ },
480
+ {
481
+ "type": "function",
482
+ "function": {
483
+ "name": "set_project",
484
+ "description": (
485
+ "Set or update projects for a task by intelligently rewriting it. "
486
+ "This preserves all existing task components and manages projects intelligently. "
487
+ "Supports multiple projects, prevents duplicates, and groups them together. "
488
+ "Empty array or empty strings are NOOPs (no changes). "
489
+ "Use '-project' syntax to remove specific projects. "
490
+ "PREFERRED METHOD: Use this instead of append_to_task() when adding project tags (+project). "
491
+ "This tool properly manages project organization and prevents formatting issues. "
492
+ "IMPORTANT: Use list_tasks() first "
493
+ "to find the correct task number if user doesn't specify it. "
494
+ "Project names should be provided without the + symbol (e.g., 'work' not '+work')."
495
+ ),
496
+ "parameters": {
497
+ "type": "object",
498
+ "properties": {
499
+ "task_number": {
500
+ "type": "integer",
501
+ "description": "The line number of the task to modify (required)",
502
+ },
503
+ "projects": {
504
+ "type": "array",
505
+ "items": {"type": "string"},
506
+ "description": "Array of project operations. Each item can be: project name (add), '-project' (remove), or empty string (remove all) (required).",
507
+ },
508
+ },
509
+ "required": ["task_number", "projects"],
510
+ },
511
+ },
512
+ },
414
513
  {
415
514
  "type": "function",
416
515
  "function": {
@@ -724,10 +823,12 @@ class ToolCallHandler:
724
823
  "delete_task": self.todo_manager.delete_task,
725
824
  "set_priority": self.todo_manager.set_priority,
726
825
  "remove_priority": self.todo_manager.remove_priority,
826
+ "set_due_date": self.todo_manager.set_due_date,
827
+ "set_context": self.todo_manager.set_context,
828
+ "set_project": self.todo_manager.set_project,
727
829
  "get_overview": self.todo_manager.get_overview,
728
830
  "move_task": self.todo_manager.move_task,
729
831
  "archive_tasks": self.todo_manager.archive_tasks,
730
- "deduplicate_tasks": self.todo_manager.deduplicate_tasks,
731
832
  "parse_date": self._parse_date,
732
833
  "get_calendar": self._get_calendar,
733
834
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -51,7 +51,7 @@ A natural language interface for [todo.sh](https://github.com/todotxt/todo.txt-c
51
51
 
52
52
  ## What it does
53
53
 
54
- Transform natural language into todo.sh commands:
54
+ Transform natural language into todo.sh commands with intelligent task management:
55
55
 
56
56
  ```bash
57
57
  # Use interactively
@@ -62,91 +62,43 @@ todo-agent "add buy groceries to shopping list"
62
62
  todo-agent "show my work tasks"
63
63
  ```
64
64
 
65
- ## Quick Start
66
-
67
- ### 1. Install
65
+ ## Why Todo Agent?
68
66
 
69
- #### Prerequisites
67
+ **Speak naturally** instead of memorizing commands. Todo Agent understands "add dentist appointment next Monday" and automatically sets the right date, project, and context.
70
68
 
71
- **Install todo.sh (required)**
69
+ **Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
72
70
 
73
- todo.sh is the underlying task management system that todo-agent interfaces with.
71
+ **Work smarter** with automatic duplicate detection, recurring task handling, and calendar-aware scheduling.
74
72
 
75
- **macOS:**
76
- ```bash
77
- # Using Homebrew
78
- brew install todo-txt
79
- # Or using MacPorts
80
- sudo port install todo-txt
81
- ```
73
+ **Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
82
74
 
83
- **Linux:**
84
- ```bash
85
- # Ubuntu/Debian
86
- sudo apt-get install todo-txt-cli
87
- # CentOS/RHEL/Fedora
88
- sudo yum install todo-txt-cli
89
- # or
90
- sudo dnf install todo-txt-cli
91
- # Arch Linux
92
- sudo pacman -S todo-txt-cli
93
- ```
75
+ ## Quick Start
94
76
 
95
- **Windows:**
96
- ```bash
97
- # Using Chocolatey
98
- choco install todo-txt-cli
99
- # Using Scoop
100
- scoop install todo-txt-cli
101
- ```
77
+ ### 1. Install
102
78
 
103
- **From source:**
104
- ```bash
105
- git clone https://github.com/todotxt/todo.txt-cli.git
106
- cd todo.txt-cli
107
- make
108
- sudo make install
109
- ```
79
+ #### Install todo.sh (required)
110
80
 
111
- #### Configure todo.sh
81
+ **macOS:** `brew install todo-txt`
82
+ **Linux:** `sudo apt-get install todo-txt-cli` (Ubuntu/Debian) or `sudo pacman -S todo-txt-cli` (Arch)
83
+ **Windows:** `choco install todo-txt-cli` (Chocolatey) or `scoop install todo-txt-cli` (Scoop)
112
84
 
113
- After installing todo.sh, you need to set up your todo.txt repository:
85
+ #### Set up todo.sh
114
86
 
115
87
  ```bash
116
- # Create a directory for your todo files
117
- mkdir ~/todo
118
- cd ~/todo
119
-
120
- # Initialize todo.sh (this creates the initial todo.txt file)
88
+ # Create and initialize your todo directory
89
+ mkdir ~/todo && cd ~/todo
121
90
  todo.sh init
122
91
 
123
- # Verify installation
124
- todo.sh version
125
- ```
126
-
127
- **Important:** Set the `TODO_DIR` environment variable to point to your todo.txt repository:
128
-
129
- ```bash
92
+ # Add to your shell profile (.bashrc, .zshrc, etc.)
130
93
  export TODO_DIR="$HOME/todo"
131
94
  ```
132
95
 
133
- You can add this to your shell profile (`.bashrc`, `.zshrc`, etc.) to make it permanent.
134
-
135
96
  #### Install todo-agent
136
97
 
137
98
  ```bash
138
- # Clone and install from source
139
99
  git clone https://github.com/codeprimate/todo-agent.git
140
100
  cd todo_agent
141
-
142
- # Option 1: Install built package locally
143
101
  make install
144
-
145
- # Option 2: Install in development mode with dev dependencies
146
- make install-dev
147
-
148
- # Option 3: Install in development mode (basic)
149
- pip install -e .
150
102
  ```
151
103
 
152
104
  ### 2. Set up your LLM provider
@@ -200,6 +152,22 @@ todo-agent "show completed tasks"
200
152
  todo-agent "list my contexts"
201
153
  ```
202
154
 
155
+ ### Strategic Planning
156
+ ```bash
157
+ todo-agent "what should I do next?"
158
+ todo-agent "organize my tasks by priority"
159
+ todo-agent "show me everything due this week"
160
+ todo-agent "what tasks are blocking other work?"
161
+ ```
162
+
163
+ ### Natural Language Intelligence
164
+ ```bash
165
+ todo-agent "add dentist appointment next Monday"
166
+ todo-agent "set up recurring daily vitamin reminder"
167
+ todo-agent "move all completed tasks to archive"
168
+ todo-agent "show me tasks I can do from home"
169
+ ```
170
+
203
171
  ## Configuration
204
172
 
205
173
 
@@ -1,10 +1,10 @@
1
1
  todo_agent/__init__.py,sha256=RUowhd14r3tqB_7rl83unGV8oBjra3UOIl7jix-33fk,254
2
- todo_agent/_version.py,sha256=NCr4lkV1m1Jmisv_F-sazmV2gGdDIaVwqLyPwmjayqM,704
2
+ todo_agent/_version.py,sha256=051on7ZmwGNyKvbO1AXKoElw7RjLuRmeJqVOApytNd4,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=nRFcDMqZtumVipggzVe94Hwz9HDbc2CrTssk_HImwEI,13548
6
6
  todo_agent/core/exceptions.py,sha256=cPvvkIbKdI7l51wC7cE-ZxUi54P3nf2m7x2lMNMRFYM,399
7
- todo_agent/core/todo_manager.py,sha256=ZiCXfgS4kor3tdojwEKwD2G1lX-W9XuaoYvER82PHNM,10438
7
+ todo_agent/core/todo_manager.py,sha256=wT82Wpwby6LR9VacLXhdDiGiMNoQbovXmU9943e5X2M,14908
8
8
  todo_agent/infrastructure/__init__.py,sha256=SGbHXgzq6U1DMgOfWPMsWEK99zjPSF-6gzy7xqc5fsI,284
9
9
  todo_agent/infrastructure/calendar_utils.py,sha256=HmF0ykXF_6GbdoJvZLIv6fKwT6ipixoywdTMkIXmkGU,1871
10
10
  todo_agent/infrastructure/config.py,sha256=zyp6qOlg1nN_awphivlgGNBE6fL0Hf66YgvWxR8ldyQ,2117
@@ -13,17 +13,17 @@ todo_agent/infrastructure/llm_client.py,sha256=ZoObyqaRP6i_eqGYGfJWGeWTJ-VNxpY70
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=RVgHqX37k7q5xEaEXdhcCv2HYQiszs1QV39RxFFMIas,5902
16
- todo_agent/infrastructure/openrouter_client.py,sha256=7qusHuOH-zMKMrQw98v3MKwSWUJwL0mZsdShSjeoti0,7121
17
- todo_agent/infrastructure/todo_shell.py,sha256=z6kqUKDX-i4DfYJKoOLiPLCp8y6m1HdTDLHTvmLpzMc,5801
16
+ todo_agent/infrastructure/openrouter_client.py,sha256=FV240sQxoJ8j_Umh0qc6IorHkwk3LAFTSekGHKxuFwk,7000
17
+ todo_agent/infrastructure/todo_shell.py,sha256=kYS0UwDZU1OpUVpWMkVsnsxnHINoWliRqzDRD8VTXJQ,17375
18
18
  todo_agent/infrastructure/token_counter.py,sha256=PCKheOVJbp1s89yhh_i6iKgURMt9mVoYkwjQJCc2xCE,4958
19
- todo_agent/infrastructure/prompts/system_prompt.txt,sha256=j7N8GhMbyRR7QqfkokbCJiElv6CdAhc22PVzY6vrucE,15880
19
+ todo_agent/infrastructure/prompts/system_prompt.txt,sha256=HTv7Yl2oQi-AbQgdBzZKNKnmAMdOuOVRlbi75L-w-5g,19962
20
20
  todo_agent/interface/__init__.py,sha256=vDD3rQu4qDkpvVwGVtkDzE1M4IiSHYzTif4GbYSFWaI,457
21
- todo_agent/interface/cli.py,sha256=C746b83dQTiyUfthcC-pngwsJlmXZAu10_hxsSOMzdM,12022
21
+ todo_agent/interface/cli.py,sha256=9DUKTrxTwKx5Go0cA8nNCKDuM6QDmAG8jzK4_v_1WS8,12096
22
22
  todo_agent/interface/formatters.py,sha256=DiQLemndiuFWjLcBRPpu1wVnJcoYAFP8t_fJstOgaDs,18918
23
- todo_agent/interface/tools.py,sha256=_GlZwX09dYpqPB2Q-EsgGguA1PVl8_mRCdks6ucaJgU,39686
24
- todo_agent-0.2.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
- todo_agent-0.2.8.dist-info/METADATA,sha256=ynVrlFUs89Qfgp6R3YnbvZGykjHD4CjNnJaLNm-oz1w,10047
26
- todo_agent-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- todo_agent-0.2.8.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
28
- todo_agent-0.2.8.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
29
- todo_agent-0.2.8.dist-info/RECORD,,
23
+ todo_agent/interface/tools.py,sha256=OE9qOwWo7itqmNc85tZSCqWHCvValMa4fQE-EygnNNQ,45929
24
+ todo_agent-0.2.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
+ todo_agent-0.2.9.dist-info/METADATA,sha256=OPm-ho33qlhnZqrnNlqRR2p4tLqlXzkNk-fgcY73Dg0,10134
26
+ todo_agent-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ todo_agent-0.2.9.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
28
+ todo_agent-0.2.9.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
29
+ todo_agent-0.2.9.dist-info/RECORD,,