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
    
        code_puppy/config.py
    ADDED
    
    | @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            import os
         | 
| 2 | 
            +
            import configparser
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".code_puppy")
         | 
| 5 | 
            +
            CONFIG_FILE = os.path.join(CONFIG_DIR, "puppy.cfg")
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            DEFAULT_SECTION = "puppy"
         | 
| 8 | 
            +
            REQUIRED_KEYS = ["puppy_name", "owner_name"]
         | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
            def ensure_config_exists():
         | 
| 12 | 
            +
                """
         | 
| 13 | 
            +
                Ensure that the .code_puppy dir and puppy.cfg exist, prompting if needed.
         | 
| 14 | 
            +
                Returns configparser.ConfigParser for reading.
         | 
| 15 | 
            +
                """
         | 
| 16 | 
            +
                if not os.path.exists(CONFIG_DIR):
         | 
| 17 | 
            +
                    os.makedirs(CONFIG_DIR, exist_ok=True)
         | 
| 18 | 
            +
                exists = os.path.isfile(CONFIG_FILE)
         | 
| 19 | 
            +
                config = configparser.ConfigParser()
         | 
| 20 | 
            +
                if exists:
         | 
| 21 | 
            +
                    config.read(CONFIG_FILE)
         | 
| 22 | 
            +
                missing = []
         | 
| 23 | 
            +
                if DEFAULT_SECTION not in config:
         | 
| 24 | 
            +
                    config[DEFAULT_SECTION] = {}
         | 
| 25 | 
            +
                for key in REQUIRED_KEYS:
         | 
| 26 | 
            +
                    if not config[DEFAULT_SECTION].get(key):
         | 
| 27 | 
            +
                        missing.append(key)
         | 
| 28 | 
            +
                if missing:
         | 
| 29 | 
            +
                    print("🐾 Let's get your Puppy ready!")
         | 
| 30 | 
            +
                    for key in missing:
         | 
| 31 | 
            +
                        if key == "puppy_name":
         | 
| 32 | 
            +
                            val = input("What should we name the puppy? ").strip()
         | 
| 33 | 
            +
                        elif key == "owner_name":
         | 
| 34 | 
            +
                            val = input("What's your name (so Code Puppy knows its master)? ").strip()
         | 
| 35 | 
            +
                        else:
         | 
| 36 | 
            +
                            val = input(f"Enter {key}: ").strip()
         | 
| 37 | 
            +
                        config[DEFAULT_SECTION][key] = val
         | 
| 38 | 
            +
                    with open(CONFIG_FILE, "w") as f:
         | 
| 39 | 
            +
                        config.write(f)
         | 
| 40 | 
            +
                return config
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            def get_value(key: str):
         | 
| 43 | 
            +
                config = configparser.ConfigParser()
         | 
| 44 | 
            +
                config.read(CONFIG_FILE)
         | 
| 45 | 
            +
                val = config.get(DEFAULT_SECTION, key, fallback=None)
         | 
| 46 | 
            +
                return val
         | 
| 47 | 
            +
             | 
| 48 | 
            +
             | 
| 49 | 
            +
            def get_puppy_name():
         | 
| 50 | 
            +
                return get_value("puppy_name") or "Puppy"
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            def get_owner_name():
         | 
| 53 | 
            +
                return get_value("owner_name") or "Master"
         | 
    
        code_puppy/main.py
    CHANGED
    
    | @@ -5,6 +5,7 @@ from code_puppy.version_checker import fetch_latest_version | |
| 5 5 | 
             
            from code_puppy import __version__
         | 
| 6 6 | 
             
            import sys
         | 
| 7 7 | 
             
            from dotenv import load_dotenv
         | 
| 8 | 
            +
            from code_puppy.config import ensure_config_exists
         | 
| 8 9 | 
             
            from rich.console import Console
         | 
| 9 10 | 
             
            from rich.markdown import Markdown
         | 
| 10 11 | 
             
            from rich.console import ConsoleOptions, RenderResult
         | 
| @@ -12,12 +13,13 @@ from rich.markdown import CodeBlock | |
| 12 13 | 
             
            from rich.text import Text
         | 
| 13 14 | 
             
            from rich.syntax import Syntax
         | 
| 14 15 | 
             
            from code_puppy.command_line.prompt_toolkit_completion import (
         | 
| 15 | 
            -
                 | 
| 16 | 
            +
                get_input_with_combined_completion,
         | 
| 17 | 
            +
                get_prompt_with_active_model
         | 
| 16 18 | 
             
            )
         | 
| 17 19 |  | 
| 18 20 | 
             
            # Initialize rich console for pretty output
         | 
| 19 21 | 
             
            from code_puppy.tools.common import console
         | 
| 20 | 
            -
            from code_puppy.agent import  | 
| 22 | 
            +
            from code_puppy.agent import get_code_generation_agent, session_memory
         | 
| 21 23 |  | 
| 22 24 | 
             
            from code_puppy.tools import *
         | 
