code-puppy 0.0.30__py3-none-any.whl → 0.0.32__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.
- code_puppy/agent.py +50 -16
- code_puppy/agent_prompts.py +4 -4
- code_puppy/command_line/file_path_completion.py +65 -0
- code_puppy/command_line/meta_command_handler.py +72 -0
- code_puppy/command_line/model_picker_completion.py +92 -0
- code_puppy/command_line/prompt_toolkit_completion.py +95 -131
- code_puppy/command_line/utils.py +36 -0
- code_puppy/config.py +53 -0
- code_puppy/main.py +46 -16
- code_puppy/session_memory.py +71 -0
- code_puppy/tools/__init__.py +11 -4
- code_puppy/tools/code_map.py +86 -0
- code_puppy/tools/command_runner.py +61 -197
- code_puppy/tools/common.py +3 -1
- code_puppy/tools/file_modifications.py +179 -329
- code_puppy/tools/file_operations.py +193 -353
- code_puppy/tools/web_search.py +11 -28
- {code_puppy-0.0.30.dist-info → code_puppy-0.0.32.dist-info}/METADATA +2 -1
- code_puppy-0.0.32.dist-info/RECORD +28 -0
- code_puppy-0.0.30.dist-info/RECORD +0 -21
- {code_puppy-0.0.30.data → code_puppy-0.0.32.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.30.dist-info → code_puppy-0.0.32.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.30.dist-info → code_puppy-0.0.32.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.30.dist-info → code_puppy-0.0.32.dist-info}/licenses/LICENSE +0 -0
| @@ -4,338 +4,188 @@ import difflib | |
| 4 4 | 
             
            import json
         | 
| 5 5 | 
             
            from code_puppy.tools.common import console
         | 
| 6 6 | 
             
            from typing import Dict, Any, List
         | 
| 7 | 
            -
            from code_puppy.agent import code_generation_agent
         | 
| 8 7 | 
             
            from pydantic_ai import RunContext
         | 
| 9 8 |  | 
| 10 | 
            -
             | 
| 11 | 
            -
            @ | 
