code-puppy 0.0.53__py3-none-any.whl → 0.0.55__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/__init__.py +1 -0
- code_puppy/agent.py +20 -8
- code_puppy/agent_prompts.py +2 -3
- code_puppy/command_line/file_path_completion.py +11 -4
- code_puppy/command_line/meta_command_handler.py +48 -28
- code_puppy/command_line/model_picker_completion.py +27 -13
- code_puppy/command_line/prompt_toolkit_completion.py +95 -51
- code_puppy/command_line/utils.py +8 -6
- code_puppy/config.py +22 -11
- code_puppy/main.py +32 -22
- code_puppy/model_factory.py +7 -7
- code_puppy/session_memory.py +31 -19
- code_puppy/tools/__init__.py +1 -0
- code_puppy/tools/code_map.py +16 -11
- code_puppy/tools/command_runner.py +160 -63
- code_puppy/tools/common.py +1 -1
- code_puppy/tools/file_modifications.py +352 -302
- code_puppy/tools/file_operations.py +237 -192
- code_puppy/tools/web_search.py +24 -8
- code_puppy/version_checker.py +4 -4
- {code_puppy-0.0.53.dist-info → code_puppy-0.0.55.dist-info}/METADATA +1 -1
- code_puppy-0.0.55.dist-info/RECORD +28 -0
- code_puppy-0.0.53.dist-info/RECORD +0 -28
- {code_puppy-0.0.53.data → code_puppy-0.0.55.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.53.dist-info → code_puppy-0.0.55.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.53.dist-info → code_puppy-0.0.55.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.53.dist-info → code_puppy-0.0.55.dist-info}/licenses/LICENSE +0 -0
code_puppy/session_memory.py
CHANGED
|
@@ -3,20 +3,24 @@ from pathlib import Path
|
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
4
|
from typing import Any, List, Dict, Optional
|
|
5
5
|
|
|
6
|
-
DEFAULT_MEMORY_PATH = Path(
|
|
6
|
+
DEFAULT_MEMORY_PATH = Path(".puppy_session_memory.json")
|
|
7
|
+
|
|
7
8
|
|
|
8
9
|
class SessionMemory:
|
|
9
10
|
"""
|
|
10
11
|
Simple persistent memory for Code Puppy agent sessions.
|
|
11
12
|
Stores short histories of tasks, notes, user preferences, and watched files.
|
|
12
13
|
"""
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, storage_path: Path = DEFAULT_MEMORY_PATH, memory_limit: int = 128
|
|
17
|
+
):
|
|
14
18
|
self.storage_path = storage_path
|
|
15
19
|
self.memory_limit = memory_limit
|
|
16
20
|
self._data = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
"history": [], # List of task/event dicts
|
|
22
|
+
"user_preferences": {},
|
|
23
|
+
"watched_files": [],
|
|
20
24
|
}
|
|
21
25
|
self._load()
|
|
22
26
|
|
|
@@ -25,47 +29,55 @@ class SessionMemory:
|
|
|
25
29
|
try:
|
|
26
30
|
self._data = json.loads(self.storage_path.read_text())
|
|
27
31
|
except Exception:
|
|
28
|
-
self._data = {
|
|
32
|
+
self._data = {
|
|
33
|
+
"history": [],
|
|
34
|
+
"user_preferences": {},
|
|
35
|
+
"watched_files": [],
|
|
36
|
+
}
|
|
29
37
|
|
|
30
38
|
def _save(self):
|
|
31
39
|
try:
|
|
32
40
|
self.storage_path.write_text(json.dumps(self._data, indent=2))
|
|
33
|
-
except Exception
|
|
41
|
+
except Exception:
|
|
34
42
|
pass # Don't crash the agent for memory fails
|
|
35
43
|
|
|
36
44
|
def log_task(self, description: str, extras: Optional[Dict[str, Any]] = None):
|
|
37
45
|
entry = {
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
47
|
+
"description": description,
|
|
40
48
|
}
|
|
41
49
|
if extras:
|
|
42
50
|
entry.update(extras)
|
|
43
|
-
self._data[
|
|
51
|
+
self._data["history"].append(entry)
|
|
44
52
|
# Trim memory
|
|
45
|
-
self._data[
|
|
53
|
+
self._data["history"] = self._data["history"][-self.memory_limit :]
|
|
46
54
|
self._save()
|
|
47
55
|
|
|
48
56
|
def get_history(self, within_minutes: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
49
57
|
if not within_minutes:
|
|
50
|
-
return list(self._data[
|
|
58
|
+
return list(self._data["history"])
|
|
51
59
|
cutoff = datetime.utcnow() - timedelta(minutes=within_minutes)
|
|
52
|
-
return [
|
|
60
|
+
return [
|
|
61
|
+
h
|
|
62
|
+
for h in self._data["history"]
|
|
63
|
+
if datetime.fromisoformat(h["timestamp"]) >= cutoff
|
|
64
|
+
]
|
|
53
65
|
|
|
54
66
|
def set_preference(self, key: str, value: Any):
|
|
55
|
-
self._data[
|
|
67
|
+
self._data["user_preferences"][key] = value
|
|
56
68
|
self._save()
|
|
57
69
|
|
|
58
70
|
def get_preference(self, key: str, default: Any = None) -> Any:
|
|
59
|
-
return self._data[
|
|
71
|
+
return self._data["user_preferences"].get(key, default)
|
|
60
72
|
|
|
61
73
|
def add_watched_file(self, path: str):
|
|
62
|
-
if path not in self._data[
|
|
63
|
-
self._data[
|
|
74
|
+
if path not in self._data["watched_files"]:
|
|
75
|
+
self._data["watched_files"].append(path)
|
|
64
76
|
self._save()
|
|
65
77
|
|
|
66
78
|
def list_watched_files(self) -> List[str]:
|
|
67
|
-
return list(self._data[
|
|
79
|
+
return list(self._data["watched_files"])
|
|
68
80
|
|
|
69
81
|
def clear(self):
|
|
70
|
-
self._data = {
|
|
82
|
+
self._data = {"history": [], "user_preferences": {}, "watched_files": []}
|
|
71
83
|
self._save()
|
code_puppy/tools/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ from code_puppy.tools.file_modifications import register_file_modifications_tool
|
|
|
3
3
|
from code_puppy.tools.command_runner import register_command_runner_tools
|
|
4
4
|
from code_puppy.tools.web_search import register_web_search_tools
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
def register_all_tools(agent):
|
|
7
8
|
"""Register all available tools to the provided agent."""
|
|
8
9
|
register_file_operations_tools(agent)
|
code_puppy/tools/code_map.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import ast
|
|
3
|
-
from typing import List, Tuple
|
|
4
3
|
from rich.tree import Tree
|
|
5
4
|
from rich.text import Text
|
|
6
|
-
from pathlib import Path
|
|
7
5
|
import pathspec
|
|
8
6
|
|
|
9
7
|
|
|
@@ -36,8 +34,8 @@ def map_python_file(file_path: str, show_doc: bool = True) -> Tree:
|
|
|
36
34
|
if doc:
|
|
37
35
|
t.add(Text(f'"{doc}"', style="dim"))
|
|
38
36
|
# Add inner functions
|
|
39
|
-
if hasattr(node,
|
|
40
|
-
for subnode in getattr(node,
|
|
37
|
+
if hasattr(node, "body"):
|
|
38
|
+
for subnode in getattr(node, "body"):
|
|
41
39
|
subsum = summarize_node(subnode)
|
|
42
40
|
if subsum:
|
|
43
41
|
sub_t = Tree(subsum)
|
|
@@ -50,13 +48,14 @@ def map_python_file(file_path: str, show_doc: bool = True) -> Tree:
|
|
|
50
48
|
|
|
51
49
|
|
|
52
50
|
def load_gitignore(directory: str):
|
|
53
|
-
gitignore_file = os.path.join(directory,
|
|
51
|
+
gitignore_file = os.path.join(directory, ".gitignore")
|
|
54
52
|
if os.path.exists(gitignore_file):
|
|
55
|
-
with open(gitignore_file,
|
|
56
|
-
spec = pathspec.PathSpec.from_lines(
|
|
53
|
+
with open(gitignore_file, "r") as f:
|
|
54
|
+
spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
|
|
57
55
|
return spec
|
|
58
56
|
else:
|
|
59
|
-
return pathspec.PathSpec.from_lines(
|
|
57
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", [])
|
|
58
|
+
|
|
60
59
|
|
|
61
60
|
def make_code_map(directory: str, show_doc: bool = True) -> Tree:
|
|
62
61
|
"""
|
|
@@ -71,16 +70,22 @@ def make_code_map(directory: str, show_doc: bool = True) -> Tree:
|
|
|
71
70
|
for root, dirs, files in os.walk(directory):
|
|
72
71
|
rel_root = os.path.relpath(root, abs_directory)
|
|
73
72
|
# Remove ignored directories in-place for os.walk to not descend
|
|
74
|
-
dirs[:] = [
|
|
73
|
+
dirs[:] = [
|
|
74
|
+
d
|
|
75
|
+
for d in dirs
|
|
76
|
+
if not spec.match_file(os.path.normpath(os.path.join(rel_root, d)))
|
|
77
|
+
]
|
|
75
78
|
for fname in files:
|
|
76
79
|
rel_file = os.path.normpath(os.path.join(rel_root, fname))
|
|
77
|
-
if fname.endswith(
|
|
80
|
+
if fname.endswith(".py") and not fname.startswith("__"):
|
|
78
81
|
if not spec.match_file(rel_file):
|
|
79
82
|
fpath = os.path.join(root, fname)
|
|
80
83
|
try:
|
|
81
84
|
file_tree = map_python_file(fpath, show_doc=show_doc)
|
|
82
85
|
base_tree.add(file_tree)
|
|
83
86
|
except Exception as e:
|
|
84
|
-
err = Tree(
|
|
87
|
+
err = Tree(
|
|
88
|
+
Text(f"[error reading {fname}: {e}]", style="bold red")
|
|
89
|
+
)
|
|
85
90
|
base_tree.add(err)
|
|
86
91
|
return base_tree
|
|
@@ -1,75 +1,172 @@
|
|
|
1
|
-
# command_runner.py
|
|
2
1
|
import subprocess
|
|
3
2
|
import time
|
|
4
|
-
import os
|
|
5
3
|
from typing import Dict, Any
|
|
6
4
|
from code_puppy.tools.common import console
|
|
7
5
|
from pydantic_ai import RunContext
|
|
8
6
|
from rich.markdown import Markdown
|
|
9
7
|
from rich.syntax import Syntax
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
|
|
10
|
+
def run_shell_command(
|
|
11
|
+
context: RunContext, command: str, cwd: str = None, timeout: int = 60
|
|
12
|
+
) -> Dict[str, Any]:
|
|
13
|
+
if not command or not command.strip():
|
|
14
|
+
console.print("[bold red]Error:[/bold red] Command cannot be empty")
|
|
15
|
+
return {"error": "Command cannot be empty"}
|
|
16
|
+
console.print(
|
|
17
|
+
f"\n[bold white on blue] SHELL COMMAND [/bold white on blue] \U0001f4c2 [bold green]$ {command}[/bold green]"
|
|
18
|
+
)
|
|
19
|
+
if cwd:
|
|
20
|
+
console.print(f"[dim]Working directory: {cwd}[/dim]")
|
|
21
|
+
console.print("[dim]" + "-" * 60 + "[/dim]")
|
|
22
|
+
from code_puppy.config import get_yolo_mode
|
|
23
|
+
|
|
24
|
+
yolo_mode = get_yolo_mode()
|
|
25
|
+
if not yolo_mode:
|
|
26
|
+
user_input = input("Are you sure you want to run this command? (yes/no): ")
|
|
27
|
+
if user_input.strip().lower() not in {"yes", "y"}:
|
|
28
|
+
console.print(
|
|
29
|
+
"[bold yellow]Command execution canceled by user.[/bold yellow]"
|
|
30
|
+
)
|
|
31
|
+
return {
|
|
32
|
+
"success": False,
|
|
33
|
+
"command": command,
|
|
34
|
+
"error": "User canceled command execution",
|
|
35
|
+
}
|
|
36
|
+
try:
|
|
37
|
+
start_time = time.time()
|
|
38
|
+
process = subprocess.Popen(
|
|
39
|
+
command,
|
|
40
|
+
shell=True,
|
|
41
|
+
stdout=subprocess.PIPE,
|
|
42
|
+
stderr=subprocess.PIPE,
|
|
43
|
+
text=True,
|
|
44
|
+
cwd=cwd,
|
|
45
|
+
)
|
|
29
46
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.print("[
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
stdout, stderr = process.communicate(timeout=timeout)
|
|
48
|
+
exit_code = process.returncode
|
|
49
|
+
execution_time = time.time() - start_time
|
|
50
|
+
if stdout.strip():
|
|
51
|
+
console.print("[bold white]STDOUT:[/bold white]")
|
|
52
|
+
console.print(
|
|
53
|
+
Syntax(
|
|
54
|
+
stdout.strip(),
|
|
55
|
+
"bash",
|
|
56
|
+
theme="monokai",
|
|
57
|
+
background_color="default",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
console.print("[yellow]No STDOUT output[/yellow]")
|
|
62
|
+
if stderr.strip():
|
|
63
|
+
console.print("[bold yellow]STDERR:[/bold yellow]")
|
|
64
|
+
console.print(
|
|
65
|
+
Syntax(
|
|
66
|
+
stderr.strip(),
|
|
67
|
+
"bash",
|
|
68
|
+
theme="monokai",
|
|
69
|
+
background_color="default",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if exit_code == 0:
|
|
73
|
+
console.print(
|
|
74
|
+
f"[bold green]✓ Command completed successfully[/bold green] [dim](took {execution_time:.2f}s)[/dim]"
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
console.print(
|
|
78
|
+
f"[bold red]✗ Command failed with exit code {exit_code}[/bold red] [dim](took {execution_time:.2f}s)[/dim]"
|
|
79
|
+
)
|
|
80
|
+
if not stdout.strip() and not stderr.strip():
|
|
81
|
+
console.print(
|
|
82
|
+
"[bold yellow]This command produced no output at all![/bold yellow]"
|
|
83
|
+
)
|
|
63
84
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
64
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
"success": exit_code == 0,
|
|
87
|
+
"command": command,
|
|
88
|
+
"stdout": stdout,
|
|
89
|
+
"stderr": stderr,
|
|
90
|
+
"exit_code": exit_code,
|
|
91
|
+
"execution_time": execution_time,
|
|
92
|
+
"timeout": False,
|
|
93
|
+
}
|
|
94
|
+
except subprocess.TimeoutExpired:
|
|
95
|
+
process.kill()
|
|
96
|
+
stdout, stderr = process.communicate()
|
|
97
|
+
execution_time = time.time() - start_time
|
|
98
|
+
if stdout.strip():
|
|
99
|
+
console.print(
|
|
100
|
+
"[bold white]STDOUT (incomplete due to timeout):[/bold white]"
|
|
101
|
+
)
|
|
102
|
+
console.print(
|
|
103
|
+
Syntax(
|
|
104
|
+
stdout.strip(),
|
|
105
|
+
"bash",
|
|
106
|
+
theme="monokai",
|
|
107
|
+
background_color="default",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
if stderr.strip():
|
|
111
|
+
console.print("[bold yellow]STDERR:[/bold yellow]")
|
|
112
|
+
console.print(
|
|
113
|
+
Syntax(
|
|
114
|
+
stderr.strip(),
|
|
115
|
+
"bash",
|
|
116
|
+
theme="monokai",
|
|
117
|
+
background_color="default",
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
console.print(
|
|
121
|
+
f"[bold red]⏱ Command timed out after {timeout} seconds[/bold red] [dim](ran for {execution_time:.2f}s)[/dim]"
|
|
122
|
+
)
|
|
123
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
124
|
+
return {
|
|
125
|
+
"success": False,
|
|
126
|
+
"command": command,
|
|
127
|
+
"stdout": stdout[-1000:],
|
|
128
|
+
"stderr": stderr[-1000:],
|
|
129
|
+
"exit_code": None,
|
|
130
|
+
"execution_time": execution_time,
|
|
131
|
+
"timeout": True,
|
|
132
|
+
"error": f"Command timed out after {timeout} seconds",
|
|
133
|
+
}
|
|
134
|
+
except Exception as e:
|
|
135
|
+
console.print_exception(show_locals=True)
|
|
136
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
137
|
+
return {
|
|
138
|
+
"success": False,
|
|
139
|
+
"command": command,
|
|
140
|
+
"error": f"Error executing command: {str(e)}",
|
|
141
|
+
"stdout": "",
|
|
142
|
+
"stderr": "",
|
|
143
|
+
"exit_code": -1,
|
|
144
|
+
"timeout": False,
|
|
145
|
+
}
|
|
146
|
+
|
|
65
147
|
|
|
148
|
+
def share_your_reasoning(
|
|
149
|
+
context: RunContext, reasoning: str, next_steps: str = None
|
|
150
|
+
) -> Dict[str, Any]:
|
|
151
|
+
console.print("\n[bold white on purple] AGENT REASONING [/bold white on purple]")
|
|
152
|
+
console.print("[bold cyan]Current reasoning:[/bold cyan]")
|
|
153
|
+
console.print(Markdown(reasoning))
|
|
154
|
+
if next_steps and next_steps.strip():
|
|
155
|
+
console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
|
|
156
|
+
console.print(Markdown(next_steps))
|
|
157
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
158
|
+
return {"success": True, "reasoning": reasoning, "next_steps": next_steps}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def register_command_runner_tools(agent):
|
|
66
162
|
@agent.tool
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
163
|
+
def agent_run_shell_command(
|
|
164
|
+
context: RunContext, command: str, cwd: str = None, timeout: int = 60
|
|
165
|
+
) -> Dict[str, Any]:
|
|
166
|
+
return run_shell_command(context, command, cwd, timeout)
|
|
167
|
+
|
|
168
|
+
@agent.tool
|
|
169
|
+
def agent_share_your_reasoning(
|
|
170
|
+
context: RunContext, reasoning: str, next_steps: str = None
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
return share_your_reasoning(context, reasoning, next_steps)
|
code_puppy/tools/common.py
CHANGED