| 23 25 |  | 
| @@ -31,6 +33,8 @@ def get_secret_file_path(): | |
| 31 33 |  | 
| 32 34 |  | 
| 33 35 | 
             
            async def main():
         | 
| 36 | 
            +
                # Ensure the config directory and puppy.cfg with name info exist (prompt user if needed)
         | 
| 37 | 
            +
                ensure_config_exists()
         | 
| 34 38 | 
             
                current_version = __version__
         | 
| 35 39 | 
             
                latest_version = fetch_latest_version('code-puppy')
         | 
| 36 40 | 
             
                console.print(f'Current version: {current_version}')
         | 
| @@ -59,9 +63,18 @@ async def main(): | |
| 59 63 | 
             
                    command = " ".join(args.command)
         | 
| 60 64 | 
             
                    try:
         | 
| 61 65 | 
             
                        while not shutdown_flag:
         | 
| 62 | 
            -
                             | 
| 66 | 
            +
                            agent = get_code_generation_agent()
         | 
| 67 | 
            +
                            response = await agent.run(command)
         | 
| 63 68 | 
             
                            agent_response = response.output
         | 
| 64 69 | 
             
                            console.print(agent_response.output_message)
         | 
| 70 | 
            +
                            # Log to session memory
         | 
| 71 | 
            +
                            session_memory().log_task(
         | 
| 72 | 
            +
                                f'Command executed: {command}',
         | 
| 73 | 
            +
                                extras={ 
         | 
| 74 | 
            +
                                    'output': agent_response.output_message,
         | 
| 75 | 
            +
                                    'awaiting_user_input': agent_response.awaiting_user_input
         | 
| 76 | 
            +
                                }
         | 
| 77 | 
            +
                            )
         | 
| 65 78 | 
             
                            if agent_response.awaiting_user_input:
         | 
| 66 79 | 
             
                                console.print(
         | 
| 67 80 | 
             
                                    "[bold red]The agent requires further input. Interactive mode is recommended for such tasks."
         | 
| @@ -82,13 +95,12 @@ async def main(): | |
| 82 95 |  | 
| 83 96 | 
             
            # Add the file handling functionality for interactive mode
         | 
| 84 97 | 
             
            async def interactive_mode(history_file_path: str) -> None:
         | 
| 98 | 
            +
                from code_puppy.command_line.meta_command_handler import handle_meta_command
         | 
| 85 99 | 
             
                """Run the agent in interactive mode."""
         | 
| 86 100 | 
             
                console.print("[bold green]Code Puppy[/bold green] - Interactive Mode")
         | 
| 87 101 | 
             
                console.print("Type 'exit' or 'quit' to exit the interactive mode.")
         | 
| 88 102 | 
             
                console.print("Type 'clear' to reset the conversation history.")
         | 
| 89 | 
            -
                console.print(
         | 
| 90 | 
            -
                    "Type [bold blue]@[/bold blue] followed by a path to use file path completion."
         | 
| 91 | 
            -
                )
         | 
| 103 | 
            +
                console.print("Type [bold blue]@[/bold blue] for path completion, or [bold blue]~m[/bold blue] to pick a model.")
         | 
| 92 104 |  | 
| 93 105 | 
             
                # Check if prompt_toolkit is installed
         | 
| 94 106 | 
             
                try:
         | 
| @@ -133,9 +145,10 @@ async def interactive_mode(history_file_path: str) -> None: | |
| 133 145 | 
             
                    try:
         | 
| 134 146 | 
             
                        # Use prompt_toolkit for enhanced input with path completion
         | 
| 135 147 | 
             
                        try:
         | 
| 136 | 
            -
                            # Use the async version of  | 
| 137 | 
            -
                            task = await  | 
| 138 | 
            -
                                 | 
| 148 | 
            +
                            # Use the async version of get_input_with_combined_completion
         | 
| 149 | 
            +
                            task = await get_input_with_combined_completion(
         | 
| 150 | 
            +
                                get_prompt_with_active_model(),
         | 
| 151 | 
            +
                                history_file=history_file_path_prompt
         | 
| 139 152 | 
             
                            )
         | 
| 140 153 | 
             
                        except ImportError:
         | 
| 141 154 | 
             
                            # Fall back to basic input if prompt_toolkit is not available
         | 
| @@ -151,8 +164,8 @@ async def interactive_mode(history_file_path: str) -> None: | |
| 151 164 | 
             
                        console.print("[bold green]Goodbye![/bold green]")
         | 
| 152 165 | 
             
                        break
         | 
| 153 166 |  | 
| 154 | 
            -
                    # Check for clear command
         | 
| 155 | 
            -
                    if task.strip().lower()  | 
| 167 | 
            +
                    # Check for clear command (supports both `clear` and `~clear`)
         | 
| 168 | 
            +
                    if task.strip().lower() in ("clear", "~clear"):
         | 
| 156 169 | 
             
                        message_history = []
         | 
| 157 170 | 
             
                        console.print("[bold yellow]Conversation history cleared![/bold yellow]")
         | 
| 158 171 | 
             
                        console.print(
         | 
| @@ -160,6 +173,10 @@ async def interactive_mode(history_file_path: str) -> None: | |
| 160 173 | 
             
                        )
         | 
| 161 174 | 
             
                        continue
         | 
| 162 175 |  | 
| 176 | 
            +
                    # Handle ~ meta/config commands before anything else
         | 
| 177 | 
            +
                    if task.strip().startswith('~'):
         | 
| 178 | 
            +
                        if handle_meta_command(task.strip(), console):
         | 
| 179 | 
            +
                            continue
         | 
| 163 180 | 
             
                    if task.strip():
         | 
| 164 181 | 
             
                        console.print(f"\n[bold blue]Processing task:[/bold blue] {task}\n")
         | 
| 165 182 |  | 
| @@ -175,15 +192,28 @@ async def interactive_mode(history_file_path: str) -> None: | |
| 175 192 | 
             
                            # Store agent's full response
         | 
| 176 193 | 
             
                            agent_response = None
         | 
| 177 194 |  | 
| 178 | 
            -
                             | 
| 179 | 
            -
             | 
| 180 | 
            -
                            )
         | 
