code-puppy 0.0.30__py3-none-any.whl → 0.0.31__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/main.py CHANGED
@@ -12,12 +12,13 @@ from rich.markdown import CodeBlock
12
12
  from rich.text import Text
13
13
  from rich.syntax import Syntax
14
14
  from code_puppy.command_line.prompt_toolkit_completion import (
15
- get_input_with_path_completion,
15
+ get_input_with_combined_completion,
16
+ get_prompt_with_active_model
16
17
  )
17
18
 
18
19
  # Initialize rich console for pretty output
19
20
  from code_puppy.tools.common import console
20
- from code_puppy.agent import code_generation_agent
21
+ from code_puppy.agent import get_code_generation_agent, session_memory
21
22
 
22
23
  from code_puppy.tools import *
23
24
 
@@ -59,9 +60,18 @@ async def main():
59
60
  command = " ".join(args.command)
60
61
  try:
61
62
  while not shutdown_flag:
62
- response = await code_generation_agent.run(command)
63
+ agent = get_code_generation_agent()
64
+ response = await agent.run(command)
63
65
  agent_response = response.output
64
66
  console.print(agent_response.output_message)
67
+ # Log to session memory
68
+ session_memory().log_task(
69
+ f'Command executed: {command}',
70
+ extras={
71
+ 'output': agent_response.output_message,
72
+ 'awaiting_user_input': agent_response.awaiting_user_input
73
+ }
74
+ )
65
75
  if agent_response.awaiting_user_input:
66
76
  console.print(
67
77
  "[bold red]The agent requires further input. Interactive mode is recommended for such tasks."
@@ -82,13 +92,12 @@ async def main():
82
92
 
83
93
  # Add the file handling functionality for interactive mode
84
94
  async def interactive_mode(history_file_path: str) -> None:
95
+ from code_puppy.command_line.meta_command_handler import handle_meta_command
85
96
  """Run the agent in interactive mode."""
86
97
  console.print("[bold green]Code Puppy[/bold green] - Interactive Mode")
87
98
  console.print("Type 'exit' or 'quit' to exit the interactive mode.")
88
99
  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
- )
100
+ console.print("Type [bold blue]@[/bold blue] for path completion, or [bold blue]~m[/bold blue] to pick a model.")
92
101
 
93
102
  # Check if prompt_toolkit is installed
94
103
  try:
@@ -133,9 +142,10 @@ async def interactive_mode(history_file_path: str) -> None:
133
142
  try:
134
143
  # Use prompt_toolkit for enhanced input with path completion
135
144
  try:
136
- # Use the async version of get_input_with_path_completion
137
- task = await get_input_with_path_completion(
138
- ">>> 🐶 ", symbol="@", history_file=history_file_path_prompt
145
+ # Use the async version of get_input_with_combined_completion
146
+ task = await get_input_with_combined_completion(
147
+ get_prompt_with_active_model(),
148
+ history_file=history_file_path_prompt
139
149
  )
140
150
  except ImportError:
141
151
  # Fall back to basic input if prompt_toolkit is not available
@@ -151,8 +161,8 @@ async def interactive_mode(history_file_path: str) -> None:
151
161
  console.print("[bold green]Goodbye![/bold green]")
152
162
  break
153
163
 
154
- # Check for clear command
155
- if task.strip().lower() == "clear":
164
+ # Check for clear command (supports both `clear` and `~clear`)
165
+ if task.strip().lower() in ("clear", "~clear"):
156
166
  message_history = []
157
167
  console.print("[bold yellow]Conversation history cleared![/bold yellow]")
158
168
  console.print(
@@ -160,6 +170,10 @@ async def interactive_mode(history_file_path: str) -> None:
160
170
  )
161
171
  continue
162
172
 
173
+ # Handle ~ meta/config commands before anything else
174
+ if task.strip().startswith('~'):
175
+ if handle_meta_command(task.strip(), console):
176
+ continue
163
177
  if task.strip():
164
178
  console.print(f"\n[bold blue]Processing task:[/bold blue] {task}\n")
165
179
 
@@ -175,15 +189,28 @@ async def interactive_mode(history_file_path: str) -> None:
175
189
  # Store agent's full response
176
190
  agent_response = None
177
191
 
178
- result = await code_generation_agent.run(
179
- task, message_history=message_history
180
- )
192
+ agent = get_code_generation_agent()
193
+ result = await agent.run(task, message_history=message_history)
181
194
  # Get the structured response
182
195
  agent_response = result.output
183
196
  console.print(agent_response.output_message)
197
+ # Log to session memory
198
+ session_memory().log_task(
199
+ f'Interactive task: {task}',
200
+ extras={
201
+ 'output': agent_response.output_message,
202
+ 'awaiting_user_input': agent_response.awaiting_user_input
203
+ }
204
+ )
184
205
 
185
- # Update message history with all messages from this interaction
186
- message_history = result.new_messages()
206
+ # Update message history but apply filters & limits
207
+ new_msgs = result.new_messages()
208
+ # 1. Drop any system/config messages (e.g., "agent loaded with model")
209
+ filtered = [m for m in new_msgs if not (isinstance(m, dict) and m.get("role") == "system")]
210
+ # 2. Append to existing history and keep only the most recent 40
211
+ message_history.extend(filtered)
212
+ if len(message_history) > 40:
213
+ message_history = message_history[-40:]
187
214
 
188
215
  if agent_response and agent_response.awaiting_user_input:
189
216
  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()
@@ -1,4 +1,11 @@
1
- import code_puppy.tools.file_modifications
2
- import code_puppy.tools.file_operations
3
- import code_puppy.tools.command_runner
4
- import code_puppy.tools.web_search
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
- # Environment variables used in this module:
13
- # - YOLO_MODE: When set to "true" (case-insensitive), bypasses the safety confirmation
14
- # prompt when running shell commands. This allows commands to execute
15
- # without user intervention, which can be useful for automation but
16
- # introduces security risks. Default is "false".
17
-
18
-
19
- @code_generation_agent.tool
20
- def run_shell_command(
21
- context: RunContext, command: str, cwd: str = None, timeout: int = 60
22
- ) -> Dict[str, Any]:
23
- """Run a shell command and return its output.
24
-
25
- Args:
26
- command: The shell command to execute.
27
- cwd: The current working directory to run the command in. Defaults to None (current directory).
28
- timeout: Maximum time in seconds to wait for the command to complete. Defaults to 60.
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
- stdout, stderr = process.communicate(timeout=timeout)
75
- exit_code = process.returncode
76
- execution_time = time.time() - start_time
77
-
78
- # Display command output
79
- if stdout.strip():
80
- console.print("[bold white]STDOUT:[/bold white]")
81
- console.print(
82
- Syntax(
83
- stdout.strip(),
84
- "bash",
85
- theme="monokai",
86
- background_color="default",
87
- )
88
- )
89
-
90
- if stderr.strip():
91
- console.print("[bold yellow]STDERR:[/bold yellow]")
92
- console.print(
93
- Syntax(
94
- stderr.strip(),
95
- "bash",
96
- theme="monokai",
97
- background_color="default",
98
- )
99
- )
100
-
101
- # Show execution summary
102
- if exit_code == 0:
103
- console.print(
104
- f"[bold green]✓ Command completed successfully[/bold green] [dim](took {execution_time:.2f}s)[/dim]"
105
- )
106
- else:
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
- return {
114
- "success": exit_code == 0,
115
- "command": command,
116
- "stdout": stdout,
117
- "stderr": stderr,
118
- "exit_code": exit_code,
119
- "execution_time": execution_time,
120
- "timeout": False,
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}
@@ -1,3 +1,5 @@
1
+ import os
1
2
  from rich.console import Console
2
3
 
3
- console = Console()
4
+ NO_COLOR = bool(int(os.environ.get('CODE_PUPPY_NO_COLOR', '0')))
5
+ console = Console(no_color=NO_COLOR)