aloop 0.1.1__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.
Files changed (66) hide show
  1. agent/__init__.py +0 -0
  2. agent/agent.py +182 -0
  3. agent/base.py +406 -0
  4. agent/context.py +126 -0
  5. agent/prompts/__init__.py +1 -0
  6. agent/todo.py +149 -0
  7. agent/tool_executor.py +54 -0
  8. agent/verification.py +135 -0
  9. aloop-0.1.1.dist-info/METADATA +252 -0
  10. aloop-0.1.1.dist-info/RECORD +66 -0
  11. aloop-0.1.1.dist-info/WHEEL +5 -0
  12. aloop-0.1.1.dist-info/entry_points.txt +2 -0
  13. aloop-0.1.1.dist-info/licenses/LICENSE +21 -0
  14. aloop-0.1.1.dist-info/top_level.txt +9 -0
  15. cli.py +19 -0
  16. config.py +146 -0
  17. interactive.py +865 -0
  18. llm/__init__.py +51 -0
  19. llm/base.py +26 -0
  20. llm/compat.py +226 -0
  21. llm/content_utils.py +309 -0
  22. llm/litellm_adapter.py +450 -0
  23. llm/message_types.py +245 -0
  24. llm/model_manager.py +265 -0
  25. llm/retry.py +95 -0
  26. main.py +246 -0
  27. memory/__init__.py +20 -0
  28. memory/compressor.py +554 -0
  29. memory/manager.py +538 -0
  30. memory/serialization.py +82 -0
  31. memory/short_term.py +88 -0
  32. memory/store/__init__.py +6 -0
  33. memory/store/memory_store.py +100 -0
  34. memory/store/yaml_file_memory_store.py +414 -0
  35. memory/token_tracker.py +203 -0
  36. memory/types.py +51 -0
  37. tools/__init__.py +6 -0
  38. tools/advanced_file_ops.py +557 -0
  39. tools/base.py +51 -0
  40. tools/calculator.py +50 -0
  41. tools/code_navigator.py +975 -0
  42. tools/explore.py +254 -0
  43. tools/file_ops.py +150 -0
  44. tools/git_tools.py +791 -0
  45. tools/notify.py +69 -0
  46. tools/parallel_execute.py +420 -0
  47. tools/session_manager.py +205 -0
  48. tools/shell.py +147 -0
  49. tools/shell_background.py +470 -0
  50. tools/smart_edit.py +491 -0
  51. tools/todo.py +130 -0
  52. tools/web_fetch.py +673 -0
  53. tools/web_search.py +61 -0
  54. utils/__init__.py +15 -0
  55. utils/logger.py +105 -0
  56. utils/model_pricing.py +49 -0
  57. utils/runtime.py +75 -0
  58. utils/terminal_ui.py +422 -0
  59. utils/tui/__init__.py +39 -0
  60. utils/tui/command_registry.py +49 -0
  61. utils/tui/components.py +306 -0
  62. utils/tui/input_handler.py +393 -0
  63. utils/tui/model_ui.py +204 -0
  64. utils/tui/progress.py +292 -0
  65. utils/tui/status_bar.py +178 -0
  66. utils/tui/theme.py +165 -0