| 195 | 
            +
                            agent = get_code_generation_agent()
         | 
| 196 | 
            +
                            result = await agent.run(task, message_history=message_history)
         | 
| 181 197 | 
             
                            # Get the structured response
         | 
| 182 198 | 
             
                            agent_response = result.output
         | 
| 183 199 | 
             
                            console.print(agent_response.output_message)
         | 
| 200 | 
            +
                            # Log to session memory
         | 
| 201 | 
            +
                            session_memory().log_task(
         | 
| 202 | 
            +
                                f'Interactive task: {task}',
         | 
| 203 | 
            +
                                extras={ 
         | 
| 204 | 
            +
                                    'output': agent_response.output_message,
         | 
| 205 | 
            +
                                    'awaiting_user_input': agent_response.awaiting_user_input
         | 
| 206 | 
            +
                                }
         | 
| 207 | 
            +
                            )
         | 
| 184 208 |  | 
| 185 | 
            -
                            # Update message history  | 
| 186 | 
            -
                             | 
| 209 | 
            +
                            # Update message history but apply filters & limits
         | 
| 210 | 
            +
                            new_msgs = result.new_messages()
         | 
| 211 | 
            +
                            # 1. Drop any system/config messages (e.g., "agent loaded with model")
         | 
| 212 | 
            +
                            filtered = [m for m in new_msgs if not (isinstance(m, dict) and m.get("role") == "system")]
         | 
| 213 | 
            +
                            # 2. Append to existing history and keep only the most recent 40
         | 
| 214 | 
            +
                            message_history.extend(filtered)
         | 
| 215 | 
            +
                            if len(message_history) > 40:
         | 
| 216 | 
            +
                                message_history = message_history[-40:]
         | 
| 187 217 |  | 
| 188 218 | 
             
                            if agent_response and agent_response.awaiting_user_input:
         | 
