code-puppy 0.0.75__tar.gz → 0.0.76__tar.gz
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-0.0.75 → code_puppy-0.0.76}/PKG-INFO +2 -2
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/agent.py +2 -4
- code_puppy-0.0.76/code_puppy/command_line/motd.py +49 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/main.py +2 -26
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/model_factory.py +12 -1
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/models.json +15 -2
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/tools/__init__.py +1 -2
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/tools/command_runner.py +30 -14
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/tools/file_modifications.py +14 -5
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/tools/file_operations.py +59 -71
- {code_puppy-0.0.75 → code_puppy-0.0.76}/pyproject.toml +2 -2
- code_puppy-0.0.75/code_puppy/command_line/motd.py +0 -57
- code_puppy-0.0.75/code_puppy/tools/web_search.py +0 -32
- {code_puppy-0.0.75 → code_puppy-0.0.76}/.gitignore +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/LICENSE +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/README.md +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/agent_prompts.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/command_line/meta_command_handler.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/config.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/session_memory.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/tools/ts_code_map.py +0 -0
- {code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/version_checker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-puppy
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.76
|
|
4
4
|
Summary: Code generation agent
|
|
5
5
|
Author: Michael Pfaffenberger
|
|
6
6
|
License: MIT
|
|
@@ -20,7 +20,7 @@ Requires-Dist: json-repair>=0.46.2
|
|
|
20
20
|
Requires-Dist: logfire>=0.7.1
|
|
21
21
|
Requires-Dist: pathspec>=0.11.0
|
|
22
22
|
Requires-Dist: prompt-toolkit>=3.0.38
|
|
23
|
-
Requires-Dist: pydantic-ai>=0.
|
|
23
|
+
Requires-Dist: pydantic-ai>=0.4.8
|
|
24
24
|
Requires-Dist: pydantic>=2.4.0
|
|
25
25
|
Requires-Dist: pytest-cov>=6.1.1
|
|
26
26
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -74,7 +74,7 @@ def _load_mcp_servers():
|
|
|
74
74
|
url = conf.get("url")
|
|
75
75
|
if url:
|
|
76
76
|
console.print(f"Registering MCP Server - {url}")
|
|
77
|
-
servers.append(MCPServerSSE(url))
|
|
77
|
+
servers.append(MCPServerSSE(url=url))
|
|
78
78
|
return servers
|
|
79
79
|
|
|
80
80
|
|
|
@@ -100,13 +100,11 @@ def reload_code_generation_agent():
|
|
|
100
100
|
if PUPPY_RULES:
|
|
101
101
|
instructions += f"\n{PUPPY_RULES}"
|
|
102
102
|
|
|
103
|
-
mcp_servers = _load_mcp_servers()
|
|
104
103
|
agent = Agent(
|
|
105
104
|
model=model,
|
|
106
105
|
instructions=instructions,
|
|
107
|
-
output_type=
|
|
106
|
+
output_type=str,
|
|
108
107
|
retries=3,
|
|
109
|
-
mcp_servers=mcp_servers,
|
|
110
108
|
)
|
|
111
109
|
register_all_tools(agent)
|
|
112
110
|
_code_generation_agent = agent
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MOTD (Message of the Day) feature for code-puppy.
|
|
3
|
+
Stores seen versions in ~/.puppy_cfg/motd.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
MOTD_VERSION = "20250802"
|
|
9
|
+
MOTD_MESSAGE = """
|
|
10
|
+
/¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯\\
|
|
11
|
+
| 🐾 Happy Sat-urday, Aug 2, 2025! |
|
|
12
|
+
| |
|
|
13
|
+
| Biscuit the code puppy is on full zoomie mode! |
|
|
14
|
+
| Major paws-up: We now integrate Cerebras Qwen3 Coder |
|
|
15
|
+
| 480b! YES, that’s 480 billion parameters of tail-wagging|
|
|
16
|
+
| code speed. It’s so fast, even my fetch can’t keep up! |
|
|
17
|
+
| |
|
|
18
|
+
| • Take stretch breaks – you’ll need ‘em! |
|
|
19
|
+
| • DRY your code, but keep your pup hydrated. |
|
|
20
|
+
| • If you hit a bug, treat yourself for finding it! |
|
|
21
|
+
| |
|
|
22
|
+
| Today: sniff, code, roll over, and let Cerebras Qwen3 |
|
|
23
|
+
| Coder 480b do the heavy lifting. Fire up a ~motd anytime|
|
|
24
|
+
| you need some puppy hype! |
|
|
25
|
+
\___________________________________________________________/
|
|
26
|
+
"""
|
|
27
|
+
MOTD_TRACK_FILE = os.path.expanduser("~/.puppy_cfg/motd.txt")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def has_seen_motd(version: str) -> bool:
|
|
31
|
+
if not os.path.exists(MOTD_TRACK_FILE):
|
|
32
|
+
return False
|
|
33
|
+
with open(MOTD_TRACK_FILE, "r") as f:
|
|
34
|
+
seen_versions = {line.strip() for line in f if line.strip()}
|
|
35
|
+
return version in seen_versions
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def mark_motd_seen(version: str):
|
|
39
|
+
os.makedirs(os.path.dirname(MOTD_TRACK_FILE), exist_ok=True)
|
|
40
|
+
with open(MOTD_TRACK_FILE, "a") as f:
|
|
41
|
+
f.write(f"{version}\n")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def print_motd(console, force: bool = False) -> bool:
|
|
45
|
+
if force or not has_seen_motd(MOTD_VERSION):
|
|
46
|
+
console.print(MOTD_MESSAGE)
|
|
47
|
+
mark_motd_seen(MOTD_VERSION)
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
@@ -69,19 +69,7 @@ async def main():
|
|
|
69
69
|
async with agent.run_mcp_servers():
|
|
70
70
|
response = await agent.run(command)
|
|
71
71
|
agent_response = response.output
|
|
72
|
-
console.print(agent_response
|
|
73
|
-
# Log to session memory
|
|
74
|
-
session_memory().log_task(
|
|
75
|
-
f"Command executed: {command}",
|
|
76
|
-
extras={
|
|
77
|
-
"output": agent_response.output_message,
|
|
78
|
-
"awaiting_user_input": agent_response.awaiting_user_input,
|
|
79
|
-
},
|
|
80
|
-
)
|
|
81
|
-
if agent_response.awaiting_user_input:
|
|
82
|
-
console.print(
|
|
83
|
-
"[bold red]The agent requires further input. Interactive mode is recommended for such tasks."
|
|
84
|
-
)
|
|
72
|
+
console.print(agent_response)
|
|
85
73
|
break
|
|
86
74
|
except AttributeError as e:
|
|
87
75
|
console.print(f"[bold red]AttributeError:[/bold red] {str(e)}")
|
|
@@ -215,15 +203,8 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
215
203
|
result = await agent.run(task, message_history=message_history)
|
|
216
204
|
# Get the structured response
|
|
217
205
|
agent_response = result.output
|
|
218
|
-
console.print(agent_response
|
|
206
|
+
console.print(agent_response)
|
|
219
207
|
# Log to session memory
|
|
220
|
-
session_memory().log_task(
|
|
221
|
-
f"Interactive task: {task}",
|
|
222
|
-
extras={
|
|
223
|
-
"output": agent_response.output_message,
|
|
224
|
-
"awaiting_user_input": agent_response.awaiting_user_input,
|
|
225
|
-
},
|
|
226
|
-
)
|
|
227
208
|
|
|
228
209
|
# Update message history but apply filters & limits
|
|
229
210
|
new_msgs = result.new_messages()
|
|
@@ -273,11 +254,6 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
273
254
|
message_history = truncated
|
|
274
255
|
# --- END GROUP-AWARE TRUNCATION LOGIC ---
|
|
275
256
|
|
|
276
|
-
if agent_response and agent_response.awaiting_user_input:
|
|
277
|
-
console.print(
|
|
278
|
-
"\n[bold yellow]\u26a0 Agent needs your input to continue.[/bold yellow]"
|
|
279
|
-
)
|
|
280
|
-
|
|
281
257
|
# Show context status
|
|
282
258
|
console.print(
|
|
283
259
|
f"[dim]Context: {len(message_history)} messages in history[/dim]\n"
|
|
@@ -11,6 +11,7 @@ from pydantic_ai.models.openai import OpenAIModel
|
|
|
11
11
|
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
12
12
|
from pydantic_ai.providers.google_gla import GoogleGLAProvider
|
|
13
13
|
from pydantic_ai.providers.openai import OpenAIProvider
|
|
14
|
+
from pydantic_ai.providers.openrouter import OpenRouterProvider
|
|
14
15
|
|
|
15
16
|
# Environment variables used in this module:
|
|
16
17
|
# - GEMINI_API_KEY: API key for Google's Gemini models. Required when using Gemini models.
|
|
@@ -173,6 +174,16 @@ class ModelFactory:
|
|
|
173
174
|
model = OpenAIModel(model_name=model_config["name"], provider=provider)
|
|
174
175
|
setattr(model, "provider", provider)
|
|
175
176
|
return model
|
|
176
|
-
|
|
177
|
+
elif model_type == "openrouter":
|
|
178
|
+
api_key = None
|
|
179
|
+
if "api_key" in model_config:
|
|
180
|
+
if model_config["api_key"].startswith("$"):
|
|
181
|
+
api_key = os.environ.get(model_config["api_key"][1:])
|
|
182
|
+
else:
|
|
183
|
+
api_key = model_config["api_key"]
|
|
184
|
+
provider = OpenRouterProvider(api_key=api_key)
|
|
185
|
+
model_name = model_config.get("name")
|
|
186
|
+
model = OpenAIModel(model_name, provider=provider)
|
|
187
|
+
return model
|
|
177
188
|
else:
|
|
178
189
|
raise ValueError(f"Unsupported model type: {model_type}")
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"url": "http://localhost:11434/v1"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
-
"
|
|
40
|
+
"Qwen/Qwen3-235B-A22B-fp8-tput": {
|
|
41
41
|
"type": "custom_openai",
|
|
42
|
-
"name": "
|
|
42
|
+
"name": "Qwen/Qwen3-235B-A22B-fp8-tput",
|
|
43
43
|
"custom_endpoint": {
|
|
44
44
|
"url": "https://api.together.xyz/v1",
|
|
45
45
|
"api_key": "$TOGETHER_API_KEY"
|
|
@@ -53,6 +53,11 @@
|
|
|
53
53
|
"api_key": "$XAI_API_KEY"
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
+
"openrouter": {
|
|
57
|
+
"type": "openrouter",
|
|
58
|
+
"name": "meta-llama/llama-4-maverick:free",
|
|
59
|
+
"api_key": "$OPENROUTER_API_KEY"
|
|
60
|
+
},
|
|
56
61
|
"azure-gpt-4.1": {
|
|
57
62
|
"type": "azure_openai",
|
|
58
63
|
"name": "gpt-4.1",
|
|
@@ -66,5 +71,13 @@
|
|
|
66
71
|
"api_version": "2024-12-01-preview",
|
|
67
72
|
"api_key": "$AZURE_OPENAI_API_KEY",
|
|
68
73
|
"azure_endpoint": "$AZURE_OPENAI_ENDPOINT"
|
|
74
|
+
},
|
|
75
|
+
"Cerebras-Qwen3-Coder-480b": {
|
|
76
|
+
"type": "custom_openai",
|
|
77
|
+
"name": "qwen-3-coder-480b",
|
|
78
|
+
"custom_endpoint": {
|
|
79
|
+
"url": "https://api.cerebras.ai/v1",
|
|
80
|
+
"api_key": "$CEREBRAS_API_KEY"
|
|
81
|
+
}
|
|
69
82
|
}
|
|
70
83
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from code_puppy.tools.command_runner import register_command_runner_tools
|
|
2
2
|
from code_puppy.tools.file_modifications import register_file_modifications_tools
|
|
3
3
|
from code_puppy.tools.file_operations import register_file_operations_tools
|
|
4
|
-
from code_puppy.tools.web_search import register_web_search_tools
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
def register_all_tools(agent):
|
|
8
7
|
"""Register all available tools to the provided agent."""
|
|
8
|
+
|
|
9
9
|
register_file_operations_tools(agent)
|
|
10
10
|
register_file_modifications_tools(agent)
|
|
11
11
|
register_command_runner_tools(agent)
|
|
12
|
-
register_web_search_tools(agent)
|
|
@@ -2,6 +2,7 @@ import subprocess
|
|
|
2
2
|
import time
|
|
3
3
|
from typing import Any, Dict
|
|
4
4
|
|
|
5
|
+
from pydantic import BaseModel
|
|
5
6
|
from pydantic_ai import RunContext
|
|
6
7
|
from rich.markdown import Markdown
|
|
7
8
|
from rich.syntax import Syntax
|
|
@@ -9,12 +10,22 @@ from rich.syntax import Syntax
|
|
|
9
10
|
from code_puppy.tools.common import console
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
class ShellCommandOutput(BaseModel):
|
|
14
|
+
success: bool
|
|
15
|
+
command: str | None
|
|
16
|
+
error: str | None = ""
|
|
17
|
+
stdout: str | None
|
|
18
|
+
stderr: str | None
|
|
19
|
+
exit_code: int | None
|
|
20
|
+
execution_time: float | None
|
|
21
|
+
timeout: bool | None = False
|
|
22
|
+
|
|
12
23
|
def run_shell_command(
|
|
13
24
|
context: RunContext, command: str, cwd: str = None, timeout: int = 60
|
|
14
|
-
) ->
|
|
25
|
+
) -> ShellCommandOutput:
|
|
15
26
|
if not command or not command.strip():
|
|
16
27
|
console.print("[bold red]Error:[/bold red] Command cannot be empty")
|
|
17
|
-
return {"error": "Command cannot be empty"}
|
|
28
|
+
return ShellCommandOutput(**{"success": False, "error": "Command cannot be empty"})
|
|
18
29
|
console.print(
|
|
19
30
|
f"\n[bold white on blue] SHELL COMMAND [/bold white on blue] \U0001f4c2 [bold green]$ {command}[/bold green]"
|
|
20
31
|
)
|
|
@@ -30,11 +41,11 @@ def run_shell_command(
|
|
|
30
41
|
console.print(
|
|
31
42
|
"[bold yellow]Command execution canceled by user.[/bold yellow]"
|
|
32
43
|
)
|
|
33
|
-
return {
|
|
44
|
+
return ShellCommandOutput(**{
|
|
34
45
|
"success": False,
|
|
35
46
|
"command": command,
|
|
36
47
|
"error": "User canceled command execution",
|
|
37
|
-
}
|
|
48
|
+
})
|
|
38
49
|
try:
|
|
39
50
|
start_time = time.time()
|
|
40
51
|
process = subprocess.Popen(
|
|
@@ -84,7 +95,7 @@ def run_shell_command(
|
|
|
84
95
|
"[bold yellow]This command produced no output at all![/bold yellow]"
|
|
85
96
|
)
|
|
86
97
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
87
|
-
return {
|
|
98
|
+
return ShellCommandOutput(**{
|
|
88
99
|
"success": exit_code == 0,
|
|
89
100
|
"command": command,
|
|
90
101
|
"stdout": stdout,
|
|
@@ -92,7 +103,7 @@ def run_shell_command(
|
|
|
92
103
|
"exit_code": exit_code,
|
|
93
104
|
"execution_time": execution_time,
|
|
94
105
|
"timeout": False,
|
|
95
|
-
}
|
|
106
|
+
})
|
|
96
107
|
except subprocess.TimeoutExpired:
|
|
97
108
|
process.kill()
|
|
98
109
|
stdout, stderr = process.communicate()
|
|
@@ -123,7 +134,7 @@ def run_shell_command(
|
|
|
123
134
|
f"[bold red]⏱ Command timed out after {timeout} seconds[/bold red] [dim](ran for {execution_time:.2f}s)[/dim]"
|
|
124
135
|
)
|
|
125
136
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
126
|
-
return {
|
|
137
|
+
return ShellCommandOutput(**{
|
|
127
138
|
"success": False,
|
|
128
139
|
"command": command,
|
|
129
140
|
"stdout": stdout[-1000:],
|
|
@@ -132,7 +143,7 @@ def run_shell_command(
|
|
|
132
143
|
"execution_time": execution_time,
|
|
133
144
|
"timeout": True,
|
|
134
145
|
"error": f"Command timed out after {timeout} seconds",
|
|
135
|
-
}
|
|
146
|
+
})
|
|
136
147
|
except Exception as e:
|
|
137
148
|
console.print_exception(show_locals=True)
|
|
138
149
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
@@ -141,7 +152,7 @@ def run_shell_command(
|
|
|
141
152
|
stdout = None
|
|
142
153
|
if "stderr" not in locals():
|
|
143
154
|
stderr = None
|
|
144
|
-
return {
|
|
155
|
+
return ShellCommandOutput(**{
|
|
145
156
|
"success": False,
|
|
146
157
|
"command": command,
|
|
147
158
|
"error": f"Error executing command: {str(e)}",
|
|
@@ -149,12 +160,17 @@ def run_shell_command(
|
|
|
149
160
|
"stderr": stderr[-1000:] if stderr else None,
|
|
150
161
|
"exit_code": -1,
|
|
151
162
|
"timeout": False,
|
|
152
|
-
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
class ReasoningOutput(BaseModel):
|
|
166
|
+
success: bool = True
|
|
167
|
+
reasoning: str = ""
|
|
168
|
+
next_steps: str = ""
|
|
153
169
|
|
|
154
170
|
|
|
155
171
|
def share_your_reasoning(
|
|
156
172
|
context: RunContext, reasoning: str, next_steps: str = None
|
|
157
|
-
) ->
|
|
173
|
+
) -> ReasoningOutput:
|
|
158
174
|
console.print("\n[bold white on purple] AGENT REASONING [/bold white on purple]")
|
|
159
175
|
console.print("[bold cyan]Current reasoning:[/bold cyan]")
|
|
160
176
|
console.print(Markdown(reasoning))
|
|
@@ -162,18 +178,18 @@ def share_your_reasoning(
|
|
|
162
178
|
console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
|
|
163
179
|
console.print(Markdown(next_steps))
|
|
164
180
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
165
|
-
return {"success": True, "reasoning": reasoning, "next_steps": next_steps}
|
|
181
|
+
return ReasoningOutput(**{"success": True, "reasoning": reasoning, "next_steps": next_steps})
|
|
166
182
|
|
|
167
183
|
|
|
168
184
|
def register_command_runner_tools(agent):
|
|
169
185
|
@agent.tool
|
|
170
186
|
def agent_run_shell_command(
|
|
171
187
|
context: RunContext, command: str, cwd: str = None, timeout: int = 60
|
|
172
|
-
) ->
|
|
188
|
+
) -> ShellCommandOutput:
|
|
173
189
|
return run_shell_command(context, command, cwd, timeout)
|
|
174
190
|
|
|
175
191
|
@agent.tool
|
|
176
192
|
def agent_share_your_reasoning(
|
|
177
193
|
context: RunContext, reasoning: str, next_steps: str = None
|
|
178
|
-
) ->
|
|
194
|
+
) -> ReasoningOutput:
|
|
179
195
|
return share_your_reasoning(context, reasoning, next_steps)
|
|
@@ -17,6 +17,7 @@ import traceback
|
|
|
17
17
|
from typing import Any, Dict, List
|
|
18
18
|
|
|
19
19
|
from json_repair import repair_json
|
|
20
|
+
from pydantic import BaseModel
|
|
20
21
|
from pydantic_ai import RunContext
|
|
21
22
|
|
|
22
23
|
from code_puppy.tools.common import _find_best_window, console
|
|
@@ -311,7 +312,7 @@ def _edit_file(context: RunContext, path: str, diff: str) -> Dict[str, Any]:
|
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
|
|
314
|
-
def _delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
|
|
315
|
+
def _delete_file(context: RunContext, file_path: str = "") -> Dict[str, Any]:
|
|
315
316
|
console.log(f"🗑️ Deleting file [bold red]{file_path}[/bold red]")
|
|
316
317
|
file_path = os.path.abspath(file_path)
|
|
317
318
|
try:
|
|
@@ -344,13 +345,21 @@ def _delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
|
|
|
344
345
|
return res
|
|
345
346
|
|
|
346
347
|
|
|
348
|
+
class EditFileOutput(BaseModel):
|
|
349
|
+
success: bool | None
|
|
350
|
+
file_path: str | None
|
|
351
|
+
message: str | None
|
|
352
|
+
changed: bool | None
|
|
353
|
+
diff: str | None
|
|
354
|
+
|
|
355
|
+
|
|
347
356
|
def register_file_modifications_tools(agent):
|
|
348
357
|
"""Attach file-editing tools to *agent* with mandatory diff rendering."""
|
|
349
358
|
|
|
350
359
|
@agent.tool(retries=5)
|
|
351
|
-
def edit_file(context: RunContext, path: str, diff: str) ->
|
|
352
|
-
return _edit_file(context, path, diff)
|
|
360
|
+
def edit_file(context: RunContext, path: str = "", diff: str = "") -> EditFileOutput:
|
|
361
|
+
return EditFileOutput(**_edit_file(context, path, diff))
|
|
353
362
|
|
|
354
363
|
@agent.tool(retries=5)
|
|
355
|
-
def delete_file(context: RunContext, file_path: str) ->
|
|
356
|
-
return _delete_file(context, file_path)
|
|
364
|
+
def delete_file(context: RunContext, file_path: str = "") -> EditFileOutput:
|
|
365
|
+
return EditFileOutput(**_delete_file(context, file_path))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
from typing import Any, Dict, List
|
|
5
5
|
|
|
6
|
+
from pydantic import BaseModel, StrictStr, StrictInt
|
|
6
7
|
from pydantic_ai import RunContext
|
|
7
8
|
|
|
8
9
|
from code_puppy.tools.common import console
|
|
@@ -13,9 +14,21 @@ from code_puppy.tools.common import console
|
|
|
13
14
|
from code_puppy.tools.common import should_ignore_path
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class ListedFile(BaseModel):
|
|
18
|
+
path: str | None
|
|
19
|
+
type: str | None
|
|
20
|
+
size: int = 0
|
|
21
|
+
full_path: str | None
|
|
22
|
+
depth: int | None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ListFileOutput(BaseModel):
|
|
26
|
+
files: List[ListedFile]
|
|
27
|
+
|
|
28
|
+
|
|
16
29
|
def _list_files(
|
|
17
30
|
context: RunContext, directory: str = ".", recursive: bool = True
|
|
18
|
-
) ->
|
|
31
|
+
) -> ListFileOutput:
|
|
19
32
|
results = []
|
|
20
33
|
directory = os.path.abspath(directory)
|
|
21
34
|
console.print("\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]")
|
|
@@ -28,11 +41,11 @@ def _list_files(
|
|
|
28
41
|
f"[bold red]Error:[/bold red] Directory '{directory}' does not exist"
|
|
29
42
|
)
|
|
30
43
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
31
|
-
return [{"error": f"Directory '{directory}' does not exist"}]
|
|
44
|
+
return ListFileOutput(files=[ListedFile(**{"error": f"Directory '{directory}' does not exist"})])
|
|
32
45
|
if not os.path.isdir(directory):
|
|
33
46
|
console.print(f"[bold red]Error:[/bold red] '{directory}' is not a directory")
|
|
34
47
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
35
|
-
return [{"error": f"'{directory}' is not a directory"}]
|
|
48
|
+
return ListFileOutput(files=[ListedFile(**{"error": f"'{directory}' is not a directory"})])
|
|
36
49
|
folder_structure = {}
|
|
37
50
|
file_list = []
|
|
38
51
|
for root, dirs, files in os.walk(directory):
|
|
@@ -44,13 +57,13 @@ def _list_files(
|
|
|
44
57
|
if rel_path:
|
|
45
58
|
dir_path = os.path.join(directory, rel_path)
|
|
46
59
|
results.append(
|
|
47
|
-
{
|
|
60
|
+
ListedFile(**{
|
|
48
61
|
"path": rel_path,
|
|
49
62
|
"type": "directory",
|
|
50
63
|
"size": 0,
|
|
51
64
|
"full_path": dir_path,
|
|
52
65
|
"depth": depth,
|
|
53
|
-
}
|
|
66
|
+
})
|
|
54
67
|
)
|
|
55
68
|
folder_structure[rel_path] = {
|
|
56
69
|
"path": rel_path,
|
|
@@ -71,7 +84,7 @@ def _list_files(
|
|
|
71
84
|
"full_path": file_path,
|
|
72
85
|
"depth": depth,
|
|
73
86
|
}
|
|
74
|
-
results.append(file_info)
|
|
87
|
+
results.append(ListedFile(**file_info))
|
|
75
88
|
file_list.append(file_info)
|
|
76
89
|
except (FileNotFoundError, PermissionError):
|
|
77
90
|
continue
|
|
@@ -119,74 +132,81 @@ def _list_files(
|
|
|
119
132
|
|
|
120
133
|
if results:
|
|
121
134
|
files = sorted(
|
|
122
|
-
[f for f in results if f
|
|
135
|
+
[f for f in results if f.type == "file"], key=lambda x: x.path
|
|
123
136
|
)
|
|
124
137
|
console.print(
|
|
125
138
|
f"\U0001f4c1 [bold blue]{os.path.basename(directory) or directory}[/bold blue]"
|
|
126
139
|
)
|
|
127
|
-
all_items = sorted(results, key=lambda x: x
|
|
140
|
+
all_items = sorted(results, key=lambda x: x.path)
|
|
128
141
|
parent_dirs_with_content = set()
|
|
129
142
|
for i, item in enumerate(all_items):
|
|
130
|
-
if item
|
|
143
|
+
if item.type == "directory" and not item.path:
|
|
131
144
|
continue
|
|
132
|
-
if os.sep in item
|
|
133
|
-
parent_path = os.path.dirname(item
|
|
145
|
+
if os.sep in item.path:
|
|
146
|
+
parent_path = os.path.dirname(item.path)
|
|
134
147
|
parent_dirs_with_content.add(parent_path)
|
|
135
|
-
depth = item
|
|
148
|
+
depth = item.path.count(os.sep) + 1 if item.path else 0
|
|
136
149
|
prefix = ""
|
|
137
150
|
for d in range(depth):
|
|
138
151
|
if d == depth - 1:
|
|
139
152
|
prefix += "\u2514\u2500\u2500 "
|
|
140
153
|
else:
|
|
141
154
|
prefix += " "
|
|
142
|
-
name = os.path.basename(item
|
|
143
|
-
if item
|
|
155
|
+
name = os.path.basename(item.path) or item.path
|
|
156
|
+
if item.type == "directory":
|
|
144
157
|
console.print(f"{prefix}\U0001f4c1 [bold blue]{name}/[/bold blue]")
|
|
145
158
|
else:
|
|
146
|
-
icon = get_file_icon(item
|
|
147
|
-
size_str = format_size(item
|
|
159
|
+
icon = get_file_icon(item.path)
|
|
160
|
+
size_str = format_size(item.size)
|
|
148
161
|
console.print(
|
|
149
162
|
f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]"
|
|
150
163
|
)
|
|
151
164
|
else:
|
|
152
165
|
console.print("[yellow]Directory is empty[/yellow]")
|
|
153
|
-
dir_count = sum(1 for item in results if item
|
|
154
|
-
file_count = sum(1 for item in results if item
|
|
155
|
-
total_size = sum(item
|
|
166
|
+
dir_count = sum(1 for item in results if item.type == "directory")
|
|
167
|
+
file_count = sum(1 for item in results if item.type == "file")
|
|
168
|
+
total_size = sum(item.size for item in results if item.type == "file")
|
|
156
169
|
console.print("\n[bold cyan]Summary:[/bold cyan]")
|
|
157
170
|
console.print(
|
|
158
171
|
f"\U0001f4c1 [blue]{dir_count} directories[/blue], \U0001f4c4 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]"
|
|
159
172
|
)
|
|
160
173
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
161
|
-
return results
|
|
174
|
+
return ListFileOutput(files=results)
|
|
162
175
|
|
|
163
176
|
|
|
164
|
-
|
|
177
|
+
class ReadFileOutput(BaseModel):
|
|
178
|
+
content: str | None
|
|
179
|
+
|
|
180
|
+
def _read_file(context: RunContext, file_path: str) -> ReadFileOutput:
|
|
165
181
|
file_path = os.path.abspath(file_path)
|
|
166
182
|
console.print(
|
|
167
183
|
f"\n[bold white on blue] READ FILE [/bold white on blue] \U0001f4c2 [bold cyan]{file_path}[/bold cyan]"
|
|
168
184
|
)
|
|
169
185
|
console.print("[dim]" + "-" * 60 + "[/dim]")
|
|
170
186
|
if not os.path.exists(file_path):
|
|
171
|
-
return
|
|
187
|
+
return ReadFileOutput(content=f"File '{file_path}' does not exist")
|
|
172
188
|
if not os.path.isfile(file_path):
|
|
173
|
-
return
|
|
189
|
+
return ReadFileOutput(content=f"'{file_path}' is not a file")
|
|
174
190
|
try:
|
|
175
191
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
176
192
|
content = f.read()
|
|
177
|
-
return
|
|
178
|
-
"content": content,
|
|
179
|
-
"path": file_path,
|
|
180
|
-
"total_lines": len(content.splitlines()),
|
|
181
|
-
}
|
|
193
|
+
return ReadFileOutput(content=content)
|
|
182
194
|
except Exception as exc:
|
|
183
|
-
return
|
|
195
|
+
return ReadFileOutput(content="FILE NOT FOUND")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class MatchInfo(BaseModel):
|
|
199
|
+
file_path: str | None
|
|
200
|
+
line_number: int | None
|
|
201
|
+
line_content: str | None
|
|
184
202
|
|
|
203
|
+
class GrepOutput(BaseModel):
|
|
204
|
+
matches: List[MatchInfo]
|
|
185
205
|
|
|
186
206
|
def _grep(
|
|
187
207
|
context: RunContext, search_string: str, directory: str = "."
|
|
188
|
-
) ->
|
|
189
|
-
matches: List[
|
|
208
|
+
) -> GrepOutput:
|
|
209
|
+
matches: List[MatchInfo] = []
|
|
190
210
|
directory = os.path.abspath(directory)
|
|
191
211
|
console.print(
|
|
192
212
|
f"\n[bold white on blue] GREP [/bold white on blue] \U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim]for '{search_string}'[/dim]"
|
|
@@ -209,11 +229,11 @@ def _grep(
|
|
|
209
229
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as fh:
|
|
210
230
|
for line_number, line_content in enumerate(fh, 1):
|
|
211
231
|
if search_string in line_content:
|
|
212
|
-
match_info = {
|
|
232
|
+
match_info = MatchInfo(**{
|
|
213
233
|
"file_path": file_path,
|
|
214
234
|
"line_number": line_number,
|
|
215
235
|
"line_content": line_content.strip(),
|
|
216
|
-
}
|
|
236
|
+
})
|
|
217
237
|
matches.append(match_info)
|
|
218
238
|
# console.print(
|
|
219
239
|
# f"[green]Match:[/green] {file_path}:{line_number} - {line_content.strip()}"
|
|
@@ -222,7 +242,7 @@ def _grep(
|
|
|
222
242
|
console.print(
|
|
223
243
|
"[yellow]Limit of 200 matches reached. Stopping search.[/yellow]"
|
|
224
244
|
)
|
|
225
|
-
return matches
|
|
245
|
+
return GrepOutput(matches=matches)
|
|
226
246
|
except FileNotFoundError:
|
|
227
247
|
console.print(
|
|
228
248
|
f"[yellow]File not found (possibly a broken symlink): {file_path}[/yellow]"
|
|
@@ -246,54 +266,22 @@ def _grep(
|
|
|
246
266
|
f"[green]Found {len(matches)} match(es) for '{search_string}' in {directory}[/green]"
|
|
247
267
|
)
|
|
248
268
|
|
|
249
|
-
return matches
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
# Exported top-level functions for direct import by tests and other code
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def list_files(context, directory=".", recursive=True):
|
|
256
|
-
return _list_files(context, directory, recursive)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def read_file(context, file_path):
|
|
260
|
-
return _read_file(context, file_path)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def grep(context, search_string, directory="."):
|
|
264
|
-
return _grep(context, search_string, directory)
|
|
269
|
+
return GrepOutput(matches=[])
|
|
265
270
|
|
|
266
271
|
|
|
267
272
|
def register_file_operations_tools(agent):
|
|
268
273
|
@agent.tool
|
|
269
274
|
def list_files(
|
|
270
275
|
context: RunContext, directory: str = ".", recursive: bool = True
|
|
271
|
-
) ->
|
|
276
|
+
) -> ListFileOutput:
|
|
272
277
|
return _list_files(context, directory, recursive)
|
|
273
278
|
|
|
274
279
|
@agent.tool
|
|
275
|
-
def read_file(context: RunContext, file_path: str) ->
|
|
280
|
+
def read_file(context: RunContext, file_path: str = "") -> ReadFileOutput:
|
|
276
281
|
return _read_file(context, file_path)
|
|
277
282
|
|
|
278
283
|
@agent.tool
|
|
279
284
|
def grep(
|
|
280
|
-
context: RunContext, search_string: str, directory: str = "."
|
|
281
|
-
) ->
|
|
285
|
+
context: RunContext, search_string: str = "", directory: str = "."
|
|
286
|
+
) -> GrepOutput:
|
|
282
287
|
return _grep(context, search_string, directory)
|
|
283
|
-
|
|
284
|
-
@agent.tool
|
|
285
|
-
def code_map(context: RunContext, directory: str = ".") -> str:
|
|
286
|
-
"""Generate a code map for the specified directory.
|
|
287
|
-
This will have a list of all function / class names and nested structure
|
|
288
|
-
Args:
|
|
289
|
-
context: The context object.
|
|
290
|
-
directory: The directory to generate the code map for.
|
|
291
|
-
|
|
292
|
-
Returns:
|
|
293
|
-
A string containing the code map.
|
|
294
|
-
"""
|
|
295
|
-
console.print("[bold white on blue] CODE MAP [/bold white on blue]")
|
|
296
|
-
from code_puppy.tools.ts_code_map import make_code_map
|
|
297
|
-
|
|
298
|
-
result = make_code_map(directory, ignore_tests=True)
|
|
299
|
-
return result
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-puppy"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.76"
|
|
8
8
|
description = "Code generation agent"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"pydantic-ai>=0.
|
|
12
|
+
"pydantic-ai>=0.4.8",
|
|
13
13
|
"httpx>=0.24.1",
|
|
14
14
|
"rich>=13.4.2",
|
|
15
15
|
"logfire>=0.7.1",
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MOTD (Message of the Day) feature for code-puppy.
|
|
3
|
-
Stores seen versions in ~/.puppy_cfg/motd.txt.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import os
|
|
7
|
-
|
|
8
|
-
MOTD_VERSION = "20240621"
|
|
9
|
-
MOTD_MESSAGE = """
|
|
10
|
-
June 21th, 2025 - 🚀 Woof-tastic news! Code Puppy now supports **MCP (Model Context Protocol) servers** for EXTREME PUPPY POWER!!!!.
|
|
11
|
-
|
|
12
|
-
You can now connect plugins like doc search, Context7 integration, and more by simply dropping their info in your `~/.code_puppy/mcp_servers.json`. I’ll bark at remote docs or wrangle code tools for you—no extra fetches needed.
|
|
13
|
-
|
|
14
|
-
Setup is easy:
|
|
15
|
-
1. Add your MCP config to `~/.code_puppy/mcp_servers.json`.
|
|
16
|
-
2. Fire up something like Context7, or any MCP server you want.
|
|
17
|
-
3. Ask me to search docs, analyze, and more.
|
|
18
|
-
|
|
19
|
-
The following example will let code_puppy use Context7!
|
|
20
|
-
Example config (+ more details in the README):
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
"mcp_servers": {
|
|
24
|
-
"context7": {
|
|
25
|
-
"url": "https://mcp.context7.com/sse"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
I fetch docs and power-ups via those servers. If you break stuff, please file an issue—bonus treat for reproducible bugs! 🦴
|
|
31
|
-
|
|
32
|
-
This message-of-the-day won’t bug you again unless you run ~motd. Stay fluffy!
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
MOTD_TRACK_FILE = os.path.expanduser("~/.puppy_cfg/motd.txt")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def has_seen_motd(version: str) -> bool:
|
|
39
|
-
if not os.path.exists(MOTD_TRACK_FILE):
|
|
40
|
-
return False
|
|
41
|
-
with open(MOTD_TRACK_FILE, "r") as f:
|
|
42
|
-
seen_versions = {line.strip() for line in f if line.strip()}
|
|
43
|
-
return version in seen_versions
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def mark_motd_seen(version: str):
|
|
47
|
-
os.makedirs(os.path.dirname(MOTD_TRACK_FILE), exist_ok=True)
|
|
48
|
-
with open(MOTD_TRACK_FILE, "a") as f:
|
|
49
|
-
f.write(f"{version}\n")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def print_motd(console, force: bool = False) -> bool:
|
|
53
|
-
if force or not has_seen_motd(MOTD_VERSION):
|
|
54
|
-
console.print(MOTD_MESSAGE)
|
|
55
|
-
mark_motd_seen(MOTD_VERSION)
|
|
56
|
-
return True
|
|
57
|
-
return False
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from typing import Dict
|
|
2
|
-
|
|
3
|
-
import requests
|
|
4
|
-
from pydantic_ai import RunContext
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def register_web_search_tools(agent):
|
|
8
|
-
@agent.tool
|
|
9
|
-
def grab_json_from_url(context: RunContext, url: str) -> Dict:
|
|
10
|
-
from code_puppy.tools.common import console
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
response = requests.get(url)
|
|
14
|
-
response.raise_for_status()
|
|
15
|
-
ct = response.headers.get("Content-Type")
|
|
16
|
-
if "json" not in str(ct):
|
|
17
|
-
console.print(
|
|
18
|
-
f"[bold red]Error:[/bold red] Response from {url} is not JSON (got {ct})"
|
|
19
|
-
)
|
|
20
|
-
return {"error": f"Response from {url} is not of type application/json"}
|
|
21
|
-
json_data = response.json()
|
|
22
|
-
if isinstance(json_data, list) and len(json_data) > 1000:
|
|
23
|
-
console.print("[yellow]Result list truncated to 1000 items[/yellow]")
|
|
24
|
-
return json_data[:1000]
|
|
25
|
-
if not json_data:
|
|
26
|
-
console.print("[yellow]No data found for URL:[/yellow]", url)
|
|
27
|
-
else:
|
|
28
|
-
console.print(f"[green]Successfully fetched JSON from:[/green] {url}")
|
|
29
|
-
return json_data
|
|
30
|
-
except Exception as exc:
|
|
31
|
-
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
32
|
-
return {"error": str(exc)}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_puppy-0.0.75 → code_puppy-0.0.76}/code_puppy/command_line/prompt_toolkit_completion.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|