tools/smart_edit.py ADDED
@@ -0,0 +1,491 @@
1
+ """Smart code editing tool with fuzzy matching and preview capabilities.
2
+
3
+ This tool provides advanced editing features beyond the basic EditTool:
4
+ - Fuzzy matching: Handles whitespace and indentation differences
5
+ - Diff preview: Shows before/after comparison
6
+ - Auto backup: Creates .bak files before editing (disabled by default in git repos)
7
+ - Rollback: Can revert changes if editing fails
8
+ """
9
+
10
+ import subprocess
11
+ from difflib import SequenceMatcher, unified_diff
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional, Tuple
14
+
15
+ import aiofiles
16
+ import aiofiles.os
17
+
18
+ from tools.base import BaseTool
19
+
20
+
21
+ def _is_git_repo(path: Path) -> bool:
22
+ """Check if the given path is inside a git repository."""
23
+ try:
24
+ result = subprocess.run(
25
+ ["git", "rev-parse", "--is-inside-work-tree"],
26
+ cwd=path.parent if path.is_file() else path,
27
+ capture_output=True,
28
+ text=True,
29
+ timeout=5,
30
+ )
31
+ return result.returncode == 0 and result.stdout.strip() == "true"
32
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
33
+ return False
34
+
35
+
36
+ class SmartEditTool(BaseTool):
37
+ """Intelligent code editing with fuzzy matching and safety features."""
38
+
39
+ def __init__(self):
40
+ self.fuzzy_threshold = 0.8 # Minimum similarity ratio for fuzzy matching
41
+
42
+ @property
43
+ def name(self) -> str:
44
+ return "smart_edit"
45
+
46
+ @property
47
+ def description(self) -> str:
48
+ return """Intelligent code editing tool with fuzzy matching and preview.
49
+
50
+ This is the RECOMMENDED tool for editing code (prefer over edit_file).
51
+
52
+ Features:
53
+ - Fuzzy matching: Automatically handles whitespace/indentation differences
54
+ - Diff preview: Shows exactly what will change
55
+ - Auto backup: Creates .bak files before editing (disabled in git repos)
56
+ - Rollback: Automatically reverts if editing fails
57
+
58
+ Modes:
59
+ 1. diff_replace: Find and replace code with fuzzy matching (MOST COMMON)
60
+ - Handles indentation/whitespace differences automatically
61
+ - Shows diff preview before applying
62
+ - Required: old_code, new_code
63
+
64
+ 2. smart_insert: Insert code relative to an anchor point
65
+ - Find an anchor line and insert before/after it
66
+ - Required: anchor, code, position ('before'/'after')
67
+
68
+ 3. block_edit: Edit a range of lines
69
+ - Replace lines from start_line to end_line
70
+ - Required: start_line, end_line, new_content
71
+
72
+ Examples:
73
+ # Replace a function with fuzzy matching
74
+ smart_edit(
75
+ file_path="agent/base.py",
76
+ mode="diff_replace",
77
+ old_code="def run(self, task):\\n # old implementation",
78
+ new_code="def run(self, task):\\n # new implementation"
79
+ )
80
+
81
+ # Insert after a specific line
82
+ smart_edit(
83
+ file_path="config.py",
84
+ mode="smart_insert",
85
+ anchor="class Config:",
86
+ code=" FEATURE_FLAG = True",
87
+ position="after"
88
+ )
89
+
90
+ IMPORTANT:
91
+ - Always use fuzzy_match=True (default) for code to handle formatting
92
+ - Set dry_run=True first to preview changes
93
+ - Backup is disabled by default in git repos (use create_backup=True to force)"""
94
+
95
+ @property
96
+ def parameters(self) -> Dict[str, Any]:
97
+ return {
98
+ "file_path": {"type": "string", "description": "Path to the file to edit"},
99
+ "mode": {
100
+ "type": "string",
101
+ "description": "Edit mode: diff_replace, smart_insert, or block_edit",
102
+ "enum": ["diff_replace", "smart_insert", "block_edit"],
103
+ },
104
+ "old_code": {
105
+ "type": "string",
106
+ "description": "Code to find and replace (diff_replace mode). Can be approximate - fuzzy matching will find it.",
107
+ },
108
+ "new_code": {"type": "string", "description": "New code to insert (diff_replace mode)"},
109
+ "anchor": {
110
+ "type": "string",
111
+ "description": "Anchor line to insert relative to (smart_insert mode)",
112
+ },
113
+ "code": {"type": "string", "description": "Code to insert (smart_insert mode)"},
114
+ "position": {
115
+ "type": "string",
116
+ "description": "Where to insert: 'before' or 'after' anchor (smart_insert mode)",
117
+ "enum": ["before", "after"],
118
+ },
119
+ "start_line": {
120
+ "type": "integer",
121
+ "description": "Starting line number (block_edit mode, 1-indexed)",
122
+ },
123
+ "end_line": {
124
+ "type": "integer",
125
+ "description": "Ending line number (block_edit mode, 1-indexed, inclusive)",
126
+ },
127
+ "fuzzy_match": {
128
+ "type": "boolean",
129
+ "description": "Enable fuzzy matching for whitespace differences (default: true)",
130
+ },
131
+ "dry_run": {
132
+ "type": "boolean",
133
+ "description": "Preview changes without applying (default: false)",
134
+ },
135
+ "create_backup": {
136
+ "type": "boolean",
137
+ "description": "Create .bak backup file (default: false in git repos, true otherwise)",
138
+ },
139
+ "show_diff": {
140
+ "type": "boolean",
141
+ "description": "Show diff preview even when not dry_run (default: true)",
142
+ },
143
+ }
144
+
145
+ async def execute(
146
+ self,
147
+ file_path: str,
148
+ mode: str,
149
+ old_code: str = "",
150
+ new_code: str = "",
151
+ anchor: str = "",
152
+ code: str = "",
153
+ position: str = "after",
154
+ start_line: int = 0,
155
+ end_line: int = 0,
156
+ fuzzy_match: bool = True,
157
+ dry_run: bool = False,
158
+ create_backup: Optional[bool] = None,
159
+ show_diff: bool = True,
160
+ **kwargs,
161
+ ) -> str:
162
+ """Execute smart edit operation."""
163
+ try:
164
+ path = Path(file_path)
165
+
166
+ # Validation
167
+ if not await aiofiles.os.path.exists(str(path)):
168
+ return f"Error: File does not exist: {file_path}"
169
+
170
+ # Determine create_backup default: False in git repos, True otherwise
171
+ if create_backup is None:
172
+ create_backup = not _is_git_repo(path)
173
+
174
+ # Read original content
175
+ async with aiofiles.open(path, encoding="utf-8") as f:
176
+ original_content = await f.read()
177
+
178
+ # Execute the appropriate edit mode
179
+ if mode == "diff_replace":
180
+ result = await self._diff_replace(
181
+ path,
182
+ original_content,
183
+ old_code,
184
+ new_code,
185
+ fuzzy_match,
186
+ dry_run,
187
+ create_backup,
188
+ show_diff,
189
+ )
190
+ elif mode == "smart_insert":
191
+ result = await self._smart_insert(
192
+ path,
193
+ original_content,
194
+ anchor,
195
+ code,
196
+ position,
197
+ dry_run,
198
+ create_backup,
199
+ show_diff,
200
+ )
201
+ elif mode == "block_edit":
202
+ result = await self._block_edit(
203
+ path,
204
+ original_content,
205
+ start_line,
206
+ end_line,
207
+ new_code,
208
+ dry_run,
209
+ create_backup,
210
+ show_diff,
211
+ )
212
+ else:
213
+ return f"Error: Unknown mode '{mode}'. Supported: diff_replace, smart_insert, block_edit"
214
+
215
+ return result
216
+
217
+ except Exception as e:
218
+ return f"Error executing smart_edit: {str(e)}"
219
+
220
+ async def _diff_replace(
221
+ self,
222
+ path: Path,
223
+ original_content: str,
224
+ old_code: str,
225
+ new_code: str,
226
+ fuzzy_match: bool,
227
+ dry_run: bool,
228
+ create_backup: bool,
229
+ show_diff: bool,
230
+ ) -> str:
231
+ """Replace code with fuzzy matching."""
232
+ if not old_code:
233
+ return "Error: old_code parameter is required for diff_replace mode"
234
+
235
+ # Try exact match first
236
+ similarity = 1.0 # Default for exact match
237
+ if old_code in original_content:
238
+ match_start = original_content.find(old_code)
239
+ match_end = match_start + len(old_code)
240
+ elif fuzzy_match:
241
+ # Try fuzzy matching
242
+ match_result = self._fuzzy_find(old_code, original_content)
243
+ if match_result is None:
244
+ return f"Error: Could not find code block (even with fuzzy matching).\n\nSearched for:\n{old_code[:200]}..."
245
+ match_start, match_end, similarity = match_result
246
+
247
+ # Show what was actually matched if similarity is not perfect
248
+ if similarity < 0.99:
249
+ matched_text = original_content[match_start:match_end]
250
+ info = f"\n[Fuzzy match found with {similarity:.1%} similarity]\nMatched text:\n{matched_text[:200]}...\n"
251
+ else:
252
+ info = ""
253
+ else:
254
+ return f"Error: Exact match not found and fuzzy_match is disabled.\n\nSearched for:\n{old_code[:200]}..."
255
+
256
+ # Create new content with replacement
257
+ new_content = original_content[:match_start] + new_code + original_content[match_end:]
258
+
259
+ # Generate diff for preview
260
+ diff = self._generate_diff(original_content, new_content, str(path), context_lines=3)
261
+
262
+ # Show diff if requested
263
+ output_parts = []
264
+ if show_diff or dry_run:
265
+ if similarity < 0.99 and fuzzy_match:
266
+ output_parts.append(info)
267
+ output_parts.append(f"Diff preview:\n{diff}\n")
268
+
269
+ # Dry run - don't actually modify
270
+ if dry_run:
271
+ output_parts.append("[DRY RUN] No changes made to file.")
272
+ return "\n".join(output_parts)
273
+
274
+ # Create backup if requested
275
+ backup_path = None
276
+ if create_backup:
277
+ backup_path = await self._create_backup(path)
278
+ output_parts.append(f"Created backup: {backup_path}")
279
+
280
+ # Apply changes
281
+ try:
282
+ async with aiofiles.open(path, "w", encoding="utf-8") as f:
283
+ await f.write(new_content)
284
+ output_parts.append(f"✓ Successfully edited {path}")
285
+ return "\n".join(output_parts)
286
+ except Exception as e:
287
+ # Rollback if writing failed
288
+ if create_backup and backup_path and await aiofiles.os.path.exists(str(backup_path)):
289
+ await self._copy_file(backup_path, path)
290
+ output_parts.append(f"✗ Edit failed, restored from backup: {e}")
291
+ else:
292
+ output_parts.append(f"✗ Edit failed: {e}")
293
+ return "\n".join(output_parts)
294
+
295
+ async def _smart_insert(
296
+ self,
297
+ path: Path,
298
+ original_content: str,
299
+ anchor: str,
300
+ code: str,
301
+ position: str,
302
+ dry_run: bool,
303
+ create_backup: bool,
304
+ show_diff: bool,
305
+ ) -> str:
306
+ """Insert code relative to an anchor line."""
307
+ if not anchor:
308
+ return "Error: anchor parameter is required for smart_insert mode"
309
+ if not code:
310
+ return "Error: code parameter is required for smart_insert mode"
311
+
312
+ lines = original_content.splitlines(keepends=True)
313
+
314
+ # Find anchor line
315
+ anchor_idx = None
316
+ for i, line in enumerate(lines):
317
+ if anchor in line:
318
+ anchor_idx = i
319
+ break
320
+
321
+ if anchor_idx is None:
322
+ return f"Error: Anchor line not found: {anchor}"
323
+
324
+ # Ensure code ends with newline
325
+ if not code.endswith("\n"):
326
+ code += "\n"
327
+
328
+ # Insert at appropriate position
329
+ if position == "before":
330
+ lines.insert(anchor_idx, code)
331
+ else: # after
332
+ lines.insert(anchor_idx + 1, code)
333
+
334
+ new_content = "".join(lines)
335
+
336
+ # Generate and show diff
337
+ output_parts = []
338
+ if show_diff or dry_run:
339
+ diff = self._generate_diff(original_content, new_content, str(path))
340
+ output_parts.append(f"Diff preview:\n{diff}\n")
341
+
342
+ if dry_run:
343
+ output_parts.append("[DRY RUN] No changes made to file.")
344
+ return "\n".join(output_parts)
345
+
346
+ # Create backup and apply
347
+ backup_path = None
348
+ if create_backup:
349
+ backup_path = await self._create_backup(path)
350
+ output_parts.append(f"Created backup: {backup_path}")
351
+
352
+ async with aiofiles.open(path, "w", encoding="utf-8") as f:
353
+ await f.write(new_content)
354
+ output_parts.append(f"✓ Successfully inserted code {position} anchor in {path}")
355
+ return "\n".join(output_parts)
356
+
357
+ async def _block_edit(
358
+ self,
359
+ path: Path,
360
+ original_content: str,
361
+ start_line: int,
362
+ end_line: int,
363
+ new_content_block: str,
364
+ dry_run: bool,
365
+ create_backup: bool,
366
+ show_diff: bool,
367
+ ) -> str:
368
+ """Edit a block of lines."""
369
+ if start_line <= 0 or end_line <= 0:
370
+ return "Error: line numbers must be positive (1-indexed)"
371
+ if start_line > end_line:
372
+ return "Error: start_line must be <= end_line"
373
+
374
+ lines = original_content.splitlines(keepends=True)
375
+
376
+ if start_line > len(lines) or end_line > len(lines):
377
+ return f"Error: line range {start_line}-{end_line} exceeds file length {len(lines)}"
378
+
379
+ # Ensure new content ends with newline
380
+ if not new_content_block.endswith("\n"):
381
+ new_content_block += "\n"
382
+
383
+ # Replace the block
384
+ new_lines = lines[: start_line - 1] + [new_content_block] + lines[end_line:]
385
+ new_content = "".join(new_lines)
386
+
387
+ # Generate and show diff
388
+ output_parts = []
389
+ if show_diff or dry_run:
390
+ diff = self._generate_diff(original_content, new_content, str(path))
391
+ output_parts.append(f"Diff preview:\n{diff}\n")
392
+
393
+ if dry_run:
394
+ output_parts.append("[DRY RUN] No changes made to file.")
395
+ return "\n".join(output_parts)
396
+
397
+ # Create backup and apply
398
+ backup_path = None
399
+ if create_backup:
400
+ backup_path = await self._create_backup(path)
401
+ output_parts.append(f"Created backup: {backup_path}")
402
+
403
+ async with aiofiles.open(path, "w", encoding="utf-8") as f:
404
+ await f.write(new_content)
405
+ output_parts.append(f"✓ Successfully edited lines {start_line}-{end_line} in {path}")
406
+ return "\n".join(output_parts)
407
+
408
+ def _fuzzy_find(self, target: str, text: str) -> Optional[Tuple[int, int, float]]:
409
+ """
410
+ Find target in text using fuzzy matching.
411
+
412
+ Returns: (start_pos, end_pos, similarity_ratio) or None if not found
413
+ """
414
+ # Normalize whitespace for matching
415
+ target_normalized = self._normalize_whitespace(target)
416
+
417
+ # Sliding window approach
418
+ target_lines = target.splitlines()
419
+ text_lines = text.splitlines()
420
+
421
+ best_match = None
422
+ best_ratio = 0
423
+
424
+ # Try different window sizes around target length
425
+ for window_size in range(len(target_lines), len(target_lines) + 5):
426
+ if window_size > len(text_lines):
427
+ break
428
+
429
+ for i in range(len(text_lines) - window_size + 1):
430
+ window = text_lines[i : i + window_size]
431
+ window_text = "\n".join(window)
432
+ window_normalized = self._normalize_whitespace(window_text)
433
+
434
+ # Calculate similarity
435
+ ratio = SequenceMatcher(None, target_normalized, window_normalized).ratio()
436
+
437
+ if ratio > best_ratio and ratio >= self.fuzzy_threshold:
438
+ # Found better match - calculate actual character positions
439
+ char_start = len("\n".join(text_lines[:i]))
440
+ if i > 0:
441
+ char_start += 1 # Account for newline
442
+ char_end = char_start + len(window_text)
443
+
444
+ best_match = (char_start, char_end, ratio)
445
+ best_ratio = ratio
446
+
447
+ return best_match
448
+
449
+ def _normalize_whitespace(self, text: str) -> str:
450
+ """Normalize whitespace for fuzzy matching."""
451
+ # Replace multiple spaces/tabs with single space
452
+ # Keep line structure but normalize indentation
453
+ lines = []
454
+ for line in text.splitlines():
455
+ # Strip leading/trailing whitespace but keep structure
456
+ normalized = " ".join(line.split())
457
+ lines.append(normalized)
458
+ return "\n".join(lines)
459
+
460
+ def _generate_diff(
461
+ self, old_content: str, new_content: str, filename: str, context_lines: int = 3
462
+ ) -> str:
463
+ """Generate unified diff between old and new content."""
464
+ old_lines = old_content.splitlines(keepends=True)
465
+ new_lines = new_content.splitlines(keepends=True)
466
+
467
+ diff_lines = unified_diff(
468
+ old_lines,
469
+ new_lines,
470
+ fromfile=f"{filename} (original)",
471
+ tofile=f"{filename} (modified)",
472
+ lineterm="",
473
+ n=context_lines,
474
+ )
475
+
476
+ return "".join(diff_lines)
477
+
478
+ async def _create_backup(self, path: Path) -> Path:
479
+ """Create a backup file with .bak extension."""
480
+ backup_path = path.with_suffix(path.suffix + ".bak")
481
+ await self._copy_file(path, backup_path)
482
+ return backup_path
483
+
484
+ async def _copy_file(self, source: Path, destination: Path) -> None:
485
+ """Copy a file using async IO."""
486
+ async with aiofiles.open(source, "rb") as src, aiofiles.open(destination, "wb") as dst:
487
+ while True:
488
+ chunk = await src.read(1024 * 1024)
489
+ if not chunk:
490
+ break
491
+ await dst.write(chunk)
tools/todo.py ADDED
@@ -0,0 +1,130 @@
1
+ """Todo list tool for agents to manage complex multi-step tasks."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ from agent.todo import TodoList
6
+ from tools.base import BaseTool
7
+
8
+
9
+ class TodoTool(BaseTool):
10
+ """Tool for managing todo lists during task execution."""
11
+
12
+ def __init__(self, todo_list: TodoList):
13
+ """Initialize with a TodoList instance.
14
+
15
+ Args:
16
+ todo_list: The TodoList instance to manage
17
+ """
18
+ self._todo_list = todo_list
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return "manage_todo_list"
23
+
24
+ @property
25
+ def description(self) -> str:
26
+ return """Manage your task list for complex multi-step work.
27
+
28
+ WHEN TO USE:
29
+ - Tasks with 3+ distinct steps
30
+ - Multi-file operations
31
+ - Complex workflows requiring planning
32
+ - Anytime you need to track progress
33
+
34
+ OPERATIONS:
35
+ - add: Create new tasks (requires content and activeForm)
36
+ - update: Change task status to pending, in_progress, or completed (requires index and status)
37
+ - list: View all current tasks
38
+ - remove: Delete a task (requires index)
39
+ - clear_completed: Remove all completed tasks
40
+
41
+ CRITICAL RULES:
42
+ - Exactly ONE task must be in_progress at any time
43
+ - Mark tasks completed IMMEDIATELY after finishing
44
+ - Use activeForm for present continuous (e.g., "Reading file" not "Read file")
45
+
46
+ EXAMPLES:
47
+ - add: {"content": "Read data.csv", "activeForm": "Reading data.csv"}
48
+ - update: {"index": 1, "status": "in_progress"}
49
+ - update: {"index": 1, "status": "completed"}
50
+ - list: {} (no parameters)"""
51
+
52
+ @property
53
+ def parameters(self) -> Dict[str, Any]:
54
+ return {
55
+ "operation": {
56
+ "type": "string",
57
+ "description": "Operation to perform: add, update, list, remove, or clear_completed",
58
+ },
59
+ "content": {
60
+ "type": "string",
61
+ "description": "Todo content in imperative form (for add operation)",
62
+ },
63
+ "activeForm": {
64
+ "type": "string",
65
+ "description": "Todo content in present continuous form (for add operation)",
66
+ },
67
+ "index": {
68
+ "type": "integer",
69
+ "description": "1-indexed position of todo item (for update/remove operations)",
70
+ },
71
+ "status": {
72
+ "type": "string",
73
+ "description": "New status: pending, in_progress, or completed (for update operation)",
74
+ },
75
+ }
76
+
77
+ async def execute(
78
+ self,
79
+ operation: str,
80
+ content: str = "",
81
+ activeForm: str = "",
82
+ index: int = 0,
83
+ status: str = "",
84
+ **kwargs,
85
+ ) -> str:
86
+ """Execute todo list operation.
87
+
88
+ Args:
89
+ operation: The operation to perform
90
+ content: Todo content (for add)
91
+ activeForm: Active form of content (for add)
92
+ index: Item index (for update/remove)
93
+ status: New status (for update)
94
+
95
+ Returns:
96
+ Result message
97
+ """
98
+ try:
99
+ # Convert index to int if it's a float (LLM may pass 1.0 instead of 1)
100
+ if isinstance(index, float):
101
+ index = int(index)
102
+
103
+ if operation == "add":
104
+ if not content or not activeForm:
105
+ return "Error: Both 'content' and 'activeForm' are required for add operation"
106
+ return self._todo_list.add(content, activeForm)
107
+
108
+ elif operation == "update":
109
+ if index <= 0:
110
+ return "Error: 'index' must be provided and positive for update operation"
111
+ if not status:
112
+ return "Error: 'status' must be provided for update operation"
113
+ return self._todo_list.update_status(index, status)
114
+
115
+ elif operation == "list":
116
+ return self._todo_list.format_list()
117
+
118
+ elif operation == "remove":
119
+ if index <= 0:
120
+ return "Error: 'index' must be provided and positive for remove operation"
121
+ return self._todo_list.remove(index)
122
+
123
+ elif operation == "clear_completed":
124
+ return self._todo_list.clear_completed()
125
+
126
+ else:
127
+ return f"Error: Unknown operation '{operation}'. Supported: add, update, list, remove, clear_completed"
128
+
129
+ except Exception as e:
130
+ return f"Error executing todo operation: {str(e)}"