| 189 219 | 
             
                                console.print(
         | 
| @@ -0,0 +1,71 @@ | |
| 1 | 
            +
            import json
         | 
| 2 | 
            +
            from pathlib import Path
         | 
| 3 | 
            +
            from datetime import datetime, timedelta
         | 
| 4 | 
            +
            from typing import Any, List, Dict, Optional
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            DEFAULT_MEMORY_PATH = Path('.puppy_session_memory.json')
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            class SessionMemory:
         | 
| 9 | 
            +
                """
         | 
| 10 | 
            +
                Simple persistent memory for Code Puppy agent sessions.
         | 
| 11 | 
            +
                Stores short histories of tasks, notes, user preferences, and watched files.
         | 
| 12 | 
            +
                """
         | 
| 13 | 
            +
                def __init__(self, storage_path: Path = DEFAULT_MEMORY_PATH, memory_limit: int = 128):
         | 
| 14 | 
            +
                    self.storage_path = storage_path
         | 
| 15 | 
            +
                    self.memory_limit = memory_limit
         | 
| 16 | 
            +
                    self._data = {
         | 
| 17 | 
            +
                        'history': [], # List of task/event dicts
         | 
| 18 | 
            +
                        'user_preferences': {},
         | 
| 19 | 
            +
                        'watched_files': [],
         | 
| 20 | 
            +
                    }
         | 
| 21 | 
            +
                    self._load()
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def _load(self):
         | 
| 24 | 
            +
                    if self.storage_path.exists():
         | 
| 25 | 
            +
                        try:
         | 
| 26 | 
            +
                            self._data = json.loads(self.storage_path.read_text())
         | 
| 27 | 
            +
                        except Exception:
         | 
| 28 | 
            +
                            self._data = {'history': [], 'user_preferences': {}, 'watched_files': []}
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def _save(self):
         | 
| 31 | 
            +
                    try:
         | 
| 32 | 
            +
                        self.storage_path.write_text(json.dumps(self._data, indent=2))
         | 
| 33 | 
            +
                    except Exception as e:
         | 
| 34 | 
            +
                        pass  # Don't crash the agent for memory fails
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def log_task(self, description: str, extras: Optional[Dict[str, Any]] = None):
         | 
| 37 | 
            +
                    entry = {
         | 
| 38 | 
            +
                        'timestamp': datetime.utcnow().isoformat(),
         | 
| 39 | 
            +
                        'description': description,
         | 
| 40 | 
            +
                    }
         | 
| 41 | 
            +
                    if extras:
         | 
| 42 | 
            +
                        entry.update(extras)
         | 
| 43 | 
            +
                    self._data['history'].append(entry)
         | 
| 44 | 
            +
                    # Trim memory
         | 
| 45 | 
            +
                    self._data['history'] = self._data['history'][-self.memory_limit:]
         | 
| 46 | 
            +
                    self._save()
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def get_history(self, within_minutes: Optional[int] = None) -> List[Dict[str, Any]]:
         | 
| 49 | 
            +
                    if not within_minutes:
         | 
| 50 | 
            +
                        return list(self._data['history'])
         | 
| 51 | 
            +
                    cutoff = datetime.utcnow() - timedelta(minutes=within_minutes)
         | 
| 52 | 
            +
                    return [h for h in self._data['history'] if datetime.fromisoformat(h['timestamp']) >= cutoff]
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def set_preference(self, key: str, value: Any):
         | 
| 55 | 
            +
                    self._data['user_preferences'][key] = value
         | 
| 56 | 
            +
                    self._save()
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def get_preference(self, key: str, default: Any = None) -> Any:
         | 
| 59 | 
            +
                    return self._data['user_preferences'].get(key, default)
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def add_watched_file(self, path: str):
         | 
| 62 | 
            +
                    if path not in self._data['watched_files']:
         | 
| 63 | 
            +
                        self._data['watched_files'].append(path)
         | 
| 64 | 
            +
                        self._save()
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def list_watched_files(self) -> List[str]:
         | 
| 67 | 
            +
                    return list(self._data['watched_files'])
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def clear(self):
         | 
| 70 | 
            +
                    self._data = {'history': [], 'user_preferences': {}, 'watched_files': []}
         | 
| 71 | 
            +
                    self._save()
         | 
    
        code_puppy/tools/__init__.py
    CHANGED
    
    | @@ -1,4 +1,11 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 1 | 
            +
            from code_puppy.tools.file_operations import register_file_operations_tools
         | 
| 2 | 
            +
            from code_puppy.tools.file_modifications import register_file_modifications_tools
         | 
| 3 | 
            +
            from code_puppy.tools.command_runner import register_command_runner_tools
         | 
| 4 | 
            +
            from code_puppy.tools.web_search import register_web_search_tools
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            def register_all_tools(agent):
         | 
| 7 | 
            +
                """Register all available tools to the provided agent."""
         | 
| 8 | 
            +
                register_file_operations_tools(agent)
         | 
| 9 | 
            +
                register_file_modifications_tools(agent)
         | 
| 10 | 
            +
                register_command_runner_tools(agent)
         | 
| 11 | 
            +
                register_web_search_tools(agent)
         | 
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
            import os
         | 
| 2 | 
            +
            import ast
         | 
| 3 | 
            +
            from typing import List, Tuple
         | 
| 4 | 
            +
            from rich.tree import Tree
         | 
| 5 | 
            +
            from rich.text import Text
         | 
| 6 | 
            +
            from pathlib import Path
         | 
| 7 | 
            +
            import pathspec
         | 
| 8 | 
            +
             | 
| 9 | 
            +
             | 
| 10 | 
            +
            def summarize_node(node: ast.AST) -> str:
         | 
| 11 | 
            +
                if isinstance(node, ast.ClassDef):
         | 
| 12 | 
            +
                    return f"class {node.name}"
         | 
| 13 | 
            +
                if isinstance(node, ast.FunctionDef):
         | 
| 14 | 
            +
                    return f"def {node.name}()"
         | 
| 15 | 
            +
                return ""
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
            def get_docstring(node: ast.AST) -> str:
         | 
| 19 | 
            +
                doc = ast.get_docstring(node)
         | 
| 20 | 
            +
                if doc:
         | 
| 21 | 
            +
                    lines = doc.strip().split("\n")
         | 
| 22 | 
            +
                    return lines[0] if lines else doc.strip()
         | 
| 23 | 
            +
                return ""
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
            def map_python_file(file_path: str, show_doc: bool = True) -> Tree:
         | 
| 27 | 
            +
                tree = Tree(Text(file_path, style="bold cyan"))
         | 
| 28 | 
            +
                with open(file_path, "r", encoding="utf-8") as f:
         | 
| 29 | 
            +
                    root = ast.parse(f.read(), filename=file_path)
         | 
| 30 | 
            +
                for node in root.body:
         | 
| 31 | 
            +
                    summary = summarize_node(node)
         | 
| 32 | 
            +
                    if summary:
         | 
| 33 | 
            +
                        t = Tree(summary)
         | 
| 34 | 
            +
                        if show_doc:
         | 
| 35 | 
            +
                            doc = get_docstring(node)
         | 
| 36 | 
            +
                            if doc:
         | 
| 37 | 
            +
                                t.add(Text(f'"{doc}"', style="dim"))
         | 
| 38 | 
            +
                        # Add inner functions
         | 
| 39 | 
            +
                        if hasattr(node, 'body'):
         | 
| 40 | 
            +
                            for subnode in getattr(node, 'body'):
         | 
| 41 | 
            +
                                subsum = summarize_node(subnode)
         | 
| 42 | 
            +
                                if subsum:
         | 
| 43 | 
            +
                                    sub_t = Tree(subsum)
         | 
| 44 | 
            +
                                    doc2 = get_docstring(subnode)
         | 
| 45 | 
            +
                                    if doc2:
         | 
| 46 | 
            +
                                        sub_t.add(Text(f'"{doc2}"', style="dim"))
         | 
| 47 | 
            +
                                    t.add(sub_t)
         | 
| 48 | 
            +
                        tree.add(t)
         | 
| 49 | 
            +
                return tree
         | 
| 50 | 
            +
             | 
| 51 | 
            +
             | 
| 52 | 
            +
            def load_gitignore(directory: str):
         | 
| 53 | 
            +
                gitignore_file = os.path.join(directory, '.gitignore')
         | 
| 54 | 
            +
                if os.path.exists(gitignore_file):
         | 
| 55 | 
            +
                    with open(gitignore_file, 'r') as f:
         | 
| 56 | 
            +
                        spec = pathspec.PathSpec.from_lines('gitwildmatch', f)
         | 
| 57 | 
            +
                    return spec
         | 
| 58 | 
            +
                else:
         | 
| 59 | 
            +
                    return pathspec.PathSpec.from_lines('gitwildmatch', [])
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            def make_code_map(directory: str, show_doc: bool = True) -> Tree:
         | 
| 62 | 
            +
                """
         | 
| 63 | 
            +
                Recursively build a Tree displaying the code structure of all .py files in a directory,
         | 
| 64 | 
            +
                ignoring files listed in .gitignore if present.
         | 
| 65 | 
            +
                """
         | 
| 66 | 
            +
                base_tree = Tree(Text(directory, style="bold magenta"))
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                spec = load_gitignore(directory)
         | 
| 69 | 
            +
                abs_directory = os.path.abspath(directory)
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                for root, dirs, files in os.walk(directory):
         | 
| 72 | 
            +
                    rel_root = os.path.relpath(root, abs_directory)
         | 
| 73 | 
            +
                    # Remove ignored directories in-place for os.walk to not descend
         | 
| 74 | 
            +
                    dirs[:] = [d for d in dirs if not spec.match_file(os.path.normpath(os.path.join(rel_root, d)))]
         | 
| 75 | 
            +
                    for fname in files:
         | 
| 76 | 
            +
                        rel_file = os.path.normpath(os.path.join(rel_root, fname))
         | 
| 77 | 
            +
                        if fname.endswith('.py') and not fname.startswith("__"):
         | 
| 78 | 
            +
                            if not spec.match_file(rel_file):
         | 
| 79 | 
            +
                                fpath = os.path.join(root, fname)
         | 
| 80 | 
            +
                                try:
         | 
| 81 | 
            +
                                    file_tree = map_python_file(fpath, show_doc=show_doc)
         | 
| 82 | 
            +
                                    base_tree.add(file_tree)
         | 
| 83 | 
            +
                                except Exception as e:
         | 
| 84 | 
            +
                                    err = Tree(Text(f"[error reading {fname}: {e}]", style="bold red"))
         | 
| 85 | 
            +
                                    base_tree.add(err)
         | 
| 86 | 
            +
                return base_tree
         | 
| @@ -4,207 +4,71 @@ import time | |
| 4 4 | 
             
            import os
         | 
| 5 5 | 
             
            from typing import Dict, Any
         | 
| 6 6 | 
             
            from code_puppy.tools.common import console
         | 
| 7 | 
            -
            from code_puppy.agent import code_generation_agent
         | 
| 8 7 | 
             
            from pydantic_ai import RunContext
         | 
| 9 8 | 
             
            from rich.markdown import Markdown
         | 
| 10 9 | 
             
            from rich.syntax import Syntax
         | 
| 11 10 |  | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
                Returns:
         | 
| 31 | 
            -
                    A dictionary with the command result, including stdout, stderr, and exit code.
         | 
| 32 | 
            -
                """
         | 
| 33 | 
            -
                if not command or not command.strip():
         | 
| 34 | 
            -
                    console.print("[bold red]Error:[/bold red] Command cannot be empty")
         | 
| 35 | 
            -
                    return {"error": "Command cannot be empty"}
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                # Display command execution in a visually distinct way
         | 
| 38 | 
            -
                console.print("\n[bold white on blue] SHELL COMMAND [/bold white on blue]")
         | 
| 39 | 
            -
                console.print(f"[bold green]$ {command}[/bold green]")
         | 
| 40 | 
            -
                if cwd:
         | 
| 41 | 
            -
                    console.print(f"[dim]Working directory: {cwd}[/dim]")
         | 
| 42 | 
            -
                console.print("[dim]" + "-" * 60 + "[/dim]")
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                # Check for YOLO_MODE environment variable to bypass safety check
         | 
| 45 | 
            -
                yolo_mode = os.getenv("YOLO_MODE", "false").lower() == "true"
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                if not yolo_mode:
         | 
| 48 | 
            -
                    # Prompt user for confirmation before running the command
         | 
| 49 | 
            -
                    user_input = input("Are you sure you want to run this command? (yes/no): ")
         | 
| 50 | 
            -
                    if user_input.strip().lower() not in {"yes", "y"}:
         | 
| 51 | 
            -
                        console.print(
         | 
| 52 | 
            -
                            "[bold yellow]Command execution canceled by user.[/bold yellow]"
         | 
| 53 | 
            -
                        )
         | 
| 54 | 
            -
                        return {
         | 
| 55 | 
            -
                            "success": False,
         | 
| 56 | 
            -
                            "command": command,
         | 
| 57 | 
            -
                            "error": "User canceled command execution",
         | 
| 58 | 
            -
                        }
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                try:
         | 
| 61 | 
            -
                    start_time = time.time()
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                    # Execute the command with timeout
         | 
| 64 | 
            -
                    process = subprocess.Popen(
         | 
| 65 | 
            -
                        command,
         | 
| 66 | 
            -
                        shell=True,
         | 
| 67 | 
            -
                        stdout=subprocess.PIPE,
         | 
| 68 | 
            -
                        stderr=subprocess.PIPE,
         | 
| 69 | 
            -
                        text=True,
         | 
| 70 | 
            -
                        cwd=cwd,
         | 
| 71 | 
            -
                    )
         | 
| 72 | 
            -
             | 
| 11 | 
            +
            def register_command_runner_tools(agent):
         | 
| 12 | 
            +
                @agent.tool
         | 
| 13 | 
            +
                def run_shell_command(context: RunContext, command: str, cwd: str = None, timeout: int = 60) -> Dict[str, Any]:
         | 
| 14 | 
            +
                    if not command or not command.strip():
         | 
| 15 | 
            +
                        console.print("[bold red]Error:[/bold red] Command cannot be empty")
         | 
| 16 | 
            +
                        return {"error": "Command cannot be empty"}
         | 
| 17 | 
            +
                    console.print("\n[bold white on blue] SHELL COMMAND [/bold white on blue]")
         | 
| 18 | 
            +
                    console.print(f"[bold green]$ {command}[/bold green]")
         | 
| 19 | 
            +
                    if cwd:
         | 
| 20 | 
            +
                        console.print(f"[dim]Working directory: {cwd}[/dim]")
         | 
| 21 | 
            +
                    console.print("[dim]" + "-" * 60 + "[/dim]")
         | 
| 22 | 
            +
                    yolo_mode = os.getenv("YOLO_MODE", "false").lower() == "true"
         | 
| 23 | 
            +
                    if not yolo_mode:
         | 
| 24 | 
            +
                        user_input = input("Are you sure you want to run this command? (yes/no): ")
         | 
| 25 | 
            +
                        if user_input.strip().lower() not in {"yes", "y"}:
         | 
| 26 | 
            +
                            console.print("[bold yellow]Command execution canceled by user.[/bold yellow]")
         | 
| 27 | 
            +
                            return {"success": False, "command": command, "error": "User canceled command execution"}
         | 
| 73 28 | 
             
                    try:
         | 
| 74 | 
            -
                         | 
| 75 | 
            -
                         | 
| 76 | 
            -
                         | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
                             | 
| 81 | 
            -
             | 
| 82 | 
            -
                                Syntax(
         | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
                                )
         | 
| 88 | 
            -
                             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
                             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
                                )
         | 
| 99 | 
            -
                            )
         | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
                            console.print(
         | 
| 104 | 
            -
             | 
| 105 | 
            -
             | 
| 106 | 
            -
                         | 
| 107 | 
            -
                            console.print(
         | 
| 108 | 
            -
                                f"[bold red]✗ Command failed with exit code {exit_code}[/bold red] [dim](took {execution_time:.2f}s)[/dim]"
         | 
| 109 | 
            -
                            )
         | 
| 110 | 
            -
             | 
| 29 | 
            +
                        start_time = time.time()
         | 
| 30 | 
            +
                        process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd)
         | 
| 31 | 
            +
                        try:
         | 
| 32 | 
            +
                            stdout, stderr = process.communicate(timeout=timeout)
         | 
| 33 | 
            +
                            exit_code = process.returncode
         | 
| 34 | 
            +
                            execution_time = time.time() - start_time
         | 
| 35 | 
            +
                            if stdout.strip():
         | 
| 36 | 
            +
                                console.print("[bold white]STDOUT:[/bold white]")
         | 
| 37 | 
            +
                                console.print(Syntax(stdout.strip(), "bash", theme="monokai", background_color="default"))
         | 
| 38 | 
            +
                            if stderr.strip():
         | 
| 39 | 
            +
                                console.print("[bold yellow]STDERR:[/bold yellow]")
         | 
| 40 | 
            +
                                console.print(Syntax(stderr.strip(), "bash", theme="monokai", background_color="default"))
         | 
| 41 | 
            +
                            if exit_code == 0:
         | 
| 42 | 
            +
                                console.print(f"[bold green]✓ Command completed successfully[/bold green] [dim](took {execution_time:.2f}s)[/dim]")
         | 
| 43 | 
            +
                            else:
         | 
| 44 | 
            +
                                console.print(f"[bold red]✗ Command failed with exit code {exit_code}[/bold red] [dim](took {execution_time:.2f}s)[/dim]")
         | 
| 45 | 
            +
                            console.print("[dim]" + "-" * 60 + "[/dim]\n")
         | 
| 46 | 
            +
                            return {"success": exit_code == 0, "command": command, "stdout": stdout, "stderr": stderr, "exit_code": exit_code, "execution_time": execution_time, "timeout": False}
         | 
| 47 | 
            +
                        except subprocess.TimeoutExpired:
         | 
| 48 | 
            +
                            process.kill()
         | 
| 49 | 
            +
                            stdout, stderr = process.communicate()
         | 
| 50 | 
            +
                            execution_time = time.time() - start_time
         | 
| 51 | 
            +
                            if stdout.strip():
         | 
| 52 | 
            +
                                console.print("[bold white]STDOUT (incomplete due to timeout):[/bold white]")
         | 
| 53 | 
            +
                                console.print(Syntax(stdout.strip(), "bash", theme="monokai", background_color="default"))
         | 
| 54 | 
            +
                            if stderr.strip():
         | 
| 55 | 
            +
                                console.print("[bold yellow]STDERR:[/bold yellow]")
         | 
| 56 | 
            +
                                console.print(Syntax(stderr.strip(), "bash", theme="monokai", background_color="default"))
         | 
| 57 | 
            +
                            console.print(f"[bold red]⏱ Command timed out after {timeout} seconds[/bold red] [dim](ran for {execution_time:.2f}s)[/dim]")
         | 
| 58 | 
            +
                            console.print("[dim]" + "-" * 60 + "[/dim]\n")
         | 
| 59 | 
            +
                            return {"success": False,"command": command, "stdout": stdout[-1000:], "stderr": stderr[-1000:], "exit_code": None, "execution_time": execution_time, "timeout": True, "error": f"Command timed out after {timeout} seconds"}
         | 
| 60 | 
            +
                    except Exception as e:
         | 
| 61 | 
            +
                        console.print_exception(show_locals=True)
         | 
| 111 62 | 
             
                        console.print("[dim]" + "-" * 60 + "[/dim]\n")
         | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
                         | 
| 122 | 
            -
                    except subprocess.TimeoutExpired:
         | 
| 123 | 
            -
                        # Kill the process if it times out
         | 
| 124 | 
            -
                        process.kill()
         | 
| 125 | 
            -
                        stdout, stderr = process.communicate()
         | 
| 126 | 
            -
                        execution_time = time.time() - start_time
         | 
| 127 | 
            -
             | 
| 128 | 
            -
                        # Display timeout information
         | 
| 129 | 
            -
                        if stdout.strip():
         | 
| 130 | 
            -
                            console.print(
         | 
| 131 | 
            -
                                "[bold white]STDOUT (incomplete due to timeout):[/bold white]"
         | 
| 132 | 
            -
                            )
         | 
| 133 | 
            -
                            console.print(
         | 
| 134 | 
            -
                                Syntax(
         | 
| 135 | 
            -
                                    stdout.strip(),
         | 
| 136 | 
            -
                                    "bash",
         | 
| 137 | 
            -
                                    theme="monokai",
         | 
| 138 | 
            -
                                    background_color="default",
         | 
| 139 | 
            -
                                )
         | 
| 140 | 
            -
                            )
         | 
| 141 | 
            -
             | 
| 142 | 
            -
                        if stderr.strip():
         | 
| 143 | 
            -
                            console.print("[bold yellow]STDERR:[/bold yellow]")
         | 
| 144 | 
            -
                            console.print(
         | 
| 145 | 
            -
                                Syntax(
         | 
| 146 | 
            -
                                    stderr.strip(),
         | 
| 147 | 
            -
                                    "bash",
         | 
| 148 | 
            -
                                    theme="monokai",
         | 
| 149 | 
            -
                                    background_color="default",
         | 
| 150 | 
            -
                                )
         | 
| 151 | 
            -
                            )
         | 
| 152 | 
            -
             | 
| 153 | 
            -
                        console.print(
         | 
| 154 | 
            -
                            f"[bold red]⏱ Command timed out after {timeout} seconds[/bold red] [dim](ran for {execution_time:.2f}s)[/dim]"
         | 
| 155 | 
            -
                        )
         | 
| 156 | 
            -
                        console.print("[dim]" + "-" * 60 + "[/dim]\n")
         | 
| 157 | 
            -
             | 
| 158 | 
            -
                        return {
         | 
| 159 | 
            -
                            "success": False,
         | 
| 160 | 
            -
                            "command": command,
         | 
| 161 | 
            -
                            "stdout": stdout[-1000:],
         | 
| 162 | 
            -
                            "stderr": stderr[-1000:],
         | 
| 163 | 
            -
                            "exit_code": None,  # No exit code since the process was killed
         | 
| 164 | 
            -
                            "execution_time": execution_time,
         | 
| 165 | 
            -
                            "timeout": True,
         | 
| 166 | 
            -
                            "error": f"Command timed out after {timeout} seconds",
         | 
| 167 | 
            -
                        }
         | 
| 168 | 
            -
                except Exception as e:
         | 
| 169 | 
            -
                    # Display error information
         | 
| 170 | 
            -
                    console.print_exception(show_locals=True)
         | 
| 63 | 
            +
                        return {"success": False, "command": command, "error": f"Error executing command: {str(e)}", "stdout": "", "stderr": "", "exit_code": -1, "timeout": False}
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                @agent.tool
         | 
| 66 | 
            +
                def share_your_reasoning(context: RunContext, reasoning: str, next_steps: str = None) -> Dict[str, Any]:
         | 
| 67 | 
            +
                    console.print("\n[bold white on purple] AGENT REASONING [/bold white on purple]")
         | 
| 68 | 
            +
                    console.print("[bold cyan]Current reasoning:[/bold cyan]")
         | 
| 69 | 
            +
                    console.print(Markdown(reasoning))
         | 
| 70 | 
            +
                    if next_steps and next_steps.strip():
         | 
| 71 | 
            +
                        console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
         | 
| 72 | 
            +
                        console.print(Markdown(next_steps))
         | 
| 171 73 | 
             
                    console.print("[dim]" + "-" * 60 + "[/dim]\n")
         | 
| 172 | 
            -
             | 
| 173 | 
            -
                    return {
         | 
| 174 | 
            -
                        "success": False,
         | 
| 175 | 
            -
                        "command": command,
         | 
| 176 | 
            -
                        "error": f"Error executing command: {str(e)}",
         | 
| 177 | 
            -
                        "stdout": "",
         | 
| 178 | 
            -
                        "stderr": "",
         | 
| 179 | 
            -
                        "exit_code": -1,
         | 
| 180 | 
            -
                        "timeout": False,
         | 
| 181 | 
            -
                    }
         | 
| 182 | 
            -
             | 
| 183 | 
            -
             | 
| 184 | 
            -
            @code_generation_agent.tool
         | 
| 185 | 
            -
            def share_your_reasoning(
         | 
| 186 | 
            -
                context: RunContext, reasoning: str, next_steps: str = None
         | 
| 187 | 
            -
            ) -> Dict[str, Any]:
         | 
| 188 | 
            -
                """Share the agent's current reasoning and planned next steps with the user.
         | 
| 189 | 
            -
             | 
| 190 | 
            -
                Args:
         | 
| 191 | 
            -
                    reasoning: The agent's current reasoning or thought process.
         | 
| 192 | 
            -
                    next_steps: Optional description of what the agent plans to do next.
         | 
| 193 | 
            -
             | 
| 194 | 
            -
                Returns:
         | 
| 195 | 
            -
                    A dictionary with the reasoning information.
         | 
| 196 | 
            -
                """
         | 
| 197 | 
            -
                console.print("\n[bold white on purple] AGENT REASONING [/bold white on purple]")
         | 
| 198 | 
            -
             | 
| 199 | 
            -
                # Display the reasoning with markdown formatting
         | 
| 200 | 
            -
                console.print("[bold cyan]Current reasoning:[/bold cyan]")
         | 
| 201 | 
            -
                console.print(Markdown(reasoning))
         | 
| 202 | 
            -
             | 
| 203 | 
            -
                # Display next steps if provided
         | 
| 204 | 
            -
                if next_steps and next_steps.strip():
         | 
| 205 | 
            -
                    console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
         | 
| 206 | 
            -
                    console.print(Markdown(next_steps))
         | 
| 207 | 
            -
             | 
| 208 | 
            -
                console.print("[dim]" + "-" * 60 + "[/dim]\n")
         | 
| 209 | 
            -
             | 
| 210 | 
            -
                return {"success": True, "reasoning": reasoning, "next_steps": next_steps}
         | 
| 74 | 
            +
                    return {"success": True, "reasoning": reasoning, "next_steps": next_steps}
         | 
    
        code_puppy/tools/common.py
    CHANGED