| 12 | 
            -
            def delete_snippet_from_file(
         | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
                             | 
| 35 | 
            -
             | 
| 9 | 
            +
            def register_file_modifications_tools(agent):
         | 
| 10 | 
            +
                @agent.tool
         | 
| 11 | 
            +
                def delete_snippet_from_file(context: RunContext, file_path: str, snippet: str) -> Dict[str, Any]:
         | 
| 12 | 
            +
                    console.log(f"🗑️ Deleting snippet from file [bold red]{file_path}[/bold red]")
         | 
| 13 | 
            +
                    file_path = os.path.abspath(file_path)
         | 
| 14 | 
            +
                    console.print("\n[bold white on red] SNIPPET DELETION [/bold white on red]")
         | 
| 15 | 
            +
                    console.print(f"[bold yellow]From file:[/bold yellow] {file_path}")
         | 
| 16 | 
            +
                    try:
         | 
| 17 | 
            +
                        if not os.path.exists(file_path):
         | 
| 18 | 
            +
                            console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
         | 
| 19 | 
            +
                            return {"error": f"File '{file_path}' does not exist."}
         | 
| 20 | 
            +
                        if not os.path.isfile(file_path):
         | 
| 21 | 
            +
                            return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
         | 
| 22 | 
            +
                        with open(file_path, "r", encoding="utf-8") as f:
         | 
| 23 | 
            +
                            content = f.read()
         | 
| 24 | 
            +
                        if snippet not in content:
         | 
| 25 | 
            +
                            console.print(f"[bold red]Error:[/bold red] Snippet not found in file '{file_path}'")
         | 
| 26 | 
            +
                            return {"error": f"Snippet not found in file '{file_path}'."}
         | 
| 27 | 
            +
                        modified_content = content.replace(snippet, "")
         | 
| 28 | 
            +
                        diff_lines = list(difflib.unified_diff(content.splitlines(keepends=True), modified_content.splitlines(keepends=True), fromfile=f"a/{os.path.basename(file_path)}", tofile=f"b/{os.path.basename(file_path)}", n=3))
         | 
| 29 | 
            +
                        diff_text = "".join(diff_lines)
         | 
| 30 | 
            +
                        console.print("[bold cyan]Changes to be applied:[/bold cyan]")
         | 
| 31 | 
            +
                        if diff_text.strip():
         | 
| 32 | 
            +
                            formatted_diff = ""
         | 
| 33 | 
            +
                            for line in diff_lines:
         | 
| 34 | 
            +
                                if line.startswith("+") and not line.startswith("+++"):
         | 
| 35 | 
            +
                                    formatted_diff += f"[bold green]{line}[/bold green]"
         | 
| 36 | 
            +
                                elif line.startswith("-") and not line.startswith("---"):
         | 
| 37 | 
            +
                                    formatted_diff += f"[bold red]{line}[/bold red]"
         | 
| 38 | 
            +
                                elif line.startswith("@"):
         | 
| 39 | 
            +
                                    formatted_diff += f"[bold cyan]{line}[/bold cyan]"
         | 
| 40 | 
            +
                                else:
         | 
| 41 | 
            +
                                    formatted_diff += line
         | 
| 42 | 
            +
                            console.print(formatted_diff)
         | 
| 43 | 
            +
                        else:
         | 
| 44 | 
            +
                            console.print("[dim]No changes detected[/dim]")
         | 
| 45 | 
            +
                            return {"success": False, "path": file_path, "message": "No changes needed.", "diff": ""}
         | 
| 46 | 
            +
                        with open(file_path, "w", encoding="utf-8") as f:
         | 
| 47 | 
            +
                            f.write(modified_content)
         | 
| 48 | 
            +
                        return {"success": True, "path": file_path, "message": f"Snippet deleted from file '{file_path}'.", "diff": diff_text}
         | 
| 49 | 
            +
                    except PermissionError:
         | 
| 50 | 
            +
                        return {"error": f"Permission denied to delete '{file_path}'."}
         | 
| 51 | 
            +
                    except FileNotFoundError:
         | 
| 36 52 | 
             
                        return {"error": f"File '{file_path}' does not exist."}
         | 
| 53 | 
            +
                    except Exception as e:
         | 
| 54 | 
            +
                        return {"error": f"Error deleting file '{file_path}': {str(e)}"}
         | 
| 37 55 |  | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
                        console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
         | 
| 41 | 
            -
                        return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                    # Read the file content
         | 
| 44 | 
            -
                    with open(file_path, "r", encoding="utf-8") as f:
         | 
| 45 | 
            -
                        content = f.read()
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                    # Check if the snippet exists in the file
         | 
| 48 | 
            -
                    if snippet not in content:
         | 
| 49 | 
            -
                        console.print(
         | 
| 50 | 
            -
                            f"[bold red]Error:[/bold red] Snippet not found in file '{file_path}'"
         | 
| 51 | 
            -
                        )
         | 
| 52 | 
            -
                        return {"error": f"Snippet not found in file '{file_path}'."}
         | 
| 53 | 
            -
             | 
| 54 | 
            -
                    # Remove the snippet from the file content
         | 
| 55 | 
            -
                    modified_content = content.replace(snippet, "")
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                    # Generate a diff
         | 
| 58 | 
            -
                    diff_lines = list(
         | 
| 59 | 
            -
                        difflib.unified_diff(
         | 
| 60 | 
            -
                            content.splitlines(keepends=True),
         | 
| 61 | 
            -
                            modified_content.splitlines(keepends=True),
         | 
| 62 | 
            -
                            fromfile=f"a/{os.path.basename(file_path)}",
         | 
| 63 | 
            -
                            tofile=f"b/{os.path.basename(file_path)}",
         | 
| 64 | 
            -
                            n=3,  # Context lines
         | 
| 65 | 
            -
                        )
         | 
| 66 | 
            -
                    )
         | 
| 67 | 
            -
             | 
| 68 | 
            -
                    diff_text = "".join(diff_lines)
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                    # Display the diff
         | 
| 71 | 
            -
                    console.print("[bold cyan]Changes to be applied:[/bold cyan]")
         | 
| 72 | 
            -
             | 
| 73 | 
            -
                    if diff_text.strip():
         | 
| 74 | 
            -
                        # Format the diff for display with colorization
         | 
| 75 | 
            -
                        formatted_diff = ""
         | 
| 76 | 
            -
                        for line in diff_lines:
         | 
| 77 | 
            -
                            if line.startswith("+") and not line.startswith("+++"):
         | 
| 78 | 
            -
                                formatted_diff += f"[bold green]{line}[/bold green]"
         | 
| 79 | 
            -
                            elif line.startswith("-") and not line.startswith("---"):
         | 
| 80 | 
            -
                                formatted_diff += f"[bold red]{line}[/bold red]"
         | 
| 81 | 
            -
                            elif line.startswith("@"):
         | 
| 82 | 
            -
                                formatted_diff += f"[bold cyan]{line}[/bold cyan]"
         | 
| 83 | 
            -
                            else:
         | 
| 84 | 
            -
                                formatted_diff += line
         | 
| 85 | 
            -
             | 
| 86 | 
            -
                        console.print(formatted_diff)
         | 
| 87 | 
            -
                    else:
         | 
| 88 | 
            -
                        console.print("[dim]No changes detected[/dim]")
         | 
| 89 | 
            -
                        return {
         | 
| 90 | 
            -
                            "success": False,
         | 
| 91 | 
            -
                            "path": file_path,
         | 
| 92 | 
            -
                            "message": "No changes needed.",
         | 
| 93 | 
            -
                            "diff": "",
         | 
| 94 | 
            -
                        }
         | 
| 95 | 
            -
             | 
| 96 | 
            -
                    # Write the modified content back to the file
         | 
| 97 | 
            -
                    with open(file_path, "w", encoding="utf-8") as f:
         | 
| 98 | 
            -
                        f.write(modified_content)
         | 
| 99 | 
            -
             | 
| 100 | 
            -
                    return {
         | 
| 101 | 
            -
                        "success": True,
         | 
| 102 | 
            -
                        "path": file_path,
         | 
| 103 | 
            -
                        "message": f"Snippet deleted from file '{file_path}'.",
         | 
| 104 | 
            -
                        "diff": diff_text,
         | 
| 105 | 
            -
                    }
         | 
| 106 | 
            -
                except PermissionError:
         | 
| 107 | 
            -
                    return {"error": f"Permission denied to delete '{file_path}'."}
         | 
| 108 | 
            -
                except FileNotFoundError:
         | 
| 109 | 
            -
                    # This should be caught by the initial check, but just in case
         | 
| 110 | 
            -
                    return {"error": f"File '{file_path}' does not exist."}
         | 
| 111 | 
            -
                except Exception as e:
         | 
| 112 | 
            -
                    return {"error": f"Error deleting file '{file_path}': {str(e)}"}
         | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
            @code_generation_agent.tool
         | 
| 116 | 
            -
            def write_to_file(
         | 
| 117 | 
            -
                context: RunContext,
         | 
| 118 | 
            -
                path: str, 
         | 
| 119 | 
            -
                content: str
         | 
| 120 | 
            -
            ) -> Dict[str, Any]:
         | 
| 121 | 
            -
                """Write content to a file at the specified path.
         | 
| 122 | 
            -
                
         | 
| 123 | 
            -
                If the file exists, it will be overwritten with the provided content.
         | 
| 124 | 
            -
                If the file doesn't exist, it will be created.
         | 
| 125 | 
            -
                This function will automatically create any directories needed to write the file.
         | 
| 126 | 
            -
                
         | 
| 127 | 
            -
                Args:
         | 
| 128 | 
            -
                    path: The path of the file to write to (relative to the current working directory)
         | 
| 129 | 
            -
                    content: The content to write to the file. ALWAYS provide the COMPLETE intended content of the file.
         | 
| 130 | 
            -
                    
         | 
| 131 | 
            -
                Returns:
         | 
| 132 | 
            -
                    A dictionary with status and message about the operation.
         | 
| 133 | 
            -
                """
         | 
| 134 | 
            -
                try:
         | 
| 135 | 
            -
                    # Convert to absolute path if not already
         | 
| 136 | 
            -
                    file_path = os.path.abspath(path)
         | 
| 137 | 
            -
                    
         | 
| 138 | 
            -
                    # Create directories if they don't exist
         | 
| 139 | 
            -
                    os.makedirs(os.path.dirname(file_path), exist_ok=True)
         | 
| 140 | 
            -
                    
         | 
| 141 | 
            -
                    # Display information
         | 
| 142 | 
            -
                    console.print("\n[bold white on blue] FILE WRITE [/bold white on blue]")
         | 
| 143 | 
            -
                    console.print(f"[bold yellow]Writing to:[/bold yellow] {file_path}")
         | 
| 144 | 
            -
                    
         | 
| 145 | 
            -
                    # Check if file exists
         | 
| 146 | 
            -
                    file_exists = os.path.exists(file_path)
         | 
| 147 | 
            -
                    if file_exists:
         | 
| 148 | 
            -
                        console.print(f'[bold red]Refusing to overwrite existing file:[/bold red] {file_path}')
         | 
| 149 | 
            -
                        return {
         | 
| 150 | 
            -
                            'success': False,
         | 
| 151 | 
            -
                            'path': file_path,
         | 
| 152 | 
            -
                            'message': f'Cowardly refusing to overwrite existing file: {file_path}',
         | 
| 153 | 
            -
                            'changed': False,
         | 
| 154 | 
            -
                        }
         | 
| 155 | 
            -
                    
         | 
| 156 | 
            -
                    # Show the content to be written
         | 
| 157 | 
            -
                    trimmed_content = content
         | 
| 158 | 
            -
                    max_preview = 1000
         | 
| 159 | 
            -
                    if len(content) > max_preview:
         | 
| 160 | 
            -
                        trimmed_content = content[:max_preview] + '... [truncated]'
         | 
| 161 | 
            -
                    console.print('[bold magenta]Content to be written:[/bold magenta]')
         | 
| 162 | 
            -
                    console.print(trimmed_content, highlight=False)
         | 
| 163 | 
            -
             | 
| 164 | 
            -
                    # Write the content to the file
         | 
| 165 | 
            -
                    with open(file_path, 'w', encoding='utf-8') as f:
         | 
| 166 | 
            -
                        f.write(content)
         | 
| 167 | 
            -
                    
         | 
| 168 | 
            -
                    action = "updated" if file_exists else "created"
         | 
| 169 | 
            -
                    return {
         | 
| 170 | 
            -
                        "success": True,
         | 
| 171 | 
            -
                        "path": file_path,
         | 
| 172 | 
            -
                        "message": f"File '{file_path}' {action} successfully.",
         | 
| 173 | 
            -
                        "diff": trimmed_content,
         | 
| 174 | 
            -
                        "changed": True,
         | 
| 175 | 
            -
                    }
         | 
| 176 | 
            -
                
         | 
| 177 | 
            -
                except Exception as e:
         | 
| 178 | 
            -
                    console.print(f"[bold red]Error:[/bold red] {str(e)}")
         | 
| 179 | 
            -
                    return {"error": f"Error writing to file '{path}': {str(e)}"}
         | 
| 180 | 
            -
             | 
| 181 | 
            -
             | 
| 182 | 
            -
            @code_generation_agent.tool(retries=5)
         | 
| 183 | 
            -
            def replace_in_file(
         | 
| 184 | 
            -
                context: RunContext,
         | 
| 185 | 
            -
                path: str, 
         | 
| 186 | 
            -
                diff: str
         | 
| 187 | 
            -
            ) -> Dict[str, Any]:
         | 
| 188 | 
            -
                """Replace text in a file based on a JSON-formatted replacements object.
         | 
| 189 | 
            -
                
         | 
| 190 | 
            -
                Args:
         | 
| 191 | 
            -
                    path: The path of the file to modify
         | 
| 192 | 
            -
                    diff: A JSON string containing replacements, formatted as:
         | 
| 193 | 
            -
                          {"replacements": [{"old_str": "text to find", "new_str": "replacement"}]}
         | 
| 194 | 
            -
                          
         | 
| 195 | 
            -
                Returns:
         | 
| 196 | 
            -
                    A dictionary with status and message about the operation.
         | 
| 197 | 
            -
                """
         | 
| 198 | 
            -
                try:
         | 
| 199 | 
            -
                    # Convert to absolute path if not already
         | 
| 200 | 
            -
                    file_path = os.path.abspath(path)
         | 
| 201 | 
            -
                    
         | 
| 202 | 
            -
                    # Display information
         | 
| 203 | 
            -
                    console.print("\n[bold white on yellow] FILE REPLACEMENTS [/bold white on yellow]")
         | 
| 204 | 
            -
                    console.print(f"[bold yellow]Modifying:[/bold yellow] {file_path}")
         | 
| 205 | 
            -
                    
         | 
| 206 | 
            -
                    # Check if the file exists
         | 
| 207 | 
            -
                    if not os.path.exists(file_path):
         | 
| 208 | 
            -
                        console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
         | 
| 209 | 
            -
                        return {"error": f"File '{file_path}' does not exist"}
         | 
| 210 | 
            -
                        
         | 
| 211 | 
            -
                    if not os.path.isfile(file_path):
         | 
| 212 | 
            -
                        console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
         | 
| 213 | 
            -
                        return {"error": f"'{file_path}' is not a file."}
         | 
| 214 | 
            -
                    
         | 
| 215 | 
            -
                    # Parse the JSON replacements
         | 
| 56 | 
            +
                @agent.tool
         | 
| 57 | 
            +
                def write_to_file(context: RunContext, path: str, content: str) -> Dict[str, Any]:
         | 
| 216 58 | 
             
                    try:
         | 
| 217 | 
            -
                         | 
| 218 | 
            -
                         | 
| 219 | 
            -
                        
         | 
| 220 | 
            -
                         | 
| 221 | 
            -
             | 
| 222 | 
            -
             | 
| 223 | 
            -
             | 
| 224 | 
            -
             | 
| 225 | 
            -
                         | 
| 226 | 
            -
             | 
| 227 | 
            -
             | 
| 228 | 
            -
             | 
| 229 | 
            -
                         | 
| 230 | 
            -
             | 
| 231 | 
            -
             | 
| 232 | 
            -
             | 
| 233 | 
            -
             | 
| 234 | 
            -
             | 
| 235 | 
            -
                     | 
| 236 | 
            -
                         | 
| 237 | 
            -
                         | 
| 238 | 
            -
             | 
| 239 | 
            -
             | 
| 240 | 
            -
             | 
| 241 | 
            -
             | 
| 242 | 
            -
             | 
| 243 | 
            -
                         | 
| 244 | 
            -
             | 
| 245 | 
            -
             | 
| 246 | 
            -
             | 
| 247 | 
            -
             | 
| 248 | 
            -
                         | 
| 249 | 
            -
             | 
| 250 | 
            -
             | 
| 251 | 
            -
             | 
| 252 | 
            -
             | 
| 253 | 
            -
                         | 
| 254 | 
            -
             | 
| 255 | 
            -
             | 
| 256 | 
            -
             | 
| 257 | 
            -
             | 
| 258 | 
            -
                             | 
| 259 | 
            -
             | 
| 260 | 
            -
             | 
| 261 | 
            -
             | 
| 262 | 
            -
             | 
| 263 | 
            -
             | 
| 264 | 
            -
             | 
| 265 | 
            -
             | 
| 266 | 
            -
             | 
| 267 | 
            -
             | 
| 268 | 
            -
                             | 
| 269 | 
            -
                                 | 
| 270 | 
            -
             | 
| 271 | 
            -
             | 
| 272 | 
            -
             | 
| 273 | 
            -
             | 
| 274 | 
            -
             | 
| 275 | 
            -
             | 
| 276 | 
            -
             | 
| 277 | 
            -
             | 
| 278 | 
            -
             | 
| 279 | 
            -
             | 
| 280 | 
            -
             | 
| 281 | 
            -
             | 
| 282 | 
            -
             | 
| 283 | 
            -
             | 
| 284 | 
            -
             | 
| 285 | 
            -
             | 
| 286 | 
            -
             | 
| 287 | 
            -
             | 
| 288 | 
            -
             | 
| 289 | 
            -
             | 
| 290 | 
            -
             | 
| 291 | 
            -
             | 
| 292 | 
            -
             | 
| 293 | 
            -
                        " | 
| 294 | 
            -
             | 
| 295 | 
            -
                         | 
| 296 | 
            -
                         | 
| 297 | 
            -
                         | 
| 298 | 
            -
             | 
| 299 | 
            -
             | 
| 300 | 
            -
             | 
| 301 | 
            -
             | 
| 302 | 
            -
             | 
| 303 | 
            -
             | 
| 304 | 
            -
             | 
| 305 | 
            -
             | 
| 306 | 
            -
             | 
| 307 | 
            -
             | 
| 308 | 
            -
             | 
| 309 | 
            -
             | 
| 310 | 
            -
             | 
| 311 | 
            -
             | 
| 312 | 
            -
             | 
| 313 | 
            -
             | 
| 314 | 
            -
             | 
| 315 | 
            -
             | 
| 316 | 
            -
             | 
| 317 | 
            -
             | 
| 318 | 
            -
             | 
| 319 | 
            -
             | 
| 320 | 
            -
             | 
| 59 | 
            +
                        file_path = os.path.abspath(path)
         | 
| 60 | 
            +
                        os.makedirs(os.path.dirname(file_path), exist_ok=True)
         | 
| 61 | 
            +
                        console.print("\n[bold white on blue] FILE WRITE [/bold white on blue]")
         | 
| 62 | 
            +
                        console.print(f"[bold yellow]Writing to:[/bold yellow] {file_path}")
         | 
| 63 | 
            +
                        file_exists = os.path.exists(file_path)
         | 
| 64 | 
            +
                        if file_exists:
         | 
| 65 | 
            +
                            console.print(f'[bold red]Refusing to overwrite existing file:[/bold red] {file_path}')
         | 
| 66 | 
            +
                            return {'success': False,'path': file_path,'message': f'Cowardly refusing to overwrite existing file: {file_path}','changed': False,}
         | 
| 67 | 
            +
                        trimmed_content = content
         | 
| 68 | 
            +
                        max_preview = 1000
         | 
| 69 | 
            +
                        if len(content) > max_preview:
         | 
| 70 | 
            +
                            trimmed_content = content[:max_preview] + '... [truncated]'
         | 
| 71 | 
            +
                        console.print('[bold magenta]Content to be written:[/bold magenta]')
         | 
| 72 | 
            +
                        console.print(trimmed_content, highlight=False)
         | 
| 73 | 
            +
                        with open(file_path, 'w', encoding='utf-8') as f:
         | 
| 74 | 
            +
                            f.write(content)
         | 
| 75 | 
            +
                        action = "updated" if file_exists else "created"
         | 
| 76 | 
            +
                        return {"success": True,"path": file_path,"message": f"File '{file_path}' {action} successfully.","diff": trimmed_content,"changed": True,}
         | 
| 77 | 
            +
                    except Exception as e:
         | 
| 78 | 
            +
                        console.print(f"[bold red]Error:[/bold red] {str(e)}")
         | 
| 79 | 
            +
                        return {"error": f"Error writing to file '{path}': {str(e)}"}
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                @agent.tool(retries=5)
         | 
| 82 | 
            +
                def replace_in_file(context: RunContext, path: str, diff: str) -> Dict[str, Any]:
         | 
| 83 | 
            +
                    try:
         | 
| 84 | 
            +
                        file_path = os.path.abspath(path)
         | 
| 85 | 
            +
                        console.print("\n[bold white on yellow] FILE REPLACEMENTS [/bold white on yellow]")
         | 
| 86 | 
            +
                        console.print(f"[bold yellow]Modifying:[/bold yellow] {file_path}")
         | 
| 87 | 
            +
                        if not os.path.exists(file_path):
         | 
| 88 | 
            +
                            console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
         | 
| 89 | 
            +
                            return {"error": f"File '{file_path}' does not exist"}
         | 
| 90 | 
            +
                        if not os.path.isfile(file_path):
         | 
| 91 | 
            +
                            return {"error": f"'{file_path}' is not a file."}
         | 
| 92 | 
            +
                        # ------------------------------------------------------------------
         | 
| 93 | 
            +
                        # Robust parsing of the diff argument
         | 
| 94 | 
            +
                        # The agent sometimes sends single-quoted or otherwise invalid JSON.
         | 
| 95 | 
            +
                        # Attempt to recover by trying several strategies before giving up.
         | 
| 96 | 
            +
                        # ------------------------------------------------------------------
         | 
| 97 | 
            +
                        parsed_successfully = False
         | 
| 98 | 
            +
                        replacements: List[Dict[str, str]] = []
         | 
| 99 | 
            +
                        try:
         | 
| 100 | 
            +
                            replacements_data = json.loads(diff)
         | 
| 101 | 
            +
                            replacements = replacements_data.get("replacements", [])
         | 
| 102 | 
            +
                            parsed_successfully = True
         | 
| 103 | 
            +
                        except json.JSONDecodeError:
         | 
| 104 | 
            +
                            # Fallback 1: convert single quotes to double quotes and retry
         | 
| 105 | 
            +
                            try:
         | 
| 106 | 
            +
                                sanitized = diff.replace("'", '"')
         | 
| 107 | 
            +
                                replacements_data = json.loads(sanitized)
         | 
| 108 | 
            +
                                replacements = replacements_data.get("replacements", [])
         | 
| 109 | 
            +
                                parsed_successfully = True
         | 
| 110 | 
            +
                            except json.JSONDecodeError:
         | 
| 111 | 
            +
                                # Fallback 2: attempt Python literal eval
         | 
| 112 | 
            +
                                try:
         | 
| 113 | 
            +
                                    import ast
         | 
| 114 | 
            +
                                    replacements_data = ast.literal_eval(diff)
         | 
| 115 | 
            +
                                    if isinstance(replacements_data, dict):
         | 
| 116 | 
            +
                                        replacements = replacements_data.get("replacements", []) if "replacements" in replacements_data else []
         | 
| 117 | 
            +
                                        # If dict keys look like a single replacement, wrap it
         | 
| 118 | 
            +
                                        if not replacements:
         | 
| 119 | 
            +
                                            # maybe it's already {"old_str": ..., "new_str": ...}
         | 
| 120 | 
            +
                                            if all(k in replacements_data for k in ("old_str", "new_str")):
         | 
| 121 | 
            +
                                                replacements = [
         | 
| 122 | 
            +
                                                    {
         | 
| 123 | 
            +
                                                        "old_str": replacements_data["old_str"],
         | 
| 124 | 
            +
                                                        "new_str": replacements_data["new_str"],
         | 
| 125 | 
            +
                                                    }
         | 
| 126 | 
            +
                                                ]
         | 
| 127 | 
            +
                                        parsed_successfully = True
         | 
| 128 | 
            +
                                except Exception as e2:
         | 
| 129 | 
            +
                                    console.print(
         | 
| 130 | 
            +
                                        f"[bold red]Error:[/bold red] Could not parse diff as JSON or Python literal. Reason: {e2}"
         | 
| 131 | 
            +
                                    )
         | 
| 132 | 
            +
                        if not parsed_successfully or not replacements:
         | 
| 133 | 
            +
                            console.print("[bold red]Error:[/bold red] No valid replacements found in the diff after all parsing attempts")
         | 
| 134 | 
            +
                            return {"error": "No valid replacements found in the diff"}
         | 
| 135 | 
            +
                        with open(file_path, "r", encoding="utf-8") as f:
         | 
| 136 | 
            +
                            current_content = f.read()
         | 
| 137 | 
            +
                        modified_content = current_content
         | 
| 138 | 
            +
                        applied_replacements = []
         | 
| 139 | 
            +
                        for i, replacement in enumerate(replacements, 1):
         | 
| 140 | 
            +
                            old_str = replacement.get("old_str", "")
         | 
| 141 | 
            +
                            new_str = replacement.get("new_str", "")
         | 
| 142 | 
            +
                            if not old_str:
         | 
| 143 | 
            +
                                console.print(f"[bold yellow]Warning:[/bold yellow] Replacement #{i} has empty old_str")
         | 
| 144 | 
            +
                                continue
         | 
| 145 | 
            +
                            if old_str not in modified_content:
         | 
| 146 | 
            +
                                console.print(f"[bold red]Error:[/bold red] Text not found in file: {old_str[:50]}...")
         | 
| 147 | 
            +
                                return {"error": f"Text to replace not found in file (replacement #{i})"}
         | 
| 148 | 
            +
                            modified_content = modified_content.replace(old_str, new_str)
         | 
| 149 | 
            +
                            applied_replacements.append({"old_str": old_str, "new_str": new_str})
         | 
| 150 | 
            +
                        diff_lines = list(difflib.unified_diff(current_content.splitlines(keepends=True), modified_content.splitlines(keepends=True), fromfile=f"a/{os.path.basename(file_path)}", tofile=f"b/{os.path.basename(file_path)}", n=3))
         | 
| 151 | 
            +
                        diff_text = "".join(diff_lines)
         | 
| 152 | 
            +
                        console.print("[bold cyan]Changes to be applied:[/bold cyan]")
         | 
| 153 | 
            +
                        if diff_text.strip():
         | 
| 154 | 
            +
                            formatted_diff = ""
         | 
| 155 | 
            +
                            for line in diff_lines:
         | 
| 156 | 
            +
                                if line.startswith("+") and not line.startswith("+++"):
         | 
| 157 | 
            +
                                    formatted_diff += f"[bold green]{line}[/bold green]"
         | 
| 158 | 
            +
                                elif line.startswith("-") and not line.startswith("---"):
         | 
| 159 | 
            +
                                    formatted_diff += f"[bold red]{line}[/bold red]"
         | 
| 160 | 
            +
                                elif line.startswith("@"):
         | 
| 161 | 
            +
                                    formatted_diff += f"[bold cyan]{line}[/bold cyan]"
         | 
| 162 | 
            +
                                else:
         | 
| 163 | 
            +
                                    formatted_diff += line
         | 
| 164 | 
            +
                            console.print(formatted_diff)
         | 
| 165 | 
            +
                        else:
         | 
| 166 | 
            +
                            console.print("[dim]No changes detected - file content is identical[/dim]")
         | 
| 167 | 
            +
                            return {"success": False,"path": file_path,"message": "No changes to apply.","diff": "","changed": False,}
         | 
| 168 | 
            +
                        with open(file_path, "w", encoding="utf-8") as f:
         | 
| 169 | 
            +
                            f.write(modified_content)
         | 
| 170 | 
            +
                        return {"success": True,"path": file_path,"message": f"Applied {len(applied_replacements)} replacements to '{file_path}'","diff": diff_text,"changed": True,"replacements_applied": len(applied_replacements)}
         | 
| 171 | 
            +
                    except Exception as e:
         | 
| 172 | 
            +
                        console.print(f"[bold red]Error:[/bold red] {str(e)}")
         | 
| 173 | 
            +
                        return {"error": f"Error replacing in file '{path}': {str(e)}"}
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                @agent.tool
         | 
| 176 | 
            +
                def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
         | 
| 177 | 
            +
                    console.log(f"🗑️ Deleting file [bold red]{file_path}[/bold red]")
         | 
| 178 | 
            +
                    file_path = os.path.abspath(file_path)
         | 
| 179 | 
            +
                    try:
         | 
| 180 | 
            +
                        if not os.path.exists(file_path):
         | 
| 181 | 
            +
                            return {"error": f"File '{file_path}' does not exist."}
         | 
| 182 | 
            +
                        if not os.path.isfile(file_path):
         | 
| 183 | 
            +
                            return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
         | 
| 184 | 
            +
                        os.remove(file_path)
         | 
| 185 | 
            +
                        return {"success": True,"path": file_path,"message": f"File '{file_path}' deleted successfully."}
         | 
| 186 | 
            +
                    except PermissionError:
         | 
| 187 | 
            +
                        return {"error": f"Permission denied to delete '{file_path}'."}
         | 
| 188 | 
            +
                    except FileNotFoundError:
         | 
| 321 189 | 
             
                        return {"error": f"File '{file_path}' does not exist."}
         | 
| 322 | 
            -
             | 
| 323 | 
            -
             | 
| 324 | 
            -
                    if not os.path.isfile(file_path):
         | 
| 325 | 
            -
                        return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
         | 
| 326 | 
            -
             | 
| 327 | 
            -
                    # Attempt to delete the file
         | 
| 328 | 
            -
                    os.remove(file_path)
         | 
| 329 | 
            -
             | 
| 330 | 
            -
                    return {
         | 
| 331 | 
            -
                        "success": True,
         | 
| 332 | 
            -
                        "path": file_path,
         | 
| 333 | 
            -
                        "message": f"File '{file_path}' deleted successfully.",
         | 
| 334 | 
            -
                    }
         | 
| 335 | 
            -
                except PermissionError:
         | 
| 336 | 
            -
                    return {"error": f"Permission denied to delete '{file_path}'."}
         | 
| 337 | 
            -
                except FileNotFoundError:
         | 
| 338 | 
            -
                    # This should be caught by the initial check, but just in case
         | 
| 339 | 
            -
                    return {"error": f"File '{file_path}' does not exist."}
         | 
| 340 | 
            -
                except Exception as e:
         | 
| 341 | 
            -
                    return {"error": f"Error deleting file '{file_path}': {str(e)}"}
         | 
| 190 | 
            +
                    except Exception as e:
         | 
| 191 | 
            +
                        return {"error": f"Error deleting file '{file_path}': {str(e)}"}
         |