raggiecode 0.2.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 (93) hide show
  1. Agent/__init__.py +0 -0
  2. Agent/agent.py +891 -0
  3. Agent/chat_history_db.py +1500 -0
  4. Agent/command.py +49 -0
  5. Agent/config.py +46 -0
  6. Agent/effort_levels.py +33 -0
  7. Agent/git_manager.py +727 -0
  8. Agent/tools.py +35 -0
  9. Commands/__init__.py +18 -0
  10. Commands/effort.py +42 -0
  11. Commands/global_todo.py +23 -0
  12. Commands/help.py +22 -0
  13. Commands/reasoning.py +24 -0
  14. Commands/redo.py +11 -0
  15. Commands/reindex.py +27 -0
  16. Commands/shell.py +28 -0
  17. Commands/stream.py +24 -0
  18. Commands/undo.py +13 -0
  19. Commands/unlimited_effort.py +8 -0
  20. Commands/window_size.py +29 -0
  21. RAG/__init__.py +0 -0
  22. RAG/document.py +119 -0
  23. RAG/find.py +408 -0
  24. RAG/graph.py +231 -0
  25. Tools/GetFileCodeStructure.py +43 -0
  26. Tools/GetSymbolSourceCode.py +27 -0
  27. Tools/__init__.py +39 -0
  28. Tools/ask_user.py +102 -0
  29. Tools/dispatch_subagent.py +215 -0
  30. Tools/document.py +35 -0
  31. Tools/edit_symbol.py +250 -0
  32. Tools/fuzzy_search.py +119 -0
  33. Tools/list_dir.py +51 -0
  34. Tools/read.py +49 -0
  35. Tools/read_image.py +75 -0
  36. Tools/remove.py +75 -0
  37. Tools/replace.py +305 -0
  38. Tools/search.py +41 -0
  39. Tools/shell.py +149 -0
  40. Tools/shell_kill.py +87 -0
  41. Tools/temp_background_service.py +113 -0
  42. Tools/todo_list.py +481 -0
  43. Tools/utils.py +116 -0
  44. Tools/view_changes.py +179 -0
  45. Tools/walk_call_tree.py +30 -0
  46. Tools/web_fetch.py +175 -0
  47. Tools/web_search.py +69 -0
  48. Tools/write.py +48 -0
  49. cli.py +111 -0
  50. config/__init__.py +0 -0
  51. config/coder_system_prompt.md +119 -0
  52. config/roles.json +43 -0
  53. config/tools.json +709 -0
  54. indexing/__init__.py +0 -0
  55. indexing/cli.py +128 -0
  56. indexing/code_index_sdk.py +832 -0
  57. indexing/code_indexer.py +1763 -0
  58. indexing/db_schema.py +396 -0
  59. indexing/export_to_json.py +346 -0
  60. indexing/extractors.py +189 -0
  61. indexing/file_utils.py +97 -0
  62. indexing/frontend/__init__.py +0 -0
  63. indexing/frontend/css_extractor.py +195 -0
  64. indexing/frontend/css_parser.py +387 -0
  65. indexing/frontend/css_selector_utils.py +226 -0
  66. indexing/frontend/edit_safety.py +573 -0
  67. indexing/frontend/graph.py +838 -0
  68. indexing/frontend/html_extractor.py +496 -0
  69. indexing/frontend/html_parser.py +314 -0
  70. indexing/frontend/jsx_extractor.py +1204 -0
  71. indexing/frontend/location_lookup.py +247 -0
  72. indexing/frontend/resolver.py +485 -0
  73. indexing/frontend/runtime_resolver.py +862 -0
  74. indexing/frontend/semantic_output.py +705 -0
  75. indexing/frontend/source_location.py +69 -0
  76. indexing/frontend_config.py +72 -0
  77. indexing/frontend_models.py +347 -0
  78. indexing/language_config.py +360 -0
  79. indexing/models.py +284 -0
  80. indexing/node_utils.py +1112 -0
  81. indexing/parse_worker.py +1082 -0
  82. indexing/queries.py +1542 -0
  83. indexing/sdk_examples.py +426 -0
  84. interactive.py +248 -0
  85. raggie.py +673 -0
  86. raggiecode-0.2.1.dist-info/METADATA +944 -0
  87. raggiecode-0.2.1.dist-info/RECORD +93 -0
  88. raggiecode-0.2.1.dist-info/WHEEL +5 -0
  89. raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
  90. raggiecode-0.2.1.dist-info/top_level.txt +10 -0
  91. skills/__init__.py +3 -0
  92. skills/manager.py +114 -0
  93. skills/tool.py +121 -0
Tools/todo_list.py ADDED
@@ -0,0 +1,481 @@
1
+ from .utils import BLUE, RESET, GREEN, YELLOW, RED, GRAY
2
+
3
+
4
+ def handle_create_todo_list(arguments, toolcall_id, parent_session_id=None):
5
+ """Create a new todo list for the current session."""
6
+ from Agent.chat_history_db import create_todo_list, get_session_effort, get_session_depth, resolve_todo_session_id
7
+ from Agent.effort_levels import is_depth_allowed, effort_name, effort_max_depth
8
+
9
+ if parent_session_id is None:
10
+ return {
11
+ "role": "tool",
12
+ "tool_call_id": toolcall_id,
13
+ "content": "Error: parent_session_id is required to create a todo list",
14
+ }
15
+
16
+ # When globalTodo is enabled, resolve to the root session so all subagents share one todo list
17
+ todo_session_id = resolve_todo_session_id(parent_session_id)
18
+
19
+ effort = get_session_effort(parent_session_id)
20
+ depth = get_session_depth(parent_session_id)
21
+ if effort is not None and not is_depth_allowed(effort, depth):
22
+ max_d = effort_max_depth(effort)
23
+ return {
24
+ "role": "tool",
25
+ "tool_call_id": toolcall_id,
26
+ "content": f"Cannot create todo list: effort level '{effort_name(effort)}' limits todo list depth to {max_d}. Current session depth is {depth}. Handle the task directly without a todo list.",
27
+ }
28
+
29
+ try:
30
+ todo_list_id = create_todo_list(todo_session_id)
31
+ print(f"{BLUE}Created todo list {todo_list_id} for session {todo_session_id}{RESET}")
32
+ return {
33
+ "role": "tool",
34
+ "tool_call_id": toolcall_id,
35
+ "content": f"Created todo list with ID: {todo_list_id}",
36
+ }
37
+ except Exception as e:
38
+ return {
39
+ "role": "tool",
40
+ "tool_call_id": toolcall_id,
41
+ "content": f"Failed to create todo list: {str(e)}",
42
+ }
43
+
44
+
45
+ def handle_add_task(arguments, toolcall_id, parent_session_id=None):
46
+ """Add a task to a todo list."""
47
+ from Agent.chat_history_db import add_todo_task
48
+
49
+ todo_list_id = arguments.get("todo_list_id")
50
+ goal = arguments.get("goal")
51
+ requirements = arguments.get("requirements")
52
+ notes = arguments.get("notes")
53
+ context = arguments.get("context")
54
+ insert_after = arguments.get("insert_after")
55
+
56
+ if not todo_list_id or not goal:
57
+ return {
58
+ "role": "tool",
59
+ "tool_call_id": toolcall_id,
60
+ "content": "Error: 'todo_list_id' and 'goal' are required parameters",
61
+ }
62
+
63
+ try:
64
+ task_id = add_todo_task(todo_list_id, goal, requirements, notes,
65
+ context=context, insert_after=insert_after)
66
+ print(f"{BLUE}Added task {task_id} to todo list {todo_list_id}: {goal[:50]}...{RESET}")
67
+ return {
68
+ "role": "tool",
69
+ "tool_call_id": toolcall_id,
70
+ "content": f"Added task with ID: {task_id}",
71
+ }
72
+ except Exception as e:
73
+ return {
74
+ "role": "tool",
75
+ "tool_call_id": toolcall_id,
76
+ "content": f"Failed to add task: {str(e)}",
77
+ }
78
+
79
+
80
+ def handle_get_todo_list(arguments, toolcall_id, parent_session_id=None):
81
+ """Get and display a todo list with all its tasks."""
82
+ from Agent.chat_history_db import get_todo_list, get_todo_tasks
83
+
84
+ todo_list_id = arguments.get("todo_list_id")
85
+
86
+ if not todo_list_id:
87
+ return {
88
+ "role": "tool",
89
+ "tool_call_id": toolcall_id,
90
+ "content": "Error: 'todo_list_id' is required",
91
+ }
92
+
93
+ try:
94
+ todo_list = get_todo_list(todo_list_id)
95
+ if not todo_list:
96
+ return {
97
+ "role": "tool",
98
+ "tool_call_id": toolcall_id,
99
+ "content": f"Todo list {todo_list_id} not found",
100
+ }
101
+
102
+ tasks = get_todo_tasks(todo_list_id)
103
+
104
+ output = f"\n{BLUE}Todo List {todo_list_id} (Status: {todo_list['status']}){RESET}\n"
105
+ output += "=" * 60 + "\n"
106
+
107
+ for task in tasks:
108
+ status_color = GREEN if task['status'] == 'completed' else YELLOW if task['status'] == 'in_progress' else RESET
109
+ output += f"{status_color}[{task['status']}] {YELLOW}{task['order_index'] + 1}.{RESET} {task['goal']}{RESET}\n"
110
+ if task['requirements']:
111
+ output += f" Requirements: {task['requirements']}\n"
112
+ if task['notes']:
113
+ output += f" Notes: {task['notes']}\n"
114
+ if task['context']:
115
+ output += f"{GRAY} Context: {task['context']}\n{RESET}"
116
+ if task.get('cancel_reason'):
117
+ output += f"{YELLOW} Cancel reason: {task['cancel_reason']}{RESET}\n"
118
+ output += "\n"
119
+
120
+ print(output)
121
+
122
+ return {
123
+ "role": "tool",
124
+ "tool_call_id": toolcall_id,
125
+ "content": f"Retrieved todo list with {len(tasks)} tasks",
126
+ }
127
+ except Exception as e:
128
+ return {
129
+ "role": "tool",
130
+ "tool_call_id": toolcall_id,
131
+ "content": f"Failed to get todo list: {str(e)}",
132
+ }
133
+
134
+
135
+ def handle_approve_todo_list(arguments, toolcall_id, parent_session_id=None):
136
+ """Approve a todo list and mark it as ready for execution."""
137
+ from Agent.chat_history_db import update_todo_list_status
138
+ from prompt_toolkit import prompt
139
+
140
+ todo_list_id = arguments.get("todo_list_id")
141
+
142
+ if not todo_list_id:
143
+ return {
144
+ "role": "tool",
145
+ "tool_call_id": toolcall_id,
146
+ "content": "Error: 'todo_list_id' is required",
147
+ }
148
+
149
+ try:
150
+ # Display the todo list first
151
+ from Agent.chat_history_db import get_todo_tasks
152
+ tasks = get_todo_tasks(todo_list_id)
153
+
154
+ print(f"\n{BLUE}Todo List {todo_list_id} - Execution Plan{RESET}")
155
+ print("=" * 60)
156
+ for task in tasks:
157
+ print(f"{YELLOW}{task['order_index'] + 1}.{RESET} {task['goal']}")
158
+ if task['requirements']:
159
+ print(f" Requirements: {task['requirements']}")
160
+ if task['notes']:
161
+ print(f" Notes: {task['notes']}")
162
+ if task['context']:
163
+ print(f"{GRAY} Context: {task['context']}{RESET}")
164
+ print("=" * 60)
165
+
166
+ # Ask for user approval
167
+ user_input = prompt("Do you want me to carry on with this plan? (y/n): ").strip().lower()
168
+
169
+ if user_input == 'y':
170
+ update_todo_list_status(todo_list_id, 'approved')
171
+ print(f"{GREEN}Todo list approved. Starting execution...{RESET}")
172
+ return {
173
+ "role": "tool",
174
+ "tool_call_id": toolcall_id,
175
+ "content": "Todo list approved and ready for execution",
176
+ }
177
+ else:
178
+ # Ask for feedback
179
+ feedback = prompt("Please elaborate on what should be changed: ").strip()
180
+ update_todo_list_status(todo_list_id, 'rejected')
181
+ print(f"{RED}Todo list rejected. Feedback: {feedback}{RESET}")
182
+ return {
183
+ "role": "tool",
184
+ "tool_call_id": toolcall_id,
185
+ "content": f"Todo list rejected. User feedback: {feedback}",
186
+ }
187
+ except Exception as e:
188
+ return {
189
+ "role": "tool",
190
+ "tool_call_id": toolcall_id,
191
+ "content": f"Failed to approve todo list: {str(e)}",
192
+ }
193
+
194
+
195
+ def handle_execute_next_task(arguments, toolcall_id, parent_session_id=None):
196
+ """Execute the next pending task in the todo list by dispatching a subagent.
197
+
198
+ If a task is already in_progress (from a crashed previous attempt),
199
+ it is resumed using the original toolcall_id so dispatch_subagent
200
+ can find and resume the existing child session.
201
+ """
202
+ from Agent.chat_history_db import get_next_pending_task, update_task_status, get_todo_list, get_todo_tasks, delete_todo_list, get_all_session_files_chain, set_task_toolcall_id, get_session_effort, get_session_depth
203
+ from Tools.dispatch_subagent import handle as dispatch_handle
204
+ from Agent.effort_levels import is_depth_allowed, effort_name, effort_max_depth
205
+
206
+ todo_list_id = arguments.get("todo_list_id")
207
+
208
+ if not todo_list_id:
209
+ return {
210
+ "role": "tool",
211
+ "tool_call_id": toolcall_id,
212
+ "content": "Error: 'todo_list_id' is required",
213
+ }
214
+
215
+ if parent_session_id is not None:
216
+ effort = get_session_effort(parent_session_id)
217
+ depth = get_session_depth(parent_session_id)
218
+ if effort is not None and not is_depth_allowed(effort, depth):
219
+ max_d = effort_max_depth(effort)
220
+ return {
221
+ "role": "tool",
222
+ "tool_call_id": toolcall_id,
223
+ "content": f"Cannot execute todo list tasks: effort level '{effort_name(effort)}' limits todo list depth to {max_d}. Current session depth is {depth}. Handle remaining tasks directly without dispatching subagents.",
224
+ }
225
+
226
+ try:
227
+ # Check if todo list is approved before executing any task
228
+ todo_list = get_todo_list(todo_list_id)
229
+ if not todo_list:
230
+ return {
231
+ "role": "tool",
232
+ "tool_call_id": toolcall_id,
233
+ "content": f"Error: Todo list {todo_list_id} not found",
234
+ }
235
+
236
+ if todo_list['status'] != 'approved':
237
+ return {
238
+ "role": "tool",
239
+ "tool_call_id": toolcall_id,
240
+ "content": f"Error: Cannot execute tasks. Todo list {todo_list_id} has not been approved by the user. Current status: {todo_list['status']}. Please call ApproveTodoList first.",
241
+ }
242
+
243
+ # Get the next task — in_progress tasks are returned first (crash recovery)
244
+ task = get_next_pending_task(todo_list_id)
245
+
246
+ if not task:
247
+ # No more pending tasks — delete the completed todo list
248
+ delete_todo_list(todo_list_id)
249
+ print(f"{GREEN}All tasks completed! Todo list {todo_list_id} is done and has been removed.{RESET}")
250
+ return {
251
+ "role": "tool",
252
+ "tool_call_id": toolcall_id,
253
+ "content": "All tasks completed. Todo list has been removed.",
254
+ }
255
+
256
+ is_resume = task['status'] == 'in_progress'
257
+
258
+ if is_resume:
259
+ # Crash recovery: resume the existing subagent session using the original toolcall_id
260
+ print(f"{BLUE}Resuming task {task['order_index'] + 1}: {task['goal']}{RESET}")
261
+ dispatch_toolcall_id = task.get('toolcall_id') or toolcall_id
262
+ else:
263
+ # New task: mark as in_progress and store the toolcall_id for future recovery
264
+ update_task_status(task['id'], 'in_progress')
265
+ set_task_toolcall_id(task['id'], toolcall_id)
266
+ print(f"{BLUE}Executing task {task['order_index'] + 1}: {task['goal']}{RESET}")
267
+ dispatch_toolcall_id = toolcall_id
268
+
269
+ # Get all tasks to build context of completed/cancelled tasks
270
+ all_tasks = get_todo_tasks(todo_list_id)
271
+ completed_tasks = [t for t in all_tasks if t['status'] == 'completed']
272
+ cancelled_tasks = [t for t in all_tasks if t['status'] == 'cancelled']
273
+
274
+ # Build prompt for subagent
275
+ prompt_parts = [f"Goal: {task['goal']}"]
276
+ if task['requirements']:
277
+ prompt_parts.append(f"Requirements: {task['requirements']}")
278
+ if task['notes']:
279
+ prompt_parts.append(f"Notes: {task['notes']}")
280
+ if task['context']:
281
+ prompt_parts.append(f"Context: {task['context']}")
282
+
283
+ # Add context from previously completed tasks
284
+ if completed_tasks:
285
+ prompt_parts.append("\nPreviously completed tasks in this todo list:")
286
+ for ct in completed_tasks:
287
+ prompt_parts.append(f" - [COMPLETED] {ct['order_index'] + 1}. {ct['goal']}")
288
+ if ct['requirements']:
289
+ prompt_parts.append(f" Requirements: {ct['requirements']}")
290
+
291
+ # Add context from cancelled tasks
292
+ if cancelled_tasks:
293
+ prompt_parts.append("\nCancelled tasks in this todo list:")
294
+ for ct in cancelled_tasks:
295
+ prompt_parts.append(f" - [CANCELLED] {ct['order_index'] + 1}. {ct['goal']}")
296
+ if ct['notes']:
297
+ prompt_parts.append(f" Notes: {ct['notes']}")
298
+
299
+ subagent_prompt = "\n".join(prompt_parts)
300
+
301
+ # Dispatch subagent (use original toolcall_id for resume so the child session is found)
302
+ dispatch_args = {
303
+ "prompt": subagent_prompt
304
+ }
305
+
306
+ result = dispatch_handle(dispatch_args, dispatch_toolcall_id, parent_session_id, skip_depth_check=True)
307
+
308
+ # Mark task as completed if subagent succeeded
309
+ if result.get('role') == 'tool' and not result.get('content', '').startswith('Error'):
310
+ update_task_status(task['id'], 'completed')
311
+ if is_resume:
312
+ print(f"{GREEN}Task {task['order_index'] + 1} resumed and completed{RESET}")
313
+ else:
314
+ print(f"{GREEN}Task {task['order_index'] + 1} completed{RESET}")
315
+ else:
316
+ update_task_status(task['id'], 'failed')
317
+ print(f"{RED}Task {task['order_index'] + 1} failed{RESET}")
318
+
319
+ # Append list of files modified during the subagent session
320
+ subagent_session_id = result.pop('subagent_session_id', None)
321
+ if subagent_session_id is not None:
322
+ session_files = get_all_session_files_chain(subagent_session_id)
323
+ if session_files:
324
+ files_summary = "\n\nFiles modified in this task session:"
325
+ for sf in session_files:
326
+ files_summary += f"\n - [{sf['operation']}] {sf['file_path']}"
327
+ result['content'] = result['content'] + files_summary
328
+
329
+ return result
330
+
331
+ except Exception as e:
332
+ return {
333
+ "role": "tool",
334
+ "tool_call_id": toolcall_id,
335
+ "content": f"Failed to execute task: {str(e)}",
336
+ }
337
+
338
+
339
+ def handle_mark_task_complete(arguments, toolcall_id, parent_session_id=None):
340
+ """Manually mark a task as completed."""
341
+ from Agent.chat_history_db import update_task_status, get_todo_tasks, delete_todo_list
342
+
343
+ task_id = arguments.get("task_id")
344
+ todo_list_id = arguments.get("todo_list_id")
345
+
346
+ if not task_id:
347
+ return {
348
+ "role": "tool",
349
+ "tool_call_id": toolcall_id,
350
+ "content": "Error: 'task_id' is required",
351
+ }
352
+
353
+ try:
354
+ update_task_status(task_id, 'completed')
355
+ print(f"{GREEN}Task {task_id} marked as completed{RESET}")
356
+
357
+ # If we know the todo_list_id, check if all tasks are now done
358
+ if todo_list_id:
359
+ tasks = get_todo_tasks(todo_list_id)
360
+ all_done = all(t['status'] == 'completed' for t in tasks)
361
+ if all_done:
362
+ delete_todo_list(todo_list_id)
363
+ print(f"{GREEN}All tasks completed! Todo list {todo_list_id} has been removed.{RESET}")
364
+ return {
365
+ "role": "tool",
366
+ "tool_call_id": toolcall_id,
367
+ "content": f"Task {task_id} marked as completed. All tasks done -- todo list has been removed.",
368
+ }
369
+
370
+ return {
371
+ "role": "tool",
372
+ "tool_call_id": toolcall_id,
373
+ "content": f"Task {task_id} marked as completed",
374
+ }
375
+ except Exception as e:
376
+ return {
377
+ "role": "tool",
378
+ "tool_call_id": toolcall_id,
379
+ "content": f"Failed to mark task as completed: {str(e)}",
380
+ }
381
+
382
+
383
+ def handle_mark_task_failed(arguments, toolcall_id, parent_session_id=None):
384
+ """Manually mark a task as failed."""
385
+ from Agent.chat_history_db import update_task_status
386
+
387
+ task_id = arguments.get("task_id")
388
+
389
+ if not task_id:
390
+ return {
391
+ "role": "tool",
392
+ "tool_call_id": toolcall_id,
393
+ "content": "Error: 'task_id' is required",
394
+ }
395
+
396
+ try:
397
+ update_task_status(task_id, 'failed')
398
+ print(f"{RED}Task {task_id} marked as failed{RESET}")
399
+ return {
400
+ "role": "tool",
401
+ "tool_call_id": toolcall_id,
402
+ "content": f"Task {task_id} marked as failed",
403
+ }
404
+ except Exception as e:
405
+ return {
406
+ "role": "tool",
407
+ "tool_call_id": toolcall_id,
408
+ "content": f"Failed to mark task as failed: {str(e)}",
409
+ }
410
+
411
+
412
+ def handle_mark_task_cancelled(arguments, toolcall_id, parent_session_id=None):
413
+ """Manually mark a task as cancelled. A reason is required."""
414
+ from Agent.chat_history_db import update_task_status
415
+
416
+ task_id = arguments.get("task_id")
417
+ reason = arguments.get("reason")
418
+
419
+ if not task_id:
420
+ return {
421
+ "role": "tool",
422
+ "tool_call_id": toolcall_id,
423
+ "content": "Error: 'task_id' is required",
424
+ }
425
+
426
+ if not reason or not reason.strip():
427
+ return {
428
+ "role": "tool",
429
+ "tool_call_id": toolcall_id,
430
+ "content": "Error: 'reason' is required when cancelling a task. Provide a brief explanation of why this task is being cancelled.",
431
+ }
432
+
433
+ try:
434
+ update_task_status(task_id, 'cancelled', cancel_reason=reason.strip())
435
+ print(f"{YELLOW}Task {task_id} marked as cancelled. Reason: {reason.strip()}{RESET}")
436
+ return {
437
+ "role": "tool",
438
+ "tool_call_id": toolcall_id,
439
+ "content": f"Task {task_id} marked as cancelled. Reason: {reason.strip()}",
440
+ }
441
+ except Exception as e:
442
+ return {
443
+ "role": "tool",
444
+ "tool_call_id": toolcall_id,
445
+ "content": f"Failed to mark task as cancelled: {str(e)}",
446
+ }
447
+
448
+
449
+ def handle_get_active_todo_list(arguments, toolcall_id, parent_session_id=None):
450
+ """Get the active todo list for the current session."""
451
+ from Agent.chat_history_db import get_active_todo_list, resolve_todo_session_id
452
+
453
+ if parent_session_id is None:
454
+ return {
455
+ "role": "tool",
456
+ "tool_call_id": toolcall_id,
457
+ "content": "Error: parent_session_id is required",
458
+ }
459
+
460
+ todo_session_id = resolve_todo_session_id(parent_session_id)
461
+
462
+ try:
463
+ todo_list = get_active_todo_list(todo_session_id)
464
+ if todo_list:
465
+ return {
466
+ "role": "tool",
467
+ "tool_call_id": toolcall_id,
468
+ "content": f"Active todo list ID: {todo_list['id']}, Status: {todo_list['status']}",
469
+ }
470
+ else:
471
+ return {
472
+ "role": "tool",
473
+ "tool_call_id": toolcall_id,
474
+ "content": "No active todo list found for this chat",
475
+ }
476
+ except Exception as e:
477
+ return {
478
+ "role": "tool",
479
+ "tool_call_id": toolcall_id,
480
+ "content": f"Failed to get active todo list: {str(e)}",
481
+ }
Tools/utils.py ADDED
@@ -0,0 +1,116 @@
1
+ import os
2
+ from pathlib import Path
3
+ from functools import lru_cache
4
+ from pathspec import PathSpec
5
+ from pathspec.patterns import GitWildMatchPattern
6
+
7
+
8
+
9
+ def remove_em_dashes(text: str) -> str:
10
+ return text.replace(" — ", ", ").replace("—", ", ")
11
+
12
+
13
+ BLUE = "\033[34m"
14
+ GREEN = "\033[32m"
15
+ YELLOW = "\033[33m"
16
+ RED = "\033[31m"
17
+ GRAY = "\033[90m"
18
+ RESET = "\033[0m"
19
+
20
+
21
+
22
+ def is_within_cwd(path: str) -> bool:
23
+ """Check if a path resolves to within the current working directory.
24
+
25
+ Uses realpath to resolve symlinks, preventing escape via symlink tricks.
26
+ """
27
+ cwd = os.path.realpath(os.getcwd())
28
+ resolved = os.path.realpath(os.path.abspath(path))
29
+ return resolved == cwd or resolved.startswith(cwd + os.sep)
30
+
31
+
32
+
33
+ @lru_cache(maxsize=1)
34
+ def _load_ignore_spec(cwd: str):
35
+ """Load ignore patterns from .aiignore, falling back to .gitignore.
36
+
37
+ Returns a PathSpec instance. Cached per working directory.
38
+ """
39
+ root = Path(cwd)
40
+ aiignore_path = root / '.aiignore'
41
+ gitignore_path = root / '.gitignore'
42
+
43
+ ignore_path = None
44
+ if aiignore_path.exists():
45
+ ignore_path = aiignore_path
46
+ elif gitignore_path.exists():
47
+ ignore_path = gitignore_path
48
+
49
+ if ignore_path is None:
50
+ return PathSpec.from_lines(GitWildMatchPattern, [])
51
+
52
+ with open(ignore_path, 'r', encoding='utf-8') as f:
53
+ patterns = f.read().splitlines()
54
+
55
+ return PathSpec.from_lines(GitWildMatchPattern, patterns)
56
+
57
+
58
+
59
+ def is_ignored(file_path: str) -> bool:
60
+ """Check if a file path is ignored by .aiignore (or .gitignore as fallback).
61
+
62
+ If a .aiignore file exists in the project root, its patterns are used.
63
+ Otherwise, .gitignore is used as a fallback.
64
+ Returns False if neither file exists.
65
+ """
66
+ cwd = os.getcwd()
67
+ spec = _load_ignore_spec(cwd)
68
+ rel = os.path.relpath(os.path.abspath(file_path), cwd)
69
+ return spec.match_file(rel)
70
+
71
+
72
+
73
+ def is_ignored_by_gitignore(file_path: str) -> bool:
74
+ """Backward-compatible alias for is_ignored."""
75
+ return is_ignored(file_path)
76
+
77
+
78
+
79
+ def reindex_after_change(code_indexer):
80
+ """Re-index the codebase after a file-modifying tool call.
81
+
82
+ Silently skips if code_indexer is None or re-indexing fails.
83
+ """
84
+ if code_indexer is None:
85
+ return
86
+ try:
87
+ code_indexer.index_directory()
88
+ except (KeyboardInterrupt, EOFError):
89
+ print(f"{YELLOW}Re-indexing interrupted. Using existing index.{RESET}")
90
+ except Exception as e:
91
+ print(f"{YELLOW}Warning: Failed to re-index after tool execution: {e}{RESET}")
92
+
93
+
94
+
95
+ def auto_record_change(session_id, file_path: str, change_type: str, description: str, details: str = None):
96
+ """Auto-record a change to the changes database.
97
+
98
+ Looks up the role from the session and records the change.
99
+ Silently fails if the session or database is unavailable.
100
+ """
101
+ if session_id is None:
102
+ return
103
+ try:
104
+ from Agent.chat_history_db import add_change, get_session_role
105
+ role = get_session_role(session_id) or "unknown"
106
+ add_change(
107
+ prompt_id=str(session_id),
108
+ role=role,
109
+ session_id=session_id,
110
+ change_type=change_type,
111
+ file_path=file_path,
112
+ description=description,
113
+ details=details,
114
+ )
115
+ except Exception as e:
116
+ print(f"{RED}Warning: Failed to record change: {e}{RESET}")