code-puppy 0.0.2__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_agent/__init__.py ADDED
File without changes
code_agent/agent.py ADDED
@@ -0,0 +1,19 @@
1
+ import os
2
+ import pydantic
3
+ from pydantic_ai import Agent
4
+ from code_agent.agent_prompts import SYSTEM_PROMPT
5
+
6
+ # Check if we have a valid API key
7
+ api_key = os.environ.get("OPENAI_API_KEY", "")
8
+
9
+ class AgentResponse(pydantic.BaseModel):
10
+ """Represents a response from the agent."""
11
+ output_message: str = pydantic.Field(..., description="The final output message to display to the user")
12
+ awaiting_user_input: bool = pydantic.Field(False, description="True if user input is needed to continue the task")
13
+
14
+ # Create agent with tool usage explicitly enabled
15
+ code_generation_agent = Agent(
16
+ model='openai:gpt-4.1-mini',
17
+ system_prompt=SYSTEM_PROMPT,
18
+ output_type=AgentResponse,
19
+ )
@@ -0,0 +1,52 @@
1
+ SYSTEM_PROMPT = """
2
+ You are a code-agent assistant with the ability to use tools to help users complete coding tasks. You MUST use the provided tools to write, modify, and execute code rather than just describing what to do.
3
+
4
+ Be super informal - we're here to have fun. Writing software is super fun. Don't be scared of being a little bit sarcastic too.
5
+ Be very pedantic about code principles like DRY, YAGNI, and SOLID.
6
+ Be super pedantic about code quality and best practices.
7
+ Be fun and playful. Don't be too serious.
8
+
9
+ Individual files should be very short and concise, at most around 250 lines if possible. If they get longer,
10
+ consider refactoring the code and splitting it into multiple files.
11
+
12
+ Always obey the Zen of Python, even if you are not writing Python code.
13
+
14
+ When given a coding task:
15
+ 1. Analyze the requirements carefully
16
+ 2. Execute the plan by using appropriate tools
17
+ 3. Provide clear explanations for your implementation choices
18
+ 4. Continue autonomously whenever possible to achieve the task.
19
+
20
+ YOU MUST USE THESE TOOLS to complete tasks (do not just describe what should be done - actually do it):
21
+
22
+ File Operations:
23
+ - list_files(directory=".", recursive=True): ALWAYS use this to explore directories before trying to read/modify files
24
+ - read_file(file_path, start_line=0, end_line=None): ALWAYS use this to read existing files before modifying them. Don't read less than 500 lines at a time.
25
+ - create_file(file_path, content=""): Use this to create new files with content
26
+ - modify_file(file_path, proposed_changes, replace_content): Use this to replace specific content in files
27
+ - delete_snippet_from_file(file_path, snippet): Use this to remove specific code snippets from files
28
+ - delete_file(file_path): Use this to remove files when needed
29
+
30
+ System Operations:
31
+ - run_shell_command(command, cwd=None, timeout=60): Use this to execute commands, run tests, or start services
32
+ - web_search(query): Use this to search the web for information
33
+ - web_crawl(url): Use this to crawl a website for information
34
+
35
+ Reasoning & Explanation:
36
+ - share_your_reasoning(reasoning, next_steps=None): Use this to explicitly share your thought process and planned next steps
37
+
38
+ Important rules:
39
+ - You MUST use tools to accomplish tasks - DO NOT just output code or descriptions
40
+ - Before every other tool use, you must use "share_your_reasoning" to explain your thought process and planned next steps
41
+ - Check if files exist before trying to modify or delete them
42
+ - After using system operations tools, always explain the results
43
+ - You're encouraged to loop between share_your_reasoning, file tools, and run_shell_command to test output in order to write programs
44
+ - Aim to continue operations independently unless user input is definitively required.
45
+
46
+ Your solutions should be production-ready, maintainable, and follow best practices for the chosen language.
47
+
48
+ Return your final response as a structured output having the following fields:
49
+ * output_message: The final output message to display to the user
50
+ * awaiting_user_input: True if user input is needed to continue the task. If you get an error, you might consider asking the user for help.
51
+
52
+ """
code_agent/main.py ADDED
@@ -0,0 +1,234 @@
1
+ import asyncio
2
+ import argparse
3
+ import os
4
+ import readline
5
+ from dotenv import load_dotenv
6
+ from rich.console import Console
7
+ from rich.markdown import Markdown
8
+ from rich.console import ConsoleOptions, RenderResult
9
+ from rich.markdown import CodeBlock
10
+ from rich.text import Text
11
+ from rich.syntax import Syntax
12
+
13
+ # Initialize rich console for pretty output
14
+ from code_agent.tools.common import console
15
+ from code_agent.agent import code_generation_agent
16
+
17
+
18
+ # Define a function to get the secret file path
19
+ def get_secret_file_path():
20
+ hidden_directory = os.path.join(os.path.expanduser("~"), ".agent_secret")
21
+ if not os.path.exists(hidden_directory):
22
+ os.makedirs(hidden_directory)
23
+ return os.path.join(hidden_directory, "history.txt")
24
+
25
+
26
+ async def main():
27
+ global shutdown_flag
28
+
29
+ # Load environment variables from .env file
30
+ load_dotenv()
31
+
32
+ # Set up argument parser
33
+ parser = argparse.ArgumentParser(
34
+ description="Code Generation Agent - Similar to Windsurf or Cursor"
35
+ )
36
+ parser.add_argument(
37
+ "--interactive", "-i", action="store_true", help="Run in interactive mode"
38
+ )
39
+ parser.add_argument("command", nargs="*", help="Run a single command")
40
+ args = parser.parse_args()
41
+
42
+ history_file_path = get_secret_file_path()
43
+
44
+ if args.command:
45
+ # Join the list of command arguments into a single string command
46
+ command = " ".join(args.command)
47
+ try:
48
+ while not shutdown_flag:
49
+ response = await code_generation_agent.run(command)
50
+ console.print(response.output_message)
51
+ if response.awaiting_user_input:
52
+ console.print(
53
+ "[bold red]The agent requires further input. Interactive mode is recommended for such tasks."
54
+ )
55
+ except AttributeError as e:
56
+ console.print(f"[bold red]AttributeError:[/bold red] {str(e)}")
57
+ console.print(
58
+ "[bold yellow]\u26a0 The response might not be in the expected format, missing attributes like 'output_message'."
59
+ )
60
+ except Exception as e:
61
+ console.print(f"[bold red]Unexpected Error:[/bold red] {str(e)}")
62
+ elif args.interactive:
63
+ await interactive_mode(history_file_path)
64
+ else:
65
+ parser.print_help()
66
+
67
+
68
+ # Add the file handling functionality for interactive mode
69
+ async def interactive_mode(history_file_path: str) -> None:
70
+ """Run the agent in interactive mode."""
71
+ console.print("[bold green]Code Generation Agent[/bold green] - Interactive Mode")
72
+ console.print("Type 'exit' or 'quit' to exit the interactive mode.")
73
+ console.print("Type 'clear' to reset the conversation history.")
74
+
75
+ message_history = []
76
+
77
+ # Set up readline history file in home directory
78
+ history_file = os.path.expanduser("~/.code_agent_history.txt")
79
+ history_dir = os.path.dirname(history_file)
80
+
81
+ # Ensure history directory exists
82
+ if history_dir and not os.path.exists(history_dir):
83
+ try:
84
+ os.makedirs(history_dir, exist_ok=True)
85
+ except Exception as e:
86
+ console.print(
87
+ f"[yellow]Warning: Could not create history directory: {e}[/yellow]"
88
+ )
89
+
90
+ # Try to read history file
91
+ try:
92
+ if os.path.exists(history_file):
93
+ readline.read_history_file(history_file)
94
+ except (FileNotFoundError, OSError) as e:
95
+ console.print(f"[yellow]Warning: Could not read history file: {e}[/yellow]")
96
+
97
+ readline.set_history_length(100)
98
+
99
+ while True:
100
+ console.print("[bold blue]Enter your coding task:[/bold blue]")
101
+
102
+ try:
103
+ # Simple single-line input
104
+ task = input(">>> ")
105
+
106
+ # Add to readline history if not empty
107
+ if task.strip():
108
+ readline.add_history(task)
109
+
110
+ # Save history
111
+ try:
112
+ readline.write_history_file(history_file)
113
+ except Exception as e:
114
+ console.print(
115
+ f"[yellow]Warning: Could not write history file: {e}[/yellow]"
116
+ )
117
+
118
+ except (KeyboardInterrupt, EOFError):
119
+ # Handle Ctrl+C or Ctrl+D
120
+ console.print("\n[yellow]Input cancelled[/yellow]")
121
+ continue
122
+
123
+ # Check for exit commands
124
+ if task.strip().lower() in ["exit", "quit"]:
125
+ console.print("[bold green]Goodbye![/bold green]")
126
+ break
127
+
128
+ # Check for clear command
129
+ if task.strip().lower() == "clear":
130
+ message_history = []
131
+ console.print("[bold yellow]Conversation history cleared![/bold yellow]")
132
+ console.print(
133
+ "[dim]The agent will not remember previous interactions.[/dim]\n"
134
+ )
135
+ continue
136
+
137
+ if task.strip():
138
+ console.print(f"\n[bold blue]Processing task:[/bold blue] {task}\n")
139
+
140
+ # Write to the secret file for permanent history
141
+ with open(history_file_path, "a") as history_file:
142
+ history_file.write(f"{task}\n")
143
+
144
+ # Counter for consecutive auto-continue invocations
145
+ auto_continue_count = 0
146
+ max_auto_continues = 10
147
+ is_done = False
148
+
149
+ while not is_done and auto_continue_count <= max_auto_continues:
150
+ try:
151
+ prettier_code_blocks()
152
+
153
+ # Only show "asking" message for initial query or if not auto-continuing
154
+ if auto_continue_count == 0:
155
+ console.log(f"Asking: {task}...", style="cyan")
156
+ else:
157
+ console.log(
158
+ f"Auto-continuing ({auto_continue_count}/{max_auto_continues})...",
159
+ style="cyan",
160
+ )
161
+
162
+ # Store agent's full response
163
+ agent_response = None
164
+
165
+ result = await code_generation_agent.run(
166
+ task, message_history=message_history
167
+ )
168
+ # Get the structured response
169
+ agent_response = result.output
170
+ console.print(agent_response.output_message)
171
+
172
+ # Update message history with all messages from this interaction
173
+ message_history = result.new_messages()
174
+ if agent_response:
175
+ # Check if the agent needs user input
176
+ if agent_response.awaiting_user_input:
177
+ console.print(
178
+ "\n[bold yellow]\u26a0 Agent needs your input to continue.[/bold yellow]"
179
+ )
180
+ is_done = True # Exit the loop to get user input
181
+ # Otherwise, auto-continue if we haven't reached the limit
182
+ elif auto_continue_count < max_auto_continues:
183
+ auto_continue_count += 1
184
+ task = "please continue"
185
+ console.print(
186
+ "\n[yellow]Agent continuing automatically...[/yellow]"
187
+ )
188
+ else:
189
+ # Reached max auto-continues
190
+ console.print(
191
+ f"\n[bold yellow]\u26a0 Reached maximum of {max_auto_continues} automatic continuations.[/bold yellow]"
192
+ )
193
+ console.print(
194
+ "[dim]You can enter a new request or type 'please continue' to resume.[/dim]"
195
+ )
196
+ is_done = True
197
+
198
+ # Show context status
199
+ console.print(
200
+ f"[dim]Context: {len(message_history)} messages in history[/dim]\n"
201
+ )
202
+
203
+ except Exception:
204
+ console.print_exception(show_locals=True)
205
+ is_done = True
206
+
207
+
208
+ def prettier_code_blocks():
209
+ class SimpleCodeBlock(CodeBlock):
210
+ def __rich_console__(
211
+ self, console: Console, options: ConsoleOptions
212
+ ) -> RenderResult:
213
+ code = str(self.text).rstrip()
214
+ yield Text(self.lexer_name, style="dim")
215
+ syntax = Syntax(
216
+ code,
217
+ self.lexer_name,
218
+ theme=self.theme,
219
+ background_color="default",
220
+ line_numbers=True,
221
+ )
222
+ yield syntax
223
+ yield Text(f"/{self.lexer_name}", style="dim")
224
+
225
+ Markdown.elements["fence"] = SimpleCodeBlock
226
+
227
+
228
+ def main_entry():
229
+ """Entry point for the installed CLI tool."""
230
+ asyncio.run(main())
231
+
232
+
233
+ if __name__ == "__main__":
234
+ main_entry()
@@ -0,0 +1,4 @@
1
+ """Code models package."""
2
+
3
+ # Import models from codesnippet.py
4
+ from code_agent.models.codesnippet import CodeSnippet, CodeResponse
@@ -0,0 +1,20 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional, List
3
+
4
+
5
+ class CodeSnippet(BaseModel):
6
+ """Model representing a code snippet with explanation."""
7
+
8
+ language: str
9
+ code: str
10
+ explanation: Optional[str] = None
11
+ imports: Optional[List[str]] = None
12
+
13
+
14
+ class CodeResponse(BaseModel):
15
+ """Model representing a response with code snippets and explanation."""
16
+
17
+ snippets: List[CodeSnippet]
18
+ overall_explanation: Optional[str] = None
19
+ success: bool = True
20
+ error_message: Optional[str] = None
@@ -0,0 +1,4 @@
1
+ import code_agent.tools.file_modifications
2
+ import code_agent.tools.file_operations
3
+ import code_agent.tools.command_runner
4
+ import code_agent.tools.web_search
@@ -0,0 +1,187 @@
1
+ # command_runner.py
2
+ import subprocess
3
+ import time
4
+ from typing import Dict, Any
5
+ from code_agent.tools.common import console
6
+ from code_agent.agent import code_generation_agent
7
+ from pydantic_ai import RunContext
8
+ from rich.markdown import Markdown
9
+ from rich.syntax import Syntax
10
+
11
+
12
+ @code_generation_agent.tool
13
+ def run_shell_command(
14
+ context: RunContext, command: str, cwd: str = None, timeout: int = 60
15
+ ) -> Dict[str, Any]:
16
+ """Run a shell command and return its output.
17
+
18
+ Args:
19
+ command: The shell command to execute.
20
+ cwd: The current working directory to run the command in. Defaults to None (current directory).
21
+ timeout: Maximum time in seconds to wait for the command to complete. Defaults to 60.
22
+
23
+ Returns:
24
+ A dictionary with the command result, including stdout, stderr, and exit code.
25
+ """
26
+ if not command or not command.strip():
27
+ console.print("[bold red]Error:[/bold red] Command cannot be empty")
28
+ return {"error": "Command cannot be empty"}
29
+
30
+ # Display command execution in a visually distinct way
31
+ console.print("\n[bold white on blue] SHELL COMMAND [/bold white on blue]")
32
+ console.print(f"[bold green]$ {command}[/bold green]")
33
+ if cwd:
34
+ console.print(f"[dim]Working directory: {cwd}[/dim]")
35
+ console.print("[dim]" + "-" * 60 + "[/dim]")
36
+
37
+ try:
38
+ start_time = time.time()
39
+
40
+ # Execute the command with timeout
41
+ process = subprocess.Popen(
42
+ command,
43
+ shell=True,
44
+ stdout=subprocess.PIPE,
45
+ stderr=subprocess.PIPE,
46
+ text=True,
47
+ cwd=cwd,
48
+ )
49
+
50
+ try:
51
+ stdout, stderr = process.communicate(timeout=timeout)
52
+ exit_code = process.returncode
53
+ execution_time = time.time() - start_time
54
+
55
+ # Display command output
56
+ if stdout.strip():
57
+ console.print("[bold white]STDOUT:[/bold white]")
58
+ console.print(
59
+ Syntax(
60
+ stdout.strip(),
61
+ "bash",
62
+ theme="monokai",
63
+ background_color="default",
64
+ )
65
+ )
66
+
67
+ if stderr.strip():
68
+ console.print("[bold yellow]STDERR:[/bold yellow]")
69
+ console.print(
70
+ Syntax(
71
+ stderr.strip(),
72
+ "bash",
73
+ theme="monokai",
74
+ background_color="default",
75
+ )
76
+ )
77
+
78
+ # Show execution summary
79
+ if exit_code == 0:
80
+ console.print(
81
+ f"[bold green]✓ Command completed successfully[/bold green] [dim](took {execution_time:.2f}s)[/dim]"
82
+ )
83
+ else:
84
+ console.print(
85
+ f"[bold red]✗ Command failed with exit code {exit_code}[/bold red] [dim](took {execution_time:.2f}s)[/dim]"
86
+ )
87
+
88
+ console.print("[dim]" + "-" * 60 + "[/dim]\n")
89
+
90
+ return {
91
+ "success": exit_code == 0,
92
+ "command": command,
93
+ "stdout": stdout,
94
+ "stderr": stderr,
95
+ "exit_code": exit_code,
96
+ "execution_time": execution_time,
97
+ "timeout": False,
98
+ }
99
+ except subprocess.TimeoutExpired:
100
+ # Kill the process if it times out
101
+ process.kill()
102
+ stdout, stderr = process.communicate()
103
+ execution_time = time.time() - start_time
104
+
105
+ # Display timeout information
106
+ if stdout.strip():
107
+ console.print(
108
+ "[bold white]STDOUT (incomplete due to timeout):[/bold white]"
109
+ )
110
+ console.print(
111
+ Syntax(
112
+ stdout.strip(),
113
+ "bash",
114
+ theme="monokai",
115
+ background_color="default",
116
+ )
117
+ )
118
+
119
+ if stderr.strip():
120
+ console.print("[bold yellow]STDERR:[/bold yellow]")
121
+ console.print(
122
+ Syntax(
123
+ stderr.strip(),
124
+ "bash",
125
+ theme="monokai",
126
+ background_color="default",
127
+ )
128
+ )
129
+
130
+ console.print(
131
+ f"[bold red]⏱ Command timed out after {timeout} seconds[/bold red] [dim](ran for {execution_time:.2f}s)[/dim]"
132
+ )
133
+ console.print("[dim]" + "-" * 60 + "[/dim]\n")
134
+
135
+ return {
136
+ "success": False,
137
+ "command": command,
138
+ "stdout": stdout,
139
+ "stderr": stderr,
140
+ "exit_code": None, # No exit code since the process was killed
141
+ "execution_time": execution_time,
142
+ "timeout": True,
143
+ "error": f"Command timed out after {timeout} seconds",
144
+ }
145
+ except Exception as e:
146
+ # Display error information
147
+ console.print_exception(show_locals=True)
148
+ console.print("[dim]" + "-" * 60 + "[/dim]\n")
149
+
150
+ return {
151
+ "success": False,
152
+ "command": command,
153
+ "error": f"Error executing command: {str(e)}",
154
+ "stdout": "",
155
+ "stderr": "",
156
+ "exit_code": -1,
157
+ "timeout": False,
158
+ }
159
+
160
+
161
+ @code_generation_agent.tool
162
+ def share_your_reasoning(
163
+ context: RunContext, reasoning: str, next_steps: str = None
164
+ ) -> Dict[str, Any]:
165
+ """Share the agent's current reasoning and planned next steps with the user.
166
+
167
+ Args:
168
+ reasoning: The agent's current reasoning or thought process.
169
+ next_steps: Optional description of what the agent plans to do next.
170
+
171
+ Returns:
172
+ A dictionary with the reasoning information.
173
+ """
174
+ console.print("\n[bold white on purple] AGENT REASONING [/bold white on purple]")
175
+
176
+ # Display the reasoning with markdown formatting
177
+ console.print("[bold cyan]Current reasoning:[/bold cyan]")
178
+ console.print(Markdown(reasoning))
179
+
180
+ # Display next steps if provided
181
+ if next_steps and next_steps.strip():
182
+ console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
183
+ console.print(Markdown(next_steps))
184
+
185
+ console.print("[dim]" + "-" * 60 + "[/dim]\n")
186
+
187
+ return {"success": True, "reasoning": reasoning, "next_steps": next_steps}
@@ -0,0 +1,3 @@
1
+ from rich.console import Console
2
+
3
+ console = Console()
@@ -0,0 +1,264 @@
1
+ # file_modifications.py
2
+ import os
3
+ import difflib
4
+ from code_agent.tools.common import console
5
+ from typing import Dict, Any, Optional
6
+ from code_agent.agent import code_generation_agent
7
+ from pydantic_ai import RunContext
8
+
9
+
10
+ import os
11
+ import difflib
12
+ from code_agent.tools.common import console
13
+ from typing import Dict, Any, Optional
14
+ from code_agent.agent import code_generation_agent
15
+ from pydantic_ai import RunContext
16
+
17
+ @code_generation_agent.tool
18
+ def modify_file(
19
+ context: RunContext,
20
+ file_path: str,
21
+ proposed_changes: str,
22
+ replace_content: str,
23
+ overwrite_entire_file: bool = False
24
+ ) -> Dict[str, Any]:
25
+ """Modify a file with proposed changes, generating a diff and applying the changes.
26
+
27
+ Args:
28
+ file_path: Path of the file to modify.
29
+ proposed_changes: The new content to replace the targeted section or entire file content.
30
+ replace_content: The content to replace. If blank or not present in the file, the whole file will be replaced ONLY if overwrite_entire_file is True.
31
+ overwrite_entire_file: Explicitly allow replacing the entire file content (default False). You MUST supply True to allow this.
32
+
33
+ Returns:
34
+ A dictionary with the operation result, including success status, message, and diff.
35
+ """
36
+ file_path = os.path.abspath(file_path)
37
+
38
+ console.print("\n[bold white on yellow] FILE MODIFICATION [/bold white on yellow]")
39
+ console.print(f"[bold yellow]Modifying:[/bold yellow] {file_path}")
40
+
41
+ try:
42
+ # Check if the file exists
43
+ if not os.path.exists(file_path):
44
+ console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
45
+ return {"error": f"File '{file_path}' does not exist"}
46
+
47
+ if not os.path.isfile(file_path):
48
+ console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
49
+ return {"error": f"'{file_path}' is not a file."}
50
+
51
+ with open(file_path, "r", encoding="utf-8") as f:
52
+ current_content = f.read()
53
+
54
+ # Decide how to modify
55
+ targeted_replacement = bool(replace_content) and (replace_content in current_content)
56
+ replace_content_provided = bool(replace_content)
57
+
58
+ if targeted_replacement:
59
+ modified_content = current_content.replace(replace_content, proposed_changes)
60
+ console.print(f"[cyan]Replacing targeted content in '{file_path}'[/cyan]")
61
+ elif not targeted_replacement:
62
+ # Only allow full replacement if explicitly authorized
63
+ if overwrite_entire_file:
64
+ modified_content = proposed_changes
65
+ if replace_content_provided:
66
+ console.print(f"[bold yellow]Target content not found—replacing the entire file by explicit request (overwrite_entire_file=True).[/bold yellow]")
67
+ else:
68
+ console.print(f"[bold yellow]No target provided—replacing the entire file by explicit request (overwrite_entire_file=True).[/bold yellow]")
69
+ else:
70
+ if not replace_content_provided:
71
+ msg = "Refusing to replace the entire file: No replace_content provided and overwrite_entire_file=False."
72
+ else:
73
+ msg = "Refusing to replace the entire file: Target content not found in file and overwrite_entire_file=False."
74
+ console.print(f"[bold red]Error:[/bold red] {msg}")
75
+ return {
76
+ "success": False,
77
+ "path": file_path,
78
+ "message": msg,
79
+ "diff": "",
80
+ "changed": False,
81
+ }
82
+
83
+ # Generate a diff for display
84
+ diff_lines = list(difflib.unified_diff(
85
+ current_content.splitlines(keepends=True),
86
+ modified_content.splitlines(keepends=True),
87
+ fromfile=f"a/{os.path.basename(file_path)}",
88
+ tofile=f"b/{os.path.basename(file_path)}",
89
+ n=3,
90
+ ))
91
+ diff_text = "".join(diff_lines)
92
+ console.print("[bold cyan]Changes to be applied:[/bold cyan]")
93
+ if diff_text.strip():
94
+ formatted_diff = ""
95
+ for line in diff_lines:
96
+ if line.startswith("+") and not line.startswith("+++"):
97
+ formatted_diff += f"[bold green]{line}[/bold green]"
98
+ elif line.startswith("-") and not line.startswith("---"):
99
+ formatted_diff += f"[bold red]{line}[/bold red]"
100
+ elif line.startswith("@"):
101
+ formatted_diff += f"[bold cyan]{line}[/bold cyan]"
102
+ else:
103
+ formatted_diff += line
104
+ console.print(formatted_diff)
105
+ else:
106
+ console.print("[dim]No changes detected - file content is identical[/dim]")
107
+ return {
108
+ "success": False,
109
+ "path": file_path,
110
+ "message": "No changes to apply.",
111
+ "diff": diff_text,
112
+ "changed": False,
113
+ }
114
+
115
+ # Write the modified content to the file
116
+ with open(file_path, "w", encoding="utf-8") as f:
117
+ f.write(modified_content)
118
+
119
+ return {
120
+ "success": True,
121
+ "path": file_path,
122
+ "message": f"File modified at '{file_path}'",
123
+ "diff": diff_text,
124
+ "changed": True,
125
+ }
126
+ except Exception as e:
127
+ return {"error": f"Error modifying file '{file_path}': {str(e)}"}
128
+
129
+
130
+ @code_generation_agent.tool
131
+ def delete_snippet_from_file(context: RunContext, file_path: str, snippet: str) -> Dict[str, Any]:
132
+ console.log(f"🗑️ Deleting snippet from file [bold red]{file_path}[/bold red]")
133
+ """Delete a snippet from a file at the given file path.
134
+
135
+ Args:
136
+ file_path: Path to the file to delete.
137
+ snippet: The snippet to delete.
138
+
139
+ Returns:
140
+ A dictionary with status and message about the operation.
141
+ """
142
+ file_path = os.path.abspath(file_path)
143
+
144
+ console.print("\n[bold white on red] SNIPPET DELETION [/bold white on red]")
145
+ console.print(f"[bold yellow]From file:[/bold yellow] {file_path}")
146
+
147
+ try:
148
+ # Check if the file exists
149
+ if not os.path.exists(file_path):
150
+ console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
151
+ return {"error": f"File '{file_path}' does not exist."}
152
+
153
+ # Check if it's a file (not a directory)
154
+ if not os.path.isfile(file_path):
155
+ console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
156
+ return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
157
+
158
+ # Read the file content
159
+ with open(file_path, "r", encoding="utf-8") as f:
160
+ content = f.read()
161
+
162
+ # Check if the snippet exists in the file
163
+ if snippet not in content:
164
+ console.print(f"[bold red]Error:[/bold red] Snippet not found in file '{file_path}'")
165
+ return {"error": f"Snippet not found in file '{file_path}'."}
166
+
167
+ # Remove the snippet from the file content
168
+ modified_content = content.replace(snippet, "")
169
+
170
+ # Generate a diff
171
+ diff_lines = list(
172
+ difflib.unified_diff(
173
+ content.splitlines(keepends=True),
174
+ modified_content.splitlines(keepends=True),
175
+ fromfile=f"a/{os.path.basename(file_path)}",
176
+ tofile=f"b/{os.path.basename(file_path)}",
177
+ n=3, # Context lines
178
+ )
179
+ )
180
+
181
+ diff_text = "".join(diff_lines)
182
+
183
+ # Display the diff
184
+ console.print("[bold cyan]Changes to be applied:[/bold cyan]")
185
+
186
+ if diff_text.strip():
187
+ # Format the diff for display with colorization
188
+ formatted_diff = ""
189
+ for line in diff_lines:
190
+ if line.startswith("+") and not line.startswith("+++"):
191
+ formatted_diff += f"[bold green]{line}[/bold green]"
192
+ elif line.startswith("-") and not line.startswith("---"):
193
+ formatted_diff += f"[bold red]{line}[/bold red]"
194
+ elif line.startswith("@"):
195
+ formatted_diff += f"[bold cyan]{line}[/bold cyan]"
196
+ else:
197
+ formatted_diff += line
198
+
199
+ console.print(formatted_diff)
200
+ else:
201
+ console.print("[dim]No changes detected[/dim]")
202
+ return {
203
+ "success": False,
204
+ "path": file_path,
205
+ "message": "No changes needed.",
206
+ "diff": "",
207
+ }
208
+
209
+ # Write the modified content back to the file
210
+ with open(file_path, "w", encoding="utf-8") as f:
211
+ f.write(modified_content)
212
+
213
+ return {
214
+ "success": True,
215
+ "path": file_path,
216
+ "message": f"Snippet deleted from file '{file_path}'.",
217
+ "diff": diff_text,
218
+ }
219
+ except PermissionError:
220
+ return {"error": f"Permission denied to delete '{file_path}'."}
221
+ except FileNotFoundError:
222
+ # This should be caught by the initial check, but just in case
223
+ return {"error": f"File '{file_path}' does not exist."}
224
+ except Exception as e:
225
+ return {"error": f"Error deleting file '{file_path}': {str(e)}"}
226
+
227
+
228
+ @code_generation_agent.tool
229
+ def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
230
+ console.log(f"🗑️ Deleting file [bold red]{file_path}[/bold red]")
231
+ """Delete a file at the given file path.
232
+
233
+ Args:
234
+ file_path: Path to the file to delete.
235
+
236
+ Returns:
237
+ A dictionary with status and message about the operation.
238
+ """
239
+ file_path = os.path.abspath(file_path)
240
+
241
+ try:
242
+ # Check if the file exists
243
+ if not os.path.exists(file_path):
244
+ return {"error": f"File '{file_path}' does not exist."}
245
+
246
+ # Check if it's a file (not a directory)
247
+ if not os.path.isfile(file_path):
248
+ return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
249
+
250
+ # Attempt to delete the file
251
+ os.remove(file_path)
252
+
253
+ return {
254
+ "success": True,
255
+ "path": file_path,
256
+ "message": f"File '{file_path}' deleted successfully.",
257
+ }
258
+ except PermissionError:
259
+ return {"error": f"Permission denied to delete '{file_path}'."}
260
+ except FileNotFoundError:
261
+ # This should be caught by the initial check, but just in case
262
+ return {"error": f"File '{file_path}' does not exist."}
263
+ except Exception as e:
264
+ return {"error": f"Error deleting file '{file_path}': {str(e)}"}
@@ -0,0 +1,350 @@
1
+ # file_operations.py
2
+ import os
3
+ import fnmatch
4
+ from typing import List, Dict, Any
5
+ from code_agent.tools.common import console
6
+ from pydantic_ai import RunContext
7
+ from code_agent.agent import code_generation_agent
8
+
9
+
10
+ # Constants for file operations
11
+ IGNORE_PATTERNS = [
12
+ "**/node_modules/**",
13
+ "**/.git/**",
14
+ "**/__pycache__/**",
15
+ "**/.DS_Store",
16
+ "**/.env",
17
+ "**/.venv/**",
18
+ "**/venv/**",
19
+ "**/.idea/**",
20
+ "**/.vscode/**",
21
+ "**/dist/**",
22
+ "**/build/**",
23
+ "**/*.pyc",
24
+ "**/*.pyo",
25
+ "**/*.pyd",
26
+ "**/*.so",
27
+ "**/*.dll",
28
+ "**/*.exe",
29
+ ]
30
+
31
+
32
+ def should_ignore_path(path: str) -> bool:
33
+ """Check if the path should be ignored based on patterns."""
34
+ for pattern in IGNORE_PATTERNS:
35
+ if fnmatch.fnmatch(path, pattern):
36
+ return True
37
+ return False
38
+
39
+
40
+ @code_generation_agent.tool
41
+ def list_files(
42
+ context: RunContext, directory: str = ".", recursive: bool = True
43
+ ) -> List[Dict[str, Any]]:
44
+ """Recursively list all files in a directory, ignoring common patterns.
45
+
46
+ Args:
47
+ directory: The directory to list files from. Defaults to current directory.
48
+ recursive: Whether to search recursively. Defaults to True.
49
+
50
+ Returns:
51
+ A list of dictionaries with file information including path, size, and type.
52
+ """
53
+ results = []
54
+ directory = os.path.abspath(directory)
55
+
56
+ # Display directory listing header
57
+ console.print("\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]")
58
+ console.print(
59
+ f"📂 [bold cyan]{directory}[/bold cyan] [dim](recursive={recursive})[/dim]"
60
+ )
61
+ console.print("[dim]" + "-" * 60 + "[/dim]")
62
+
63
+ if not os.path.exists(directory):
64
+ console.print(
65
+ f"[bold red]Error:[/bold red] Directory '{directory}' does not exist"
66
+ )
67
+ console.print("[dim]" + "-" * 60 + "[/dim]\n")
68
+ return [{"error": f"Directory '{directory}' does not exist"}]
69
+
70
+ if not os.path.isdir(directory):
71
+ console.print(f"[bold red]Error:[/bold red] '{directory}' is not a directory")
72
+ console.print("[dim]" + "-" * 60 + "[/dim]\n")
73
+ return [{"error": f"'{directory}' is not a directory"}]
74
+
75
+ # Track folders and files at each level for tree display
76
+ folder_structure = {}
77
+ file_list = []
78
+
79
+ for root, dirs, files in os.walk(directory):
80
+ # Skip ignored directories
81
+ dirs[:] = [d for d in dirs if not should_ignore_path(os.path.join(root, d))]
82
+
83
+ rel_path = os.path.relpath(root, directory)
84
+ depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
85
+
86
+ if rel_path == ".":
87
+ rel_path = ""
88
+
89
+ # Add directory entry to results
90
+ if rel_path:
91
+ dir_path = os.path.join(directory, rel_path)
92
+ results.append(
93
+ {
94
+ "path": rel_path,
95
+ "type": "directory",
96
+ "size": 0,
97
+ "full_path": dir_path,
98
+ "depth": depth,
99
+ }
100
+ )
101
+
102
+ # Add to folder structure for display
103
+ folder_structure[rel_path] = {
104
+ "path": rel_path,
105
+ "depth": depth,
106
+ "full_path": dir_path,
107
+ }
108
+
109
+ # Add file entries
110
+ for file in files:
111
+ file_path = os.path.join(root, file)
112
+ if should_ignore_path(file_path):
113
+ continue
114
+
115
+ rel_file_path = os.path.join(rel_path, file) if rel_path else file
116
+
117
+ try:
118
+ size = os.path.getsize(file_path)
119
+ file_info = {
120
+ "path": rel_file_path,
121
+ "type": "file",
122
+ "size": size,
123
+ "full_path": file_path,
124
+ "depth": depth,
125
+ }
126
+ results.append(file_info)
127
+ file_list.append(file_info)
128
+ except (FileNotFoundError, PermissionError):
129
+ # Skip files we can't access
130
+ continue
131
+
132
+ if not recursive:
133
+ break
134
+
135
+ # Helper function to format file size
136
+ def format_size(size_bytes):
137
+ if size_bytes < 1024:
138
+ return f"{size_bytes} B"
139
+ elif size_bytes < 1024 * 1024:
140
+ return f"{size_bytes / 1024:.1f} KB"
141
+ elif size_bytes < 1024 * 1024 * 1024:
142
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
143
+ else:
144
+ return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
145
+
146
+ # Helper function to get file icon based on extension
147
+ def get_file_icon(file_path):
148
+ ext = os.path.splitext(file_path)[1].lower()
149
+ if ext in [".py", ".pyw"]:
150
+ return "🐍" # Python
151
+ elif ext in [".js", ".jsx", ".ts", ".tsx"]:
152
+ return "📜" # JavaScript/TypeScript
153
+ elif ext in [".html", ".htm", ".xml"]:
154
+ return "🌐" # HTML/XML
155
+ elif ext in [".css", ".scss", ".sass"]:
156
+ return "🎨" # CSS
157
+ elif ext in [".md", ".markdown", ".rst"]:
158
+ return "📝" # Markdown/docs
159
+ elif ext in [".json", ".yaml", ".yml", ".toml"]:
160
+ return "⚙️" # Config files
161
+ elif ext in [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp"]:
162
+ return "🖼️" # Images
163
+ elif ext in [".mp3", ".wav", ".ogg", ".flac"]:
164
+ return "🎵" # Audio
165
+ elif ext in [".mp4", ".avi", ".mov", ".webm"]:
166
+ return "🎬" # Video
167
+ elif ext in [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]:
168
+ return "📄" # Documents
169
+ elif ext in [".zip", ".tar", ".gz", ".rar", ".7z"]:
170
+ return "📦" # Archives
171
+ elif ext in [".exe", ".dll", ".so", ".dylib"]:
172
+ return "⚡" # Executables
173
+ else:
174
+ return "📄" # Default file icon
175
+
176
+ # Display tree structure
177
+ if results:
178
+ # Sort directories and files
179
+ directories = sorted(
180
+ [d for d in results if d["type"] == "directory"], key=lambda x: x["path"]
181
+ )
182
+ files = sorted(
183
+ [f for f in results if f["type"] == "file"], key=lambda x: x["path"]
184
+ )
185
+
186
+ # First show directory itself
187
+ console.print(
188
+ f"📁 [bold blue]{os.path.basename(directory) or directory}[/bold blue]"
189
+ )
190
+
191
+ # After gathering all results
192
+ # Combine both directories and files, then sort
193
+ all_items = sorted(results, key=lambda x: x['path'])
194
+
195
+ current_depth = 0
196
+ parent_dirs_with_content = set()
197
+
198
+ for i, item in enumerate(all_items):
199
+ # Skip root directory
200
+ if item["type"] == "directory" and not item["path"]:
201
+ continue
202
+
203
+ # Get parent directories to track which ones have content
204
+ if os.sep in item["path"]:
205
+ parent_path = os.path.dirname(item["path"])
206
+ parent_dirs_with_content.add(parent_path)
207
+
208
+ # Calculate depth from path
209
+ depth = item["path"].count(os.sep) + 1 if item["path"] else 0
210
+
211
+ # Calculate prefix for tree structure
212
+ prefix = ""
213
+ for d in range(depth):
214
+ if d == depth - 1:
215
+ prefix += "└── "
216
+ else:
217
+ prefix += " "
218
+
219
+ # Display item with appropriate icon and color
220
+ name = os.path.basename(item["path"]) or item["path"]
221
+
222
+ if item["type"] == "directory":
223
+ console.print(f"{prefix}📁 [bold blue]{name}/[/bold blue]")
224
+ else: # file
225
+ icon = get_file_icon(item["path"])
226
+ size_str = format_size(item["size"])
227
+ console.print(
228
+ f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]"
229
+ )
230
+ else:
231
+ console.print("[yellow]Directory is empty[/yellow]")
232
+
233
+ # Display summary
234
+ dir_count = sum(1 for item in results if item["type"] == "directory")
235
+ file_count = sum(1 for item in results if item["type"] == "file")
236
+ total_size = sum(item["size"] for item in results if item["type"] == "file")
237
+
238
+ console.print("\n[bold cyan]Summary:[/bold cyan]")
239
+ console.print(
240
+ f"📁 [blue]{dir_count} directories[/blue], 📄 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]"
241
+ )
242
+ console.print("[dim]" + "-" * 60 + "[/dim]\n")
243
+
244
+ return results
245
+
246
+
247
+ @code_generation_agent.tool
248
+ def create_file(
249
+ context: RunContext, file_path: str, content: str = ""
250
+ ) -> Dict[str, Any]:
251
+ console.log(f"✨ Creating new file [bold green]{file_path}[/bold green]")
252
+ """Create a new file with optional content.
253
+
254
+ Args:
255
+ file_path: Path where the file should be created
256
+ content: Optional content to write to the file
257
+
258
+ Returns:
259
+ A dictionary with the result of the operation
260
+ """
261
+ file_path = os.path.abspath(file_path)
262
+
263
+ # Check if file already exists
264
+ if os.path.exists(file_path):
265
+ return {
266
+ "error": f"File '{file_path}' already exists. Use modify_file to edit it."
267
+ }
268
+
269
+ # Create parent directories if they don't exist
270
+ directory = os.path.dirname(file_path)
271
+ if directory and not os.path.exists(directory):
272
+ try:
273
+ os.makedirs(directory)
274
+ except Exception as e:
275
+ return {"error": f"Error creating directory '{directory}': {str(e)}"}
276
+
277
+ # Create the file
278
+ try:
279
+ with open(file_path, "w", encoding="utf-8") as f:
280
+ console.print("[yellow]Writing to file:[/yellow]")
281
+ console.print(content)
282
+ f.write(content)
283
+
284
+ return {
285
+ "success": True,
286
+ "path": file_path,
287
+ "message": f"File created at '{file_path}'",
288
+ "content_length": len(content),
289
+ }
290
+ except Exception as e:
291
+ return {"error": f"Error creating file '{file_path}': {str(e)}"}
292
+
293
+
294
+ @code_generation_agent.tool
295
+ def read_file(
296
+ context: RunContext, file_path: str, start_line: int = 0, end_line: int = None
297
+ ) -> Dict[str, Any]:
298
+ console.log(
299
+ f"📄 Reading [bold cyan]{file_path}[/bold cyan] (lines {start_line} to {end_line or 'end'})"
300
+ )
301
+ """Read the contents of a file, optionally within a line range.
302
+
303
+ Args:
304
+ file_path: Path to the file to read
305
+ start_line: Starting line number (0-indexed). Defaults to 0.
306
+ end_line: Ending line number (inclusive, 0-indexed). Defaults to None (read to end).
307
+
308
+ Returns:
309
+ A dictionary with the file contents and metadata.
310
+ """
311
+ file_path = os.path.abspath(file_path)
312
+
313
+ if not os.path.exists(file_path):
314
+ return {"error": f"File '{file_path}' does not exist"}
315
+
316
+ if not os.path.isfile(file_path):
317
+ return {"error": f"'{file_path}' is not a file"}
318
+
319
+ try:
320
+ with open(file_path, "r", encoding="utf-8") as f:
321
+ lines = f.readlines()
322
+
323
+ # Handle line range
324
+ if end_line is None:
325
+ end_line = len(lines) - 1
326
+
327
+ # Ensure valid range
328
+ start_line = max(0, min(start_line, len(lines) - 1))
329
+ end_line = max(start_line, min(end_line, len(lines) - 1))
330
+
331
+ selected_lines = lines[start_line : end_line + 1]
332
+ content = "".join(selected_lines)
333
+
334
+ # Get file extension
335
+ _, ext = os.path.splitext(file_path)
336
+
337
+ return {
338
+ "content": content,
339
+ "path": file_path,
340
+ "extension": ext.lstrip("."),
341
+ "total_lines": len(lines),
342
+ "read_lines": end_line - start_line + 1,
343
+ "start_line": start_line,
344
+ "end_line": end_line,
345
+ }
346
+ except UnicodeDecodeError:
347
+ # For binary files, return an error
348
+ return {"error": f"Cannot read '{file_path}' as text - it may be a binary file"}
349
+ except Exception as e:
350
+ return {"error": f"Error reading file '{file_path}': {str(e)}"}
@@ -0,0 +1,41 @@
1
+ from code_agent.agent import code_generation_agent
2
+ from typing import List, Dict
3
+ import requests
4
+ from bs4 import BeautifulSoup
5
+ from pydantic_ai import RunContext
6
+
7
+
8
+ @code_generation_agent.tool
9
+ def web_search(
10
+ context: RunContext, query: str, num_results: int = 5
11
+ ) -> List[Dict[str, str]]:
12
+ """Perform a web search and return a list of results with titles and URLs.
13
+
14
+ Args:
15
+ query: The search query.
16
+ num_results: Number of results to return. Defaults to 5.
17
+
18
+ Returns:
19
+ A list of dictionaries, each containing 'title' and 'url' for a search result.
20
+ """
21
+ search_url = "https://www.google.com/search"
22
+ headers = {
23
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
24
+ }
25
+ params = {"q": query}
26
+
27
+ response = requests.get(search_url, headers=headers, params=params)
28
+ response.raise_for_status()
29
+
30
+ soup = BeautifulSoup(response.text, "html.parser")
31
+ results = []
32
+
33
+ for g in soup.find_all("div", class_="tF2Cxc")[:num_results]:
34
+ title_element = g.find("h3")
35
+ link_element = g.find("a")
36
+ if title_element and link_element:
37
+ title = title_element.get_text()
38
+ url = link_element["href"]
39
+ results.append({"title": title, "url": url})
40
+
41
+ return results
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: code-puppy
3
+ Version: 0.0.2
4
+ Summary: Code generation agent similar to Windsurf or Cursor
5
+ Author: Windsurf Engineering Team
6
+ License: MIT
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Topic :: Software Development :: Code Generators
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: bs4>=0.0.2
16
+ Requires-Dist: httpx>=0.24.1
17
+ Requires-Dist: logfire>=0.7.1
18
+ Requires-Dist: pydantic-ai>=0.1.0
19
+ Requires-Dist: pydantic>=2.4.0
20
+ Requires-Dist: pytest-cov>=6.1.1
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Requires-Dist: rich>=13.4.2
23
+ Requires-Dist: ruff>=0.11.11
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Code Generation Agent
27
+
28
+ ## Overview
29
+
30
+ This project is a sophisticated AI-powered code generation agent, designed to understand programming tasks, generate high-quality code, and explain its reasoning similar to tools like Windsurf and Cursor.
31
+
32
+ ## Features
33
+
34
+ - **Multi-language support**: Capable of generating code in various programming languages.
35
+ - **Interactive CLI**: A command-line interface for interactive use.
36
+ - **Detailed explanations**: Provides insights into generated code to understand its logic and structure.
37
+ - **Easy Integration**: Embed it seamlessly into Python projects.
38
+
39
+ ## New Feature
40
+ - **Real-time collaboration**: Allows multiple users to collaboratively edit and review code generation tasks in real-time.
41
+
42
+ ## Installation
43
+
44
+ > **NOTE:** This project uses [astral-sh/uv](https://github.com/astral-sh/uv) for all dependency management and builds. Please install [uv](https://github.com/astral-sh/uv) before continuing.
45
+
46
+ 1. **Clone the repository**:
47
+ ```bash
48
+ git clone <repository_url>
49
+ cd <repository_name>
50
+ ```
51
+ 2. **Install dependencies**:
52
+ ```bash
53
+ uv pip install -e .
54
+ ```
55
+ 3. **(optional)** If contributing, install additional development dependencies:
56
+ ```bash
57
+ uv pip install -r dev-requirements.txt # If present
58
+ ```
59
+ 4. **Configure environment variables**:
60
+ - Create an `.env` file in the root, using `.env.example` as a template, to store required API keys.
61
+
62
+ ## Usage
63
+
64
+ ### Command Line Interface
65
+
66
+ Run specific tasks or engage in interactive mode:
67
+
68
+ ```bash
69
+ # Execute a task directly
70
+ uv run python main.py "write me a C++ hello world program in /tmp/main.cpp then compile it and run it"
71
+
72
+ # Enter interactive mode
73
+ uv run python main.py --interactive
74
+ ```
75
+
76
+ ### Python API
77
+
78
+ Utilize the agent programmatically within your Python scripts:
79
+
80
+ ```python
81
+ import asyncio
82
+ from code_agent.agent_tools import generate_code
83
+
84
+ async def main():
85
+ task = "Your task description"
86
+ response = await generate_code(None, task)
87
+
88
+ if response.success:
89
+ for snippet in response.snippets:
90
+ print(f"Language: {snippet.language}")
91
+ print(snippet.code)
92
+ print(snippet.explanation)
93
+
94
+ if __name__ == "__main__":
95
+ asyncio.run(main())
96
+ ```
97
+
98
+ Explore the `examples` directory for elaborated utilization samples.
99
+
100
+ ## Project Structure
101
+
102
+ - **`code_agent/agent.py`** - Core functionalities of the agent.
103
+ - **`code_agent/agent_tools.py`** - Tools and utilities for code generation.
104
+ - **`code_agent/agent_prompts.py`** - Templates and prompts used by the system.
105
+ - **`code_agent/models/`** - Data models for defining code and responses.
106
+ - **`examples/`** - Example scripts showcasing agent capabilities.
107
+ - **`main.py`** - Entry point for command-line interactions.
108
+
109
+ ## Contributing
110
+
111
+ Contributions are welcome! Please follow these steps:
112
+
113
+ 1. Fork the repository.
114
+ 2. Create a new branch (`git checkout -b feature/xyz`).
115
+ 3. Commit your changes (`git commit -m 'Add feature'`).
116
+ 4. Push to the branch (`git push origin feature/xyz`).
117
+ 5. Open a Pull Request.
118
+
119
+ ## Requirements
120
+
121
+ - Python 3.9+
122
+ - [uv](https://github.com/astral-sh/uv) (for dependency management & builds)
123
+ - OpenAI API key (for GPT models)
124
+ - Optionally: Gemini API key (for Google's Gemini models)
125
+
126
+ ## Troubleshooting
127
+
128
+ - Ensure all dependencies are installed correctly via uv and the environment is properly configured.
129
+ - Check that API keys are valid and not expired.
130
+
131
+ ## License
132
+
133
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,16 @@
1
+ code_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ code_agent/agent.py,sha256=kA_goXZ9zT87yPkfpyBIjzz0N3rfUn-C88wAV8iK7pA,689
3
+ code_agent/agent_prompts.py,sha256=6DCCsFIxG8pCSa_uING_mITkQ30N1gh7FBITCl18rJY,3171
4
+ code_agent/main.py,sha256=QuhbNK6bDsu5yRmut2pI174feZPN6WB-2mQsmWZ0yrI,8806
5
+ code_agent/models/__init__.py,sha256=mczyauLuqkIeujryzootps3Zpu5ch2ZrQRAYpurXAE8,132
6
+ code_agent/models/codesnippet.py,sha256=5fgH5HoxE3YZX7DsVwywsZ2-XXotnUfHdvEuzHk0oX8,523
7
+ code_agent/tools/__init__.py,sha256=VOubgURQJYsL5Xp-44rOX5MVwO58PLQ8tMhxNo6ZTWs,157
8
+ code_agent/tools/command_runner.py,sha256=Bz8tDEL7_9MGARX2ARto9xQxa1GtUVkSOgEFCJzCcFs,6514
9
+ code_agent/tools/common.py,sha256=UpMqeJ0C8i0pkue1AHnnyyX0bFJ9zZeJ7HBR6yhuA8A,54
10
+ code_agent/tools/file_modifications.py,sha256=FeDVi-pcKp0q5QJnZsnXaiBn0VDT2Z5pD2cl-fnVLLU,10672
11
+ code_agent/tools/file_operations.py,sha256=NUHsHeO38ahO9sdOAjVES1UHj7-PyAVnyqnbpepRcTA,11747
12
+ code_agent/tools/web_search.py,sha256=vMgfg3g5lEVP2Kbqc1zUWKGwUnNmKJ0CY_zIUGKE7aw,1347
13
+ code_puppy-0.0.2.dist-info/METADATA,sha256=zfX-1rCFsYrCITbEW7E2wqvXBKFS1IQfBk-k4RtITBQ,4354
14
+ code_puppy-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ code_puppy-0.0.2.dist-info/entry_points.txt,sha256=qzzQH24Qc3KtiZ-FQMBU9ogKmzY_OKm4OtXQv2_IUTw,58
16
+ code_puppy-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ code-puppy = code_agent.main:main_entry