├── .format.py ├── .run.xml ├── README.md ├── aaider.py └── requirements.txt /.format.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import pyperclip 3 | import re 4 | 5 | # Read the file content 6 | with open('.run.xml', 'r') as file: 7 | content = file.read() 8 | 9 | # Remove XML comments () 10 | # The regex pattern looks for 11 | content_no_comments = re.sub(r'', '', content, flags=re.DOTALL) 12 | 13 | # Split the content on the tag 14 | parts = content_no_comments.split('') 15 | if len(parts) != 2: 16 | print("Error: Could not find tag") 17 | exit(1) 18 | 19 | before, rest = parts 20 | 21 | # Further split to isolate the command between and 22 | after_parts = rest.split('') 23 | if len(after_parts) != 2: 24 | print("Error: Could not find tag") 25 | exit(1) 26 | command_block, after = after_parts 27 | 28 | # Combine multiple lines in the command block into a single command line 29 | # This handles cases where the command is spread across multiple lines 30 | # Example: 31 | # 32 | # files-to-prompt --cxml 33 | # researcher.py 34 | # tutorial.py 35 | # text_source.py 36 | # 37 | # Will be converted to: 38 | # files-to-prompt --cxml researcher.py tutorial.py text_source.py 39 | 40 | # Split the command block into individual lines, strip whitespace, and join with spaces 41 | command_lines = command_block.strip().splitlines() 42 | # Remove any leading/trailing whitespace from each line and filter out empty lines 43 | command_cleaned = ' '.join(line.strip() for line in command_lines if line.strip()) 44 | 45 | # Now, command_cleaned contains the combined single command line 46 | command = command_cleaned 47 | 48 | # Run the command 49 | try: 50 | result = subprocess.check_output(command, shell=True, text=True) 51 | except subprocess.CalledProcessError as e: 52 | print(f"Error running command: {e}") 53 | exit(1) 54 | 55 | # Construct the new content by replacing the old command with the command's output 56 | new_content = f"{before}\n{result}{after}" 57 | 58 | # Copy the modified content to the clipboard 59 | pyperclip.copy(new_content) 60 | print("Modified content copied to clipboard.") 61 | -------------------------------------------------------------------------------- /.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Check tutorial.py and integrate Tavily search engine into researcher.py. Update all related parts of the code. 4 | 5 | 6 | 7 | 8 | 9 | 10 | Provide code for each new or updated file in a single block. Only include the updated parts 11 | of the code in your response. 12 | 13 | 14 | Maintain the existing file names unless a change is necessary for better clarity or 15 | structure. Respect the currently used libraries to avoid introducing unnecessary 16 | dependencies. 17 | 18 | 19 | 20 | 21 | 22 | files-to-prompt --cxml 23 | researcher.py 24 | tutorial.py 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | path/to/file 33 | create | update | delete 34 | Description of changes and purpose. 35 | Updated parts of the code 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aaider.py - Asynchronized Automated Aider Task Executor 2 | 3 | *Note*: Feel free to open an issue if you have any questions. This isn't user-friendly due to my little time. 4 | 5 | `aaider.py` is a script designed to automate the execution of multiple tasks using the Aider AI assistant. 6 | It receives an output file of a bigger model using a specific format (`example.xml`), parses it into Tasks and executes them asynchronously using Aider. 7 | 8 | ## Example Usage 9 | 10 | 0. Install requirements 11 | 1. Configure DeepSeek API KEY, OpenRouter (if you want to use 'o1') like me 12 | 2. Prepare your prompt: 13 | - Modify instructions in `example.xml` 14 | - Run `files-to-prompt --cxml YOUR_FILES_&_FOLDER | cop` 15 | - Paste in `` 16 | 3. `llm -m o1-mini < example.xml > out.xml` 17 | 4. `cd` to your current repo: 18 | - Make sure you commit your code for backup 19 | - `python PATH_TO_AAIDER.py --input out.xml` 20 | 21 | ## Notes 22 | - Be very specific in your `` 23 | - You can add more `` if you want 24 | - For me, o1 works very well with this setup. 25 | - Instead of `OpenRouter API`, you can copy and paste the full prompt with code to `o1` and copy back and apply to the code base with `aaider.py`. 26 | 27 | ## Options 28 | 29 | - `--input FILE`: Output of bigger LLM 30 | - `--deepseek`: Use deepseek/deepseek-coder model (default) 31 | - `--model MODEL`: Specify the model to use for the main chat 32 | - `--opus`: Use claude-3-opus-20240229 model 33 | - `--sonnet`: Use claude-3-5-sonnet-20240620 model 34 | - `--4`: Use gpt-4-0613 model 35 | - `--4o`: Use gpt-4o-2024-08-06 model 36 | - `--mini`: Use gpt-4o-mini model 37 | - `--4-turbo`: Use gpt-4-1106-preview model 38 | - `--auto-commits`: Enable automatic Git commits 39 | - `--no-auto-commits`: Disable automatic Git commits (default) 40 | - `--skip N`: Number of tasks to skip 41 | - `--only N [N ...]`: Only execute specified task numbers 42 | - `--use-json`: Use JSON parsing instead of XML 43 | 44 | ### Aider Arguments 45 | 46 | Any additional arguments after the script options will be passed directly to the Aider command. 47 | 48 | ## Input Format 49 | 50 | The script accepts input in either XML or JSON format. Here's an example of the XML format: 51 | 52 | ```xml 53 | 54 | 55 | /path/to/file.py 56 | update 57 | Add a new function to calculate factorial 58 | 59 | def factorial(n): 60 | if n == 0: 61 | return 1 62 | else: 63 | return n * factorial(n-1) 64 | 65 | 66 | 67 | 68 | ``` 69 | 70 | For JSON format, use the `--use-json` flag and provide input in this structure: 71 | 72 | ```json 73 | { 74 | "filesContent": [ 75 | { 76 | "file": "/path/to/file.py", 77 | "action": "update", 78 | "description": "Add a new function to calculate factorial", 79 | "code": "def factorial(n):\n if n == 0:\n return 1\n else:\n return n * factorial(n-1)" 80 | } 81 | ] 82 | } 83 | ``` 84 | 85 | ## Examples 86 | 87 | 1. Execute tasks from an XML file using the Claude 3 Opus model: 88 | ```bash 89 | python aaider.py --opus --input tasks.xml 90 | ``` 91 | 92 | 2. Execute tasks from a JSON file, skipping the first 2 tasks: 93 | ```bash 94 | python aaider.py --use-json --input tasks.json --skip 2 95 | ``` 96 | 97 | 3. Execute only tasks 3 and 5 with auto-commits enabled: 98 | ```bash 99 | python aaider.py --auto-commits --only 3 5 --input tasks.xml 100 | ``` 101 | 102 | 4. Pass additional arguments to Aider: 103 | ```bash 104 | python aaider.py --input tasks.xml --model gpt-4 -- --openai-api-key YOUR_API_KEY 105 | ``` 106 | 107 | ## Output 108 | 109 | The script will create a `log` directory containing: 110 | - A log file for each executed task (`task_N.log`) 111 | - A `tasks.json` file with all tasks and their assigned IDs 112 | 113 | After execution, a summary of task results will be displayed in the console. 114 | 115 | ## Contributing 116 | 117 | Contributions to improve `aaider.py` are welcome. Please submit pull requests or open issues on the project's repository. 118 | -------------------------------------------------------------------------------- /aaider.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | import argparse 4 | import shlex 5 | import sys 6 | import json 7 | import os 8 | import xml.etree.ElementTree as ET 9 | from typing import List, Dict, Any 10 | import asyncio 11 | import shutil # Added for cleaning log directory 12 | 13 | def parse_fault_tolerant_xml(xml_string: str) -> List[Dict[str, Any]]: 14 | # Normalize line endings 15 | xml_string = xml_string.replace('\r\n', '\n').replace('\r', '\n') 16 | 17 | # Remove any XML declaration to avoid parsing issues 18 | xml_string = re.sub(r'<\?xml.*?\?>', '', xml_string) 19 | 20 | # Wrap content in a root element if not present 21 | if not xml_string.strip().startswith(''): 22 | xml_string = f"{xml_string}" 23 | 24 | # Replace problematic characters 25 | xml_string = re.sub(r'&(?!amp;|lt;|gt;|apos;|quot;)', '&', xml_string) 26 | 27 | result = [] 28 | 29 | try: 30 | # Parse the entire XML structure 31 | root = ET.fromstring(xml_string) 32 | 33 | # Find all file elements 34 | file_elements = root.findall('.//file') 35 | 36 | for file_elem in file_elements: 37 | file_info = {} 38 | for child in file_elem: 39 | if child.tag == 'code': 40 | file_info[child.tag] = child.text.strip() if child.text else "" 41 | else: 42 | file_info[child.tag] = child.text.strip() if child.text else "" 43 | result.append(file_info) 44 | except ET.ParseError as e: 45 | print(f"Warning: Error parsing XML, attempting manual extraction: {str(e)}", file=sys.stderr) 46 | # If parsing fails, try to extract information manually 47 | file_blocks = re.findall(r'.*?', xml_string, re.DOTALL) 48 | for block in file_blocks: 49 | file_info = {} 50 | for tag in ['path', 'action', 'description', 'code']: 51 | match = re.search(f'<{tag}>(.*?)', block, re.DOTALL) 52 | if match: 53 | file_info[tag] = match.group(1).strip() 54 | if file_info: 55 | result.append(file_info) 56 | else: 57 | print(f"Warning: Unable to extract information from block: {block[:100]}...", file=sys.stderr) 58 | 59 | return result 60 | 61 | def extract_tasks(text, use_json=False): 62 | if use_json: 63 | # Try to parse as JSON 64 | try: 65 | data = json.loads(text) 66 | if isinstance(data, dict) and "filesContent" in data: 67 | return [ 68 | { 69 | "file": file_info['file'], 70 | "action": file_info['action'], 71 | "description": file_info['description'], 72 | "code": file_info.get('code', '') 73 | } 74 | for file_info in data["filesContent"] 75 | ] 76 | except json.JSONDecodeError: 77 | print("Warning: Failed to parse JSON, falling back to XML parsing", file=sys.stderr) 78 | 79 | # Default to XML parsing 80 | return parse_fault_tolerant_xml(text) 81 | 82 | def trim_command(command, max_length=400): 83 | if len(command) <= max_length: 84 | return command 85 | head = command[:max_length//2 - 3] 86 | tail = command[-max_length//2 + 3:] 87 | return f"{head}...{tail}" 88 | 89 | def parse_arguments(): 90 | parser = argparse.ArgumentParser(description="Execute tasks using aider with custom arguments.") 91 | 92 | # Model selection group 93 | model_group = parser.add_mutually_exclusive_group() 94 | model_group.add_argument('--model', help="Specify the model to use for the main chat [env var: AIDER_MODEL]") 95 | model_group.add_argument('--opus', action='store_true', help="Use claude-3-opus-20240229 model for the main chat [env var: AIDER_OPUS]") 96 | model_group.add_argument('--sonnet', action='store_true', help="Use claude-3-5-sonnet-20240620 model for the main chat [env var: AIDER_SONNET]") 97 | model_group.add_argument('--4', '-4', action='store_true', help="Use gpt-4-0613 model for the main chat [env var: AIDER_4]") 98 | model_group.add_argument('--4o', action='store_true', help="Use gpt-4o-2024-08-06 model for the main chat [env var: AIDER_4O]") 99 | model_group.add_argument('--mini', action='store_true', help="Use gpt-4o-mini model for the main chat [env var: AIDER_MINI]") 100 | model_group.add_argument('--4-turbo', action='store_true', help="Use gpt-4-1106-preview model for the main chat [env var: AIDER_4_TURBO]") 101 | model_group.add_argument('--deepseek', action='store_true', help="Use deepseek/deepseek-coder model for the main chat [env var: AIDER_DEEPSEEK]") 102 | 103 | # Auto-commit flags 104 | auto_commit_group = parser.add_mutually_exclusive_group() 105 | auto_commit_group.add_argument('--auto-commits', dest='auto_commits', action='store_true', help='Enable automatic Git commits.') 106 | auto_commit_group.add_argument('--no-auto-commits', dest='auto_commits', action='store_false', help='Disable automatic Git commits (default).') 107 | parser.set_defaults(auto_commits=False) # Default to --no-auto-commits 108 | 109 | parser.add_argument('--input', help="Path to the input file (XML or JSON)") 110 | parser.add_argument('--skip', type=int, default=0, help="Number of tasks to skip") 111 | parser.add_argument('--only', type=int, nargs='+', help="Only execute specified task numbers") 112 | parser.add_argument('--use-json', action='store_true', help="Use JSON parsing instead of XML") 113 | parser.add_argument('aider_args', nargs=argparse.REMAINDER, help="Arguments to pass to aider command") 114 | return parser.parse_args() 115 | 116 | def get_input(input_file=None): 117 | if input_file: 118 | try: 119 | with open(input_file, 'r') as file: 120 | return file.read().strip() 121 | except IOError as e: 122 | print(f"Error reading input file: {e}", file=sys.stderr) 123 | sys.exit(1) 124 | else: 125 | print("Please paste your text below. When finished, press Ctrl+D (Unix) or Ctrl+Z (Windows) followed by Enter:") 126 | return sys.stdin.read().strip() 127 | 128 | def delete_file(file_path, log_fh, auto_commits): 129 | try: 130 | os.remove(file_path) 131 | message = f"File deleted successfully: {file_path}" 132 | print(message) 133 | log_fh.write(message + "\n") 134 | 135 | if auto_commits: 136 | # Git operations 137 | try: 138 | # Remove the file from Git 139 | subprocess.run(['git', 'rm', file_path], check=True) 140 | message = f"File removed from Git: {file_path}" 141 | print(message) 142 | log_fh.write(message + "\n") 143 | 144 | # Commit the change 145 | commit_message = f"Remove file: {file_path}" 146 | subprocess.run(['git', 'commit', '-m', commit_message], check=True) 147 | message = f"Changes committed: {commit_message}" 148 | print(message) 149 | log_fh.write(message + "\n") 150 | except subprocess.CalledProcessError as e: 151 | error_message = f"Error performing Git operations: {e}" 152 | print(error_message) 153 | log_fh.write(error_message + "\n") 154 | else: 155 | message = "Auto commits are disabled. Skipping Git operations." 156 | print(message) 157 | log_fh.write(message + "\n") 158 | except FileNotFoundError: 159 | message = f"File not found: {file_path}" 160 | print(message) 161 | log_fh.write(message + "\n") 162 | except PermissionError: 163 | message = f"Permission denied: Unable to delete {file_path}" 164 | print(message) 165 | log_fh.write(message + "\n") 166 | except Exception as e: 167 | message = f"An error occurred while deleting {file_path}: {e}" 168 | print(message) 169 | log_fh.write(message + "\n") 170 | 171 | def get_model_flag(args): 172 | if args.model: 173 | return f'--model {args.model}' 174 | elif args.opus: 175 | return '--opus' 176 | elif args.sonnet: 177 | return '--sonnet' 178 | elif getattr(args, '4'): 179 | return '--4' 180 | elif args.__dict__['4o']: 181 | return '--4o' 182 | elif args.mini: 183 | return '--mini' 184 | elif args.__dict__['4_turbo']: 185 | return '--4-turbo' 186 | elif args.deepseek: 187 | return '--deepseek' 188 | else: 189 | return '--deepseek' # Default to deepseek if no model is specified 190 | 191 | def format_task(task): 192 | formatted_task = f"Update file: {task['path']}\n\n" 193 | formatted_task += f"\"action\": \"{task['action']}\"\n\n" 194 | formatted_task += f"Description: {task['description']}\n\n" 195 | if task['action'].lower() != 'delete': 196 | if 'code' in task and task['code'].strip(): 197 | formatted_task += f"New content:\n\n{task['code']}" 198 | else: 199 | formatted_task += "No code provided.\n" 200 | return formatted_task 201 | 202 | async def run_task(i, task, model_flag, aider_args, args, total_tasks, results, timeout_event): 203 | if timeout_event.is_set(): 204 | return # Skip execution if timeout has occurred 205 | 206 | print("#" * 20) 207 | print("#" * 20) 208 | 209 | # Calculate the original task number 210 | original_task_number = args.only[i] if args.only else i + args.skip + 1 211 | 212 | print(f"Executing task {original_task_number}/{total_tasks}...") 213 | 214 | file_path = task['path'] 215 | action = task['action'].lower() 216 | 217 | print(f"[Action]: {action}") 218 | 219 | # Collect per-task result 220 | task_result = { 221 | 'status': False, # Will update to True if successful 222 | 'code_missing': False, 223 | 'unexpected_action': False, 224 | 'action': action, # Added action 225 | 'path': file_path, # Added path 226 | } 227 | 228 | expected_actions = ['create', 'update', 'delete'] 229 | if action not in expected_actions: 230 | print(f"Warning: Unexpected action '{action}' in task {original_task_number}") 231 | task_result['unexpected_action'] = True 232 | # Proceed with caution or handle accordingly 233 | 234 | if 'code' not in task or not task.get('code'): 235 | print(f"Notice: 'code' field is missing in task {original_task_number}") 236 | task_result['code_missing'] = True 237 | 238 | # Prepare the log file 239 | log_folder = 'log' 240 | os.makedirs(log_folder, exist_ok=True) 241 | log_file = os.path.join(log_folder, f'task_{original_task_number}.log') 242 | 243 | try: 244 | with open(log_file, 'w') as log_fh: 245 | if action == "delete": 246 | delete_file(file_path, log_fh, args.auto_commits) 247 | task_result['status'] = True # Mark as success 248 | results[original_task_number] = task_result 249 | return # Skip to the next task after deletion 250 | 251 | dir_path = os.path.dirname(file_path) 252 | 253 | if action == "create" and dir_path: 254 | # Create directory only if action is create and dir_path is not empty 255 | mkdir_command = f'mkdir -p {shlex.quote(dir_path)}' 256 | log_fh.write(f"Creating directory: {mkdir_command}\n") 257 | print(f"Creating directory: {mkdir_command}") 258 | try: 259 | subprocess.run(mkdir_command, shell=True, check=True) 260 | except subprocess.CalledProcessError as e: 261 | warning_message = f"Warning: Failed to create directory: {e}" 262 | print(warning_message) 263 | log_fh.write(warning_message + "\n") 264 | 265 | # Touch file for create or update actions 266 | if action in ["create", "update"]: 267 | touch_command = f'touch {shlex.quote(file_path)}' 268 | log_fh.write(f"Creating/updating file: {touch_command}\n") 269 | print(f"Creating/updating file: {touch_command}") 270 | try: 271 | subprocess.run(touch_command, shell=True, check=True) 272 | except subprocess.CalledProcessError as e: 273 | warning_message = f"Warning: Failed to create/update file: {e}" 274 | print(warning_message) 275 | log_fh.write(warning_message + "\n") 276 | 277 | # Format the task to string before sending to the command 278 | formatted_task = format_task(task) 279 | escaped_task = shlex.quote(formatted_task) 280 | command = ( 281 | f'python -m aider ' 282 | f'--yes ' 283 | f'{model_flag} ' 284 | f'--no-suggest-shell-commands ' 285 | f'{aider_args} ' 286 | f'--message {escaped_task}' 287 | ) 288 | if action.lower() != 'delete': 289 | command += f' {shlex.quote(file_path)}' 290 | 291 | log_fh.write(f"Running command: {command}\n") 292 | print(f"Running command: {trim_command(command)}") 293 | 294 | try: 295 | process = await asyncio.create_subprocess_shell( 296 | command, 297 | stdout=asyncio.subprocess.PIPE, 298 | stderr=asyncio.subprocess.PIPE, 299 | ) 300 | stdout, stderr = await process.communicate() 301 | 302 | # Write outputs to log file 303 | log_fh.write("Standard Output:\n") 304 | log_fh.write(stdout.decode() + "\n") 305 | log_fh.write("Standard Error:\n") 306 | log_fh.write(stderr.decode() + "\n") 307 | 308 | if process.returncode == 0: 309 | success_message = f"Task {original_task_number} executed successfully." 310 | print(success_message) 311 | log_fh.write(success_message + "\n") 312 | task_result['status'] = True # Mark as success 313 | else: 314 | error_message = f"Error executing task {original_task_number}. See {log_file} for details." 315 | print(error_message) 316 | log_fh.write(error_message + "\n") 317 | task_result['status'] = False # Mark as failure 318 | 319 | except Exception as e: 320 | exception_message = f"Exception while executing task {original_task_number}: {e}" 321 | print(exception_message) 322 | log_fh.write(exception_message + "\n") 323 | task_result['status'] = False # Mark as failure 324 | 325 | except Exception as e: 326 | print(f"Failed to write to log file {log_file}: {e}") 327 | task_result['status'] = False 328 | 329 | # Update results 330 | results[original_task_number] = task_result 331 | 332 | async def main(): 333 | args = parse_arguments() 334 | aider_args = ' '.join(args.aider_args) 335 | 336 | # Append auto-commit flags to aider_args 337 | if args.auto_commits: 338 | aider_args += ' --auto-commits' 339 | else: 340 | aider_args += ' --no-auto-commits' 341 | 342 | text = get_input(args.input) 343 | 344 | tasks = extract_tasks(text, args.use_json) 345 | if not tasks: 346 | print("No tasks found in the input.") 347 | return 348 | 349 | model_flag = get_model_flag(args) 350 | 351 | # Assign IDs to tasks and save to tasks.json 352 | log_folder = 'log' 353 | 354 | # Clean log/* before running 355 | if os.path.exists(log_folder): 356 | try: 357 | shutil.rmtree(log_folder) 358 | print(f"Cleaned the '{log_folder}' directory before running.") 359 | except Exception as e: 360 | print(f"Error cleaning the '{log_folder}' directory: {e}", file=sys.stderr) 361 | sys.exit(1) 362 | os.makedirs(log_folder, exist_ok=True) 363 | 364 | tasks_with_ids = [] 365 | for idx, task in enumerate(tasks, start=1): 366 | task_with_id = {'id': idx, **task} 367 | tasks_with_ids.append(task_with_id) 368 | 369 | # Save tasks to tasks.json 370 | tasks_json_path = os.path.join(log_folder, 'tasks.json') 371 | try: 372 | with open(tasks_json_path, 'w') as f: 373 | json.dump(tasks_with_ids, f, indent=4) 374 | print(f"Tasks saved to {tasks_json_path}") 375 | except IOError as e: 376 | print(f"Error saving tasks to {tasks_json_path}: {e}", file=sys.stderr) 377 | sys.exit(1) 378 | 379 | # Apply task selection based on --skip and --only arguments 380 | if args.only: 381 | selected_tasks = [tasks_with_ids[i-1] for i in args.only if 1 <= i <= len(tasks)] 382 | else: 383 | selected_tasks = tasks_with_ids[args.skip:] 384 | 385 | total_tasks = len(tasks) # Store the total number of tasks 386 | 387 | # Dictionary to keep track of task results 388 | results = {} 389 | 390 | # Create an event to signal timeout 391 | timeout_event = asyncio.Event() 392 | 393 | # Create a list of tasks to run asynchronously 394 | task_coroutines = [] 395 | for i, task in enumerate(selected_tasks): 396 | task_coroutines.append( 397 | run_task(i, task, model_flag, aider_args, args, total_tasks, results, timeout_event) 398 | ) 399 | 400 | # Set the timeout 401 | timeout_seconds = 600 # 5 minutes 402 | 403 | try: 404 | await asyncio.wait_for( 405 | asyncio.gather(*task_coroutines), 406 | timeout=timeout_seconds 407 | ) 408 | except asyncio.TimeoutError: 409 | print(f"\nTimeout reached after {timeout_seconds} seconds.") 410 | timeout_event.set() # Signal tasks to stop if possible 411 | 412 | # After all tasks are completed or timeout occurred, report the results 413 | print("\nSummary of task execution:") 414 | for task_num in sorted(results.keys()): 415 | task_result = results[task_num] 416 | status = "Success" if task_result['status'] else "Failed" 417 | code_missing = "Yes" if task_result.get('code_missing') else "No" 418 | unexpected_action = "Yes" if task_result.get('unexpected_action') else "No" 419 | action = task_result.get('action', 'N/A') 420 | path = task_result.get('path', 'N/A') 421 | print(f"Task {task_num}: {status}, Action: {action}, Path: {path}, Code Missing: {code_missing}, Unexpected Action: {unexpected_action}") 422 | 423 | if __name__ == "__main__": 424 | asyncio.run(main()) 425 | 426 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | files-to-prompt 2 | aider-chat 3 | llm 4 | 5 | --------------------------------------------------------------------------------