todo-agent 0.3.3__py3-none-any.whl → 0.3.6__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 +2 -2
- todo_agent/core/todo_manager.py +299 -171
- todo_agent/infrastructure/inference.py +16 -9
- todo_agent/infrastructure/prompts/system_prompt.txt +124 -17
- todo_agent/infrastructure/todo_shell.py +63 -0
- todo_agent/interface/cli.py +53 -0
- todo_agent/interface/formatters.py +1 -0
- todo_agent/interface/tools.py +54 -10
- todo_agent/main.py +37 -2
- {todo_agent-0.3.3.dist-info → todo_agent-0.3.6.dist-info}/METADATA +1 -1
- {todo_agent-0.3.3.dist-info → todo_agent-0.3.6.dist-info}/RECORD +15 -15
- {todo_agent-0.3.3.dist-info → todo_agent-0.3.6.dist-info}/WHEEL +0 -0
- {todo_agent-0.3.3.dist-info → todo_agent-0.3.6.dist-info}/entry_points.txt +0 -0
- {todo_agent-0.3.3.dist-info → todo_agent-0.3.6.dist-info}/licenses/LICENSE +0 -0
- {todo_agent-0.3.3.dist-info → todo_agent-0.3.6.dist-info}/top_level.txt +0 -0
@@ -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(
|
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(
|
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(
|
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,135 @@ 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
|
-
-
|
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
|
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:
|
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.
|
41
|
-
4.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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`
|
106
|
+
|
107
|
+
### Task Completion Protocol
|
108
|
+
NOTE: use complete_task() for existing non-recurring tasks
|
109
|
+
1. **Discovery**: Use `list_tasks()` to search semantically in active tasks
|
110
|
+
2. **For Non-Recurring Tasks**:
|
111
|
+
- The task will NOT have the tag "rec:"
|
112
|
+
- Single match → use `complete_task(task_number='XX')` to mark existing active tasks complete
|
113
|
+
- Multiple/fuzzy → show options
|
114
|
+
- No match → suggest alternatives
|
115
|
+
3. **For Recurring Tasks** (containing +daily, +weekly, +monthly, +weekdays, etc.):
|
116
|
+
- The task will have the tag "rec:"
|
117
|
+
- **USE** `create_completed_task()` with the original task number as parent_number
|
118
|
+
- **PRESERVE** the original recurring task for future occurrences. **IMPORTANT** DO NOT MARK COMPLETE! DO NOT MODIFY!
|
119
|
+
- **TOOL CALL**: `create_completed_task(description='Task description', parent_number='XX', completion_date='YYYY-MM-DD', context='context', project='project')`
|
120
|
+
|
121
|
+
|
122
|
+
### Recurring Task Completion Examples
|
123
|
+
NOTE: ONLY USE create_completed_task() for RECURRING TASKS!
|
124
|
+
- **User says**: "I put out the trash" → Find `(B) Put out trash and recycling +weekly +thursday @home duration:5m`
|
125
|
+
- **Tool Call**: ONLY FOR EXISTING TASKS! `create_completed_task(description='Put out trash and recycling', parent_number='B', completion_date='YYYY-MM-DD', context='home', project='weekly')`
|
126
|
+
- **Result**: Original task remains active for next Thursday
|
127
|
+
- **User says**: "Done with standup" → Find `Standup meeting at 9:00AM +daily +weekdays @office duration:10m`
|
128
|
+
- **Tool Call**: ONLY FOR EXISTING TASKS! `create_completed_task(description='Standup meeting at 9:00AM', parent_number='XX', completion_date='YYYY-MM-DD', context='office', project='daily')`
|
129
|
+
- **Result**: Original task remains active for next weekday
|
48
130
|
|
49
131
|
### Task Suggestions
|
50
132
|
**Trigger**: User asks, seems stuck, or after completions
|
51
|
-
**Method**:
|
133
|
+
**Method**:
|
134
|
+
- Identify tasks within the user's implied temporal scope
|
135
|
+
- Consider that today is {current_datetime}, and match any recurring tasks
|
136
|
+
- Pay careful attention to due dates and their relation to the current date
|
137
|
+
- @office and work tasks are always the highest priority
|
138
|
+
- +wip tasks are considered high priority and due today
|
52
139
|
- Balance urgency and priority. Use your best judgment.
|
53
|
-
- Logical dependencies
|
140
|
+
- Logical and explicit dependencies should be suggested first (tasks that unblock others get priority)
|
54
141
|
- Then urgency (overdue → due today → due soon)
|
55
|
-
-
|
56
|
-
-
|
142
|
+
- Be exhaustive in your search and mention everything relevant
|
143
|
+
- Always state days of the week
|
57
144
|
|
58
145
|
### Context Patterns
|
59
146
|
- `@phone`: calls, appointments
|
@@ -64,6 +151,23 @@ x YYYY-MM-DD Completed task description
|
|
64
151
|
|
65
152
|
### Project Patterns
|
66
153
|
- Health → `+health`, Work → `+work`, Bills → `+bills`, etc.
|
154
|
+
- Recurring tasks:
|
155
|
+
- `+daily`: ALWAYS consider due today
|
156
|
+
- `+weekly`: ALWAYS consider due on the specific day THIS WEEK
|
157
|
+
|
158
|
+
## Notes Protocol
|
159
|
+
**When the user wants to create a note**:
|
160
|
+
**Triggers**: 'note:', or 'Create a note', or 'progress:', etc
|
161
|
+
**Action**: create_completed_task()
|
162
|
+
**Method**:
|
163
|
+
1. ALWAYS Identify semantically similar tasks that are the likely parent task
|
164
|
+
2. IF there is LIKELY match, that is the parent and parent_number
|
165
|
+
4. Create a completed task with create_completed_task():
|
166
|
+
- **IMPORTANT**: ALWAYS assign the parent_number if a match was found
|
167
|
+
- with inferred parent_number, completion_date, context, and project
|
168
|
+
- Task description should never include date of completion
|
169
|
+
- NO priority
|
170
|
+
**Response**: ALWAYS refer to your note actions as creating a note, NOT creating a task
|
67
171
|
|
68
172
|
## Critical Rules
|
69
173
|
- **Overdue definition**: A task is overdue IF AND _ONLY IF_ due < {current_datetime}. None is an acceptable answer!
|
@@ -71,11 +175,14 @@ x YYYY-MM-DD Completed task description
|
|
71
175
|
- **Task ordering**: Always dependencies first, then urgency
|
72
176
|
- **Data integrity**: Only use real tool data, never fabricate
|
73
177
|
- **Completion date reasoning**: Always explain date suggestions briefly
|
178
|
+
- **Parent Tasks**: Task dependencies are explicitly indicated by `parent:XX` tags
|
179
|
+
- **Parent Task Detection**: Always identify and establish parent-child relationships when users indicate task dependencies through natural language
|
74
180
|
|
75
181
|
## Tool Selection Strategy
|
76
182
|
- Project tags: use `set_project()`
|
77
183
|
- Context tags: use `set_context()`
|
78
184
|
- Due dates: use `set_due_date()`
|
185
|
+
- Parent relationships: use `set_parent()`
|
79
186
|
- Discovery: `list_tasks()` once for full context
|
80
187
|
- Completion: `list_tasks()` + `complete_task()`
|
81
188
|
- 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:
|
@@ -178,6 +182,10 @@ class TodoShell:
|
|
178
182
|
"""Archive completed tasks."""
|
179
183
|
return self.execute(["todo.sh", "-f", "archive"])
|
180
184
|
|
185
|
+
def get_help(self) -> str:
|
186
|
+
"""Get todo.sh help output."""
|
187
|
+
return self.execute(["todo.sh", "help"], suppress_color=False)
|
188
|
+
|
181
189
|
def set_due_date(self, task_number: int, due_date: str) -> str:
|
182
190
|
"""
|
183
191
|
Set or update due date for a task by intelligently rewriting it.
|
@@ -276,6 +284,61 @@ class TodoShell:
|
|
276
284
|
# Replace the task with the new description
|
277
285
|
return self.replace(task_number, new_description)
|
278
286
|
|
287
|
+
def set_parent(self, task_number: int, parent_number: Optional[int]) -> str:
|
288
|
+
"""
|
289
|
+
Set or update parent task number for a task by intelligently rewriting it.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
task_number: The task number to modify
|
293
|
+
parent_number: Parent task number, or None to remove parent
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
The updated task description
|
297
|
+
"""
|
298
|
+
# First, get the current task to parse its components
|
299
|
+
tasks_output = self.list_tasks()
|
300
|
+
task_lines = tasks_output.strip().split("\n")
|
301
|
+
|
302
|
+
# Find the task by its actual number (not array index)
|
303
|
+
current_task = None
|
304
|
+
for line in task_lines:
|
305
|
+
if line.strip():
|
306
|
+
# Extract task number from the beginning of the line (handling ANSI codes)
|
307
|
+
extracted_number = self._extract_task_number(line)
|
308
|
+
if extracted_number == task_number:
|
309
|
+
current_task = line
|
310
|
+
break
|
311
|
+
|
312
|
+
if not current_task:
|
313
|
+
raise TodoShellError(f"Task number {task_number} not found")
|
314
|
+
|
315
|
+
# Parse the current task components
|
316
|
+
components = self._parse_task_components(current_task)
|
317
|
+
|
318
|
+
# Update the parent (None removes it)
|
319
|
+
if parent_number is not None:
|
320
|
+
if not isinstance(parent_number, int) or parent_number <= 0:
|
321
|
+
raise TodoShellError(
|
322
|
+
f"Invalid parent_number '{parent_number}'. Must be a positive integer."
|
323
|
+
)
|
324
|
+
parent_tag = f"parent:{parent_number}"
|
325
|
+
# Remove any existing parent tag and add the new one
|
326
|
+
components["other_tags"] = [
|
327
|
+
tag for tag in components["other_tags"] if not tag.startswith("parent:")
|
328
|
+
]
|
329
|
+
components["other_tags"].append(parent_tag)
|
330
|
+
else:
|
331
|
+
# Remove parent tag
|
332
|
+
components["other_tags"] = [
|
333
|
+
tag for tag in components["other_tags"] if not tag.startswith("parent:")
|
334
|
+
]
|
335
|
+
|
336
|
+
# Reconstruct the task
|
337
|
+
new_description = self._reconstruct_task(components)
|
338
|
+
|
339
|
+
# Replace the task with the new description
|
340
|
+
return self.replace(task_number, new_description)
|
341
|
+
|
279
342
|
def _extract_task_number(self, line: str) -> Optional[int]:
|
280
343
|
"""
|
281
344
|
Extract task number from a line that may contain ANSI color codes.
|
todo_agent/interface/cli.py
CHANGED
@@ -225,6 +225,23 @@ class CLI:
|
|
225
225
|
self.console.print(table)
|
226
226
|
self.console.print("Or just type your request naturally!", style="italic green")
|
227
227
|
|
228
|
+
def _print_todo_help(self) -> None:
|
229
|
+
"""Print todo.sh help information."""
|
230
|
+
try:
|
231
|
+
# Get todo.sh help output
|
232
|
+
help_output = self.todo_shell.get_help()
|
233
|
+
formatted_output = TaskFormatter.format_task_list(help_output)
|
234
|
+
help_panel = PanelFormatter.create_task_panel(
|
235
|
+
formatted_output, title="📋 Todo.sh Help"
|
236
|
+
)
|
237
|
+
self.console.print(help_panel)
|
238
|
+
except Exception as e:
|
239
|
+
self.logger.error(f"Error getting todo.sh help: {e!s}")
|
240
|
+
error_msg = ResponseFormatter.format_error(
|
241
|
+
f"Failed to get todo.sh help: {e!s}"
|
242
|
+
)
|
243
|
+
self.console.print(error_msg)
|
244
|
+
|
228
245
|
def _print_about(self) -> None:
|
229
246
|
"""Print about information in a formatted panel."""
|
230
247
|
about_panel = PanelFormatter.create_about_panel()
|
@@ -280,6 +297,37 @@ class CLI:
|
|
280
297
|
if not user_input:
|
281
298
|
continue
|
282
299
|
|
300
|
+
# Handle todo.sh passthrough commands (starting with /)
|
301
|
+
if user_input.startswith("/"):
|
302
|
+
self.logger.debug(
|
303
|
+
f"Processing todo.sh passthrough command: {user_input}"
|
304
|
+
)
|
305
|
+
try:
|
306
|
+
# Remove the leading / and execute as todo.sh command
|
307
|
+
todo_command = user_input[1:].strip()
|
308
|
+
if not todo_command:
|
309
|
+
self.console.print(
|
310
|
+
ResponseFormatter.format_error("Empty todo.sh command")
|
311
|
+
)
|
312
|
+
continue
|
313
|
+
|
314
|
+
# Execute the todo.sh command directly
|
315
|
+
output = self.todo_shell.execute(
|
316
|
+
["todo.sh", *todo_command.split()]
|
317
|
+
)
|
318
|
+
formatted_output = TaskFormatter.format_task_list(output)
|
319
|
+
task_panel = PanelFormatter.create_task_panel(
|
320
|
+
formatted_output, title="📋 Todo.sh Output"
|
321
|
+
)
|
322
|
+
self.console.print(task_panel)
|
323
|
+
except Exception as e:
|
324
|
+
self.logger.error(f"Error executing todo.sh command: {e!s}")
|
325
|
+
error_msg = ResponseFormatter.format_error(
|
326
|
+
f"Todo.sh command failed: {e!s}"
|
327
|
+
)
|
328
|
+
self.console.print(error_msg)
|
329
|
+
continue
|
330
|
+
|
283
331
|
# Handle special commands
|
284
332
|
if user_input.lower() == "clear":
|
285
333
|
self.logger.info("User requested conversation clear")
|
@@ -302,6 +350,11 @@ class CLI:
|
|
302
350
|
self._print_help()
|
303
351
|
continue
|
304
352
|
|
353
|
+
if user_input.lower() == "todo-help":
|
354
|
+
self.logger.debug("User requested todo.sh help")
|
355
|
+
self._print_todo_help()
|
356
|
+
continue
|
357
|
+
|
305
358
|
if user_input.lower() == "about":
|
306
359
|
self.logger.debug("User requested about information")
|
307
360
|
self._print_about()
|
@@ -357,6 +357,7 @@ class TableFormatter:
|
|
357
357
|
("clear", "Clear conversation history"),
|
358
358
|
("stats", "Show conversation statistics"),
|
359
359
|
("help", "Show this help message"),
|
360
|
+
("todo-help", "Show todo.sh help"),
|
360
361
|
("about", "Show application information"),
|
361
362
|
("list", "List all tasks (no LLM interaction)"),
|
362
363
|
("done", "List completed tasks (no LLM interaction)"),
|
todo_agent/interface/tools.py
CHANGED
@@ -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
|
-
-
|
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
|
-
"
|
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", "
|
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,16 +623,17 @@ class ToolCallHandler:
|
|
585
623
|
{
|
586
624
|
"type": "function",
|
587
625
|
"function": {
|
588
|
-
"name": "
|
626
|
+
"name": "create_completed_task",
|
589
627
|
"description": (
|
590
628
|
"Create a task and immediately mark it as completed. "
|
591
|
-
"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') "
|
629
|
+
"USE CASE: WHEN NO MATCH IS FOUND! 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') "
|
592
630
|
"and you have already researched existing tasks to determine no match exists. "
|
593
|
-
"WORKFLOW: 1) Use list_tasks() to search for existing tasks,
|
594
|
-
"
|
631
|
+
"WORKFLOW: 1) Use list_tasks() to search for existing tasks, "
|
632
|
+
"2) Use list_completed_tasks() to verify it's not already done, "
|
633
|
+
"3) If a match is found, use complete_task() to mark it complete, "
|
634
|
+
"4) If no match found, call this tool to create and complete the task in one operation. "
|
595
635
|
"STRATEGIC CONTEXT: This is a convenience tool for the common pattern of 'I did X on [date]' - "
|
596
|
-
"it creates a task with the specified completion date and immediately marks it complete. "
|
597
|
-
"The LLM should handle the research and decision-making about whether to use this tool."
|
636
|
+
"when no task match is found, it creates a task with the specified completion date and immediately marks it complete. "
|
598
637
|
),
|
599
638
|
"parameters": {
|
600
639
|
"type": "object",
|
@@ -615,6 +654,10 @@ class ToolCallHandler:
|
|
615
654
|
"type": "string",
|
616
655
|
"description": "Optional context name (without the @ symbol) for new task creation",
|
617
656
|
},
|
657
|
+
"parent_number": {
|
658
|
+
"type": "integer",
|
659
|
+
"description": "Optional parent task number (required for subtasks)",
|
660
|
+
},
|
618
661
|
},
|
619
662
|
"required": ["description"],
|
620
663
|
},
|
@@ -897,11 +940,12 @@ class ToolCallHandler:
|
|
897
940
|
"set_due_date": self.todo_manager.set_due_date,
|
898
941
|
"set_context": self.todo_manager.set_context,
|
899
942
|
"set_project": self.todo_manager.set_project,
|
943
|
+
"set_parent": self.todo_manager.set_parent,
|
900
944
|
"move_task": self.todo_manager.move_task,
|
901
945
|
"archive_tasks": self.todo_manager.archive_tasks,
|
902
946
|
"parse_date": self._parse_date,
|
903
947
|
"get_calendar": self._get_calendar,
|
904
|
-
"
|
948
|
+
"create_completed_task": self.todo_manager.create_completed_task,
|
905
949
|
"restore_completed_task": self.todo_manager.restore_completed_task,
|
906
950
|
}
|
907
951
|
|
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,21 @@ Examples:
|
|
23
26
|
""",
|
24
27
|
)
|
25
28
|
|
29
|
+
parser.add_argument(
|
30
|
+
"--version",
|
31
|
+
"-v",
|
32
|
+
action="version",
|
33
|
+
version=f"%(prog)s {__version__}",
|
34
|
+
help="Show version information and exit",
|
35
|
+
)
|
36
|
+
|
37
|
+
parser.add_argument(
|
38
|
+
"--help",
|
39
|
+
"-h",
|
40
|
+
action="help",
|
41
|
+
help="Show this help message and exit",
|
42
|
+
)
|
43
|
+
|
26
44
|
parser.add_argument(
|
27
45
|
"command",
|
28
46
|
nargs="?",
|
@@ -37,11 +55,28 @@ Examples:
|
|
37
55
|
if args.command:
|
38
56
|
# Single command mode
|
39
57
|
# Handle special commands that don't need LLM processing
|
40
|
-
if args.command.lower() in ["help", "about"]:
|
58
|
+
if args.command.lower() in ["help", "about", "todo-help"]:
|
41
59
|
if args.command.lower() == "help":
|
42
60
|
cli._print_help()
|
43
61
|
elif args.command.lower() == "about":
|
44
62
|
cli._print_about()
|
63
|
+
elif args.command.lower() == "todo-help":
|
64
|
+
cli._print_todo_help()
|
65
|
+
elif args.command.startswith("/"):
|
66
|
+
# Handle todo.sh passthrough commands
|
67
|
+
try:
|
68
|
+
# Remove the leading / and execute as todo.sh command
|
69
|
+
todo_command = args.command[1:].strip()
|
70
|
+
if not todo_command:
|
71
|
+
print("Error: Empty todo.sh command")
|
72
|
+
sys.exit(1)
|
73
|
+
|
74
|
+
# Execute the todo.sh command directly
|
75
|
+
output = cli.todo_shell.execute(["todo.sh", *todo_command.split()])
|
76
|
+
print(output)
|
77
|
+
except Exception as e:
|
78
|
+
print(f"Error: Todo.sh command failed: {e}")
|
79
|
+
sys.exit(1)
|
45
80
|
else:
|
46
81
|
# Process through LLM
|
47
82
|
response = cli.run_single_request(args.command)
|