code-puppy 0.0.30__py3-none-any.whl → 0.0.32__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,338 +4,188 @@ import difflib
4
4
  import json
5
5
  from code_puppy.tools.common import console
6
6
  from typing import Dict, Any, List
7
- from code_puppy.agent import code_generation_agent
8
7
  from pydantic_ai import RunContext
9
8
 
10
-
11
- @code_generation_agent.tool
12
- def delete_snippet_from_file(
13
- context: RunContext, file_path: str, snippet: str
14
- ) -> Dict[str, Any]:
15
- console.log(f"🗑️ Deleting snippet from file [bold red]{file_path}[/bold red]")
16
- """Delete a snippet from a file at the given file path.
17
-
18
- Args:
19
- file_path: Path to the file to delete.
20
- snippet: The snippet to delete.
21
-
22
- Returns:
23
- A dictionary with status and message about the operation.
24
- """
25
- file_path = os.path.abspath(file_path)
26
-
27
- console.print("\n[bold white on red] SNIPPET DELETION [/bold white on red]")
28
- console.print(f"[bold yellow]From file:[/bold yellow] {file_path}")
29
-
30
- try:
31
- # Check if the file exists
32
- if not os.path.exists(file_path):
33
- console.print(
34
- f"[bold red]Error:[/bold red] File '{file_path}' does not exist"
35
- )
9
+ def register_file_modifications_tools(agent):
10
+ @agent.tool
11
+ def delete_snippet_from_file(context: RunContext, file_path: str, snippet: str) -> Dict[str, Any]:
12
+ console.log(f"🗑️ Deleting snippet from file [bold red]{file_path}[/bold red]")
13
+ file_path = os.path.abspath(file_path)
14
+ console.print("\n[bold white on red] SNIPPET DELETION [/bold white on red]")
15
+ console.print(f"[bold yellow]From file:[/bold yellow] {file_path}")
16
+ try:
17
+ if not os.path.exists(file_path):
18
+ console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
19
+ return {"error": f"File '{file_path}' does not exist."}
20
+ if not os.path.isfile(file_path):
21
+ return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
22
+ with open(file_path, "r", encoding="utf-8") as f:
23
+ content = f.read()
24
+ if snippet not in content:
25
+ console.print(f"[bold red]Error:[/bold red] Snippet not found in file '{file_path}'")
26
+ return {"error": f"Snippet not found in file '{file_path}'."}
27
+ modified_content = content.replace(snippet, "")
28
+ diff_lines = list(difflib.unified_diff(content.splitlines(keepends=True), modified_content.splitlines(keepends=True), fromfile=f"a/{os.path.basename(file_path)}", tofile=f"b/{os.path.basename(file_path)}", n=3))
29
+ diff_text = "".join(diff_lines)
30
+ console.print("[bold cyan]Changes to be applied:[/bold cyan]")
31
+ if diff_text.strip():
32
+ formatted_diff = ""
33
+ for line in diff_lines:
34
+ if line.startswith("+") and not line.startswith("+++"):
35
+ formatted_diff += f"[bold green]{line}[/bold green]"
36
+ elif line.startswith("-") and not line.startswith("---"):
37
+ formatted_diff += f"[bold red]{line}[/bold red]"
38
+ elif line.startswith("@"):
39
+ formatted_diff += f"[bold cyan]{line}[/bold cyan]"
40
+ else:
41
+ formatted_diff += line
42
+ console.print(formatted_diff)
43
+ else:
44
+ console.print("[dim]No changes detected[/dim]")
45
+ return {"success": False, "path": file_path, "message": "No changes needed.", "diff": ""}
46
+ with open(file_path, "w", encoding="utf-8") as f:
47
+ f.write(modified_content)
48
+ return {"success": True, "path": file_path, "message": f"Snippet deleted from file '{file_path}'.", "diff": diff_text}
49
+ except PermissionError:
50
+ return {"error": f"Permission denied to delete '{file_path}'."}
51
+ except FileNotFoundError:
36
52
  return {"error": f"File '{file_path}' does not exist."}
53
+ except Exception as e:
54
+ return {"error": f"Error deleting file '{file_path}': {str(e)}"}
37
55
 
38
- # Check if it's a file (not a directory)
39
- if not os.path.isfile(file_path):
40
- console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
41
- return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
42
-
43
- # Read the file content
44
- with open(file_path, "r", encoding="utf-8") as f:
45
- content = f.read()
46
-
47
- # Check if the snippet exists in the file
48
- if snippet not in content:
49
- console.print(
50
- f"[bold red]Error:[/bold red] Snippet not found in file '{file_path}'"
51
- )
52
- return {"error": f"Snippet not found in file '{file_path}'."}
53
-
54
- # Remove the snippet from the file content
55
- modified_content = content.replace(snippet, "")
56
-
57
- # Generate a diff
58
- diff_lines = list(
59
- difflib.unified_diff(
60
- content.splitlines(keepends=True),
61
- modified_content.splitlines(keepends=True),
62
- fromfile=f"a/{os.path.basename(file_path)}",
63
- tofile=f"b/{os.path.basename(file_path)}",
64
- n=3, # Context lines
65
- )
66
- )
67
-
68
- diff_text = "".join(diff_lines)
69
-
70
- # Display the diff
71
- console.print("[bold cyan]Changes to be applied:[/bold cyan]")
72
-
73
- if diff_text.strip():
74
- # Format the diff for display with colorization
75
- formatted_diff = ""
76
- for line in diff_lines:
77
- if line.startswith("+") and not line.startswith("+++"):
78
- formatted_diff += f"[bold green]{line}[/bold green]"
79
- elif line.startswith("-") and not line.startswith("---"):
80
- formatted_diff += f"[bold red]{line}[/bold red]"
81
- elif line.startswith("@"):
82
- formatted_diff += f"[bold cyan]{line}[/bold cyan]"
83
- else:
84
- formatted_diff += line
85
-
86
- console.print(formatted_diff)
87
- else:
88
- console.print("[dim]No changes detected[/dim]")
89
- return {
90
- "success": False,
91
- "path": file_path,
92
- "message": "No changes needed.",
93
- "diff": "",
94
- }
95
-
96
- # Write the modified content back to the file
97
- with open(file_path, "w", encoding="utf-8") as f:
98
- f.write(modified_content)
99
-
100
- return {
101
- "success": True,
102
- "path": file_path,
103
- "message": f"Snippet deleted from file '{file_path}'.",
104
- "diff": diff_text,
105
- }
106
- except PermissionError:
107
- return {"error": f"Permission denied to delete '{file_path}'."}
108
- except FileNotFoundError:
109
- # This should be caught by the initial check, but just in case
110
- return {"error": f"File '{file_path}' does not exist."}
111
- except Exception as e:
112
- return {"error": f"Error deleting file '{file_path}': {str(e)}"}
113
-
114
-
115
- @code_generation_agent.tool
116
- def write_to_file(
117
- context: RunContext,
118
- path: str,
119
- content: str
120
- ) -> Dict[str, Any]:
121
- """Write content to a file at the specified path.
122
-
123
- If the file exists, it will be overwritten with the provided content.
124
- If the file doesn't exist, it will be created.
125
- This function will automatically create any directories needed to write the file.
126
-
127
- Args:
128
- path: The path of the file to write to (relative to the current working directory)
129
- content: The content to write to the file. ALWAYS provide the COMPLETE intended content of the file.
130
-
131
- Returns:
132
- A dictionary with status and message about the operation.
133
- """
134
- try:
135
- # Convert to absolute path if not already
136
- file_path = os.path.abspath(path)
137
-
138
- # Create directories if they don't exist
139
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
140
-
141
- # Display information
142
- console.print("\n[bold white on blue] FILE WRITE [/bold white on blue]")
143
- console.print(f"[bold yellow]Writing to:[/bold yellow] {file_path}")
144
-
145
- # Check if file exists
146
- file_exists = os.path.exists(file_path)
147
- if file_exists:
148
- console.print(f'[bold red]Refusing to overwrite existing file:[/bold red] {file_path}')
149
- return {
150
- 'success': False,
151
- 'path': file_path,
152
- 'message': f'Cowardly refusing to overwrite existing file: {file_path}',
153
- 'changed': False,
154
- }
155
-
156
- # Show the content to be written
157
- trimmed_content = content
158
- max_preview = 1000
159
- if len(content) > max_preview:
160
- trimmed_content = content[:max_preview] + '... [truncated]'
161
- console.print('[bold magenta]Content to be written:[/bold magenta]')
162
- console.print(trimmed_content, highlight=False)
163
-
164
- # Write the content to the file
165
- with open(file_path, 'w', encoding='utf-8') as f:
166
- f.write(content)
167
-
168
- action = "updated" if file_exists else "created"
169
- return {
170
- "success": True,
171
- "path": file_path,
172
- "message": f"File '{file_path}' {action} successfully.",
173
- "diff": trimmed_content,
174
- "changed": True,
175
- }
176
-
177
- except Exception as e:
178
- console.print(f"[bold red]Error:[/bold red] {str(e)}")
179
- return {"error": f"Error writing to file '{path}': {str(e)}"}
180
-
181
-
182
- @code_generation_agent.tool(retries=5)
183
- def replace_in_file(
184
- context: RunContext,
185
- path: str,
186
- diff: str
187
- ) -> Dict[str, Any]:
188
- """Replace text in a file based on a JSON-formatted replacements object.
189
-
190
- Args:
191
- path: The path of the file to modify
192
- diff: A JSON string containing replacements, formatted as:
193
- {"replacements": [{"old_str": "text to find", "new_str": "replacement"}]}
194
-
195
- Returns:
196
- A dictionary with status and message about the operation.
197
- """
198
- try:
199
- # Convert to absolute path if not already
200
- file_path = os.path.abspath(path)
201
-
202
- # Display information
203
- console.print("\n[bold white on yellow] FILE REPLACEMENTS [/bold white on yellow]")
204
- console.print(f"[bold yellow]Modifying:[/bold yellow] {file_path}")
205
-
206
- # Check if the file exists
207
- if not os.path.exists(file_path):
208
- console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
209
- return {"error": f"File '{file_path}' does not exist"}
210
-
211
- if not os.path.isfile(file_path):
212
- console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
213
- return {"error": f"'{file_path}' is not a file."}
214
-
215
- # Parse the JSON replacements
56
+ @agent.tool
57
+ def write_to_file(context: RunContext, path: str, content: str) -> Dict[str, Any]:
216
58
  try:
217
- replacements_data = json.loads(diff)
218
- replacements = replacements_data.get("replacements", [])
219
-
220
- if not replacements:
221
- console.print("[bold red]Error:[/bold red] No replacements provided in the diff")
222
- return {"error": "No replacements provided in the diff"}
223
- except json.JSONDecodeError as e:
224
- console.print(f"[bold red]Error:[/bold red] Invalid JSON in diff: {str(e)}")
225
- return {"error": f"Invalid JSON in diff: {str(e)}"}
226
-
227
- # Read the current file content
228
- with open(file_path, "r", encoding="utf-8") as f:
229
- current_content = f.read()
230
-
231
- # Apply all replacements
232
- modified_content = current_content
233
- applied_replacements = []
234
-
235
- for i, replacement in enumerate(replacements, 1):
236
- old_str = replacement.get("old_str", "")
237
- new_str = replacement.get("new_str", "")
238
-
239
- if not old_str:
240
- console.print(f"[bold yellow]Warning:[/bold yellow] Replacement #{i} has empty old_str")
241
- continue
242
-
243
- if old_str not in modified_content:
244
- console.print(f"[bold red]Error:[/bold red] Text not found in file: {old_str[:50]}...")
245
- return {"error": f"Text to replace not found in file (replacement #{i})"}
246
-
247
- # Apply the replacement
248
- modified_content = modified_content.replace(old_str, new_str)
249
- applied_replacements.append({"old_str": old_str, "new_str": new_str})
250
-
251
- # Generate a diff for display
252
- diff_lines = list(
253
- difflib.unified_diff(
254
- current_content.splitlines(keepends=True),
255
- modified_content.splitlines(keepends=True),
256
- fromfile=f"a/{os.path.basename(file_path)}",
257
- tofile=f"b/{os.path.basename(file_path)}",
258
- n=3,
259
- )
260
- )
261
- diff_text = "".join(diff_lines)
262
-
263
- # Display the diff
264
- console.print("[bold cyan]Changes to be applied:[/bold cyan]")
265
- if diff_text.strip():
266
- formatted_diff = ""
267
- for line in diff_lines:
268
- if line.startswith("+") and not line.startswith("+++"):
269
- formatted_diff += f"[bold green]{line}[/bold green]"
270
- elif line.startswith("-") and not line.startswith("---"):
271
- formatted_diff += f"[bold red]{line}[/bold red]"
272
- elif line.startswith("@"):
273
- formatted_diff += f"[bold cyan]{line}[/bold cyan]"
274
- else:
275
- formatted_diff += line
276
- console.print(formatted_diff)
277
- else:
278
- console.print("[dim]No changes detected - file content is identical[/dim]")
279
- return {
280
- "success": False,
281
- "path": file_path,
282
- "message": "No changes to apply.",
283
- "diff": "",
284
- "changed": False,
285
- }
286
-
287
- # Write the modified content to the file
288
- with open(file_path, "w", encoding="utf-8") as f:
289
- f.write(modified_content)
290
-
291
- return {
292
- "success": True,
293
- "path": file_path,
294
- "message": f"Applied {len(applied_replacements)} replacements to '{file_path}'",
295
- "diff": diff_text,
296
- "changed": True,
297
- "replacements_applied": len(applied_replacements)
298
- }
299
-
300
- except Exception as e:
301
- console.print(f"[bold red]Error:[/bold red] {str(e)}")
302
- return {"error": f"Error replacing in file '{path}': {str(e)}"}
303
-
304
-
305
- @code_generation_agent.tool
306
- def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
307
- console.log(f"🗑️ Deleting file [bold red]{file_path}[/bold red]")
308
- """Delete a file at the given file path.
309
-
310
- Args:
311
- file_path: Path to the file to delete.
312
-
313
- Returns:
314
- A dictionary with status and message about the operation.
315
- """
316
- file_path = os.path.abspath(file_path)
317
-
318
- try:
319
- # Check if the file exists
320
- if not os.path.exists(file_path):
59
+ file_path = os.path.abspath(path)
60
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
61
+ console.print("\n[bold white on blue] FILE WRITE [/bold white on blue]")
62
+ console.print(f"[bold yellow]Writing to:[/bold yellow] {file_path}")
63
+ file_exists = os.path.exists(file_path)
64
+ if file_exists:
65
+ console.print(f'[bold red]Refusing to overwrite existing file:[/bold red] {file_path}')
66
+ return {'success': False,'path': file_path,'message': f'Cowardly refusing to overwrite existing file: {file_path}','changed': False,}
67
+ trimmed_content = content
68
+ max_preview = 1000
69
+ if len(content) > max_preview:
70
+ trimmed_content = content[:max_preview] + '... [truncated]'
71
+ console.print('[bold magenta]Content to be written:[/bold magenta]')
72
+ console.print(trimmed_content, highlight=False)
73
+ with open(file_path, 'w', encoding='utf-8') as f:
74
+ f.write(content)
75
+ action = "updated" if file_exists else "created"
76
+ return {"success": True,"path": file_path,"message": f"File '{file_path}' {action} successfully.","diff": trimmed_content,"changed": True,}
77
+ except Exception as e:
78
+ console.print(f"[bold red]Error:[/bold red] {str(e)}")
79
+ return {"error": f"Error writing to file '{path}': {str(e)}"}
80
+
81
+ @agent.tool(retries=5)
82
+ def replace_in_file(context: RunContext, path: str, diff: str) -> Dict[str, Any]:
83
+ try:
84
+ file_path = os.path.abspath(path)
85
+ console.print("\n[bold white on yellow] FILE REPLACEMENTS [/bold white on yellow]")
86
+ console.print(f"[bold yellow]Modifying:[/bold yellow] {file_path}")
87
+ if not os.path.exists(file_path):
88
+ console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
89
+ return {"error": f"File '{file_path}' does not exist"}
90
+ if not os.path.isfile(file_path):
91
+ return {"error": f"'{file_path}' is not a file."}
92
+ # ------------------------------------------------------------------
93
+ # Robust parsing of the diff argument
94
+ # The agent sometimes sends single-quoted or otherwise invalid JSON.
95
+ # Attempt to recover by trying several strategies before giving up.
96
+ # ------------------------------------------------------------------
97
+ parsed_successfully = False
98
+ replacements: List[Dict[str, str]] = []
99
+ try:
100
+ replacements_data = json.loads(diff)
101
+ replacements = replacements_data.get("replacements", [])
102
+ parsed_successfully = True
103
+ except json.JSONDecodeError:
104
+ # Fallback 1: convert single quotes to double quotes and retry
105
+ try:
106
+ sanitized = diff.replace("'", '"')
107
+ replacements_data = json.loads(sanitized)
108
+ replacements = replacements_data.get("replacements", [])
109
+ parsed_successfully = True
110
+ except json.JSONDecodeError:
111
+ # Fallback 2: attempt Python literal eval
112
+ try:
113
+ import ast
114
+ replacements_data = ast.literal_eval(diff)
115
+ if isinstance(replacements_data, dict):
116
+ replacements = replacements_data.get("replacements", []) if "replacements" in replacements_data else []
117
+ # If dict keys look like a single replacement, wrap it
118
+ if not replacements:
119
+ # maybe it's already {"old_str": ..., "new_str": ...}
120
+ if all(k in replacements_data for k in ("old_str", "new_str")):
121
+ replacements = [
122
+ {
123
+ "old_str": replacements_data["old_str"],
124
+ "new_str": replacements_data["new_str"],
125
+ }
126
+ ]
127
+ parsed_successfully = True
128
+ except Exception as e2:
129
+ console.print(
130
+ f"[bold red]Error:[/bold red] Could not parse diff as JSON or Python literal. Reason: {e2}"
131
+ )
132
+ if not parsed_successfully or not replacements:
133
+ console.print("[bold red]Error:[/bold red] No valid replacements found in the diff after all parsing attempts")
134
+ return {"error": "No valid replacements found in the diff"}
135
+ with open(file_path, "r", encoding="utf-8") as f:
136
+ current_content = f.read()
137
+ modified_content = current_content
138
+ applied_replacements = []
139
+ for i, replacement in enumerate(replacements, 1):
140
+ old_str = replacement.get("old_str", "")
141
+ new_str = replacement.get("new_str", "")
142
+ if not old_str:
143
+ console.print(f"[bold yellow]Warning:[/bold yellow] Replacement #{i} has empty old_str")
144
+ continue
145
+ if old_str not in modified_content:
146
+ console.print(f"[bold red]Error:[/bold red] Text not found in file: {old_str[:50]}...")
147
+ return {"error": f"Text to replace not found in file (replacement #{i})"}
148
+ modified_content = modified_content.replace(old_str, new_str)
149
+ applied_replacements.append({"old_str": old_str, "new_str": new_str})
150
+ diff_lines = list(difflib.unified_diff(current_content.splitlines(keepends=True), modified_content.splitlines(keepends=True), fromfile=f"a/{os.path.basename(file_path)}", tofile=f"b/{os.path.basename(file_path)}", n=3))
151
+ diff_text = "".join(diff_lines)
152
+ console.print("[bold cyan]Changes to be applied:[/bold cyan]")
153
+ if diff_text.strip():
154
+ formatted_diff = ""
155
+ for line in diff_lines:
156
+ if line.startswith("+") and not line.startswith("+++"):
157
+ formatted_diff += f"[bold green]{line}[/bold green]"
158
+ elif line.startswith("-") and not line.startswith("---"):
159
+ formatted_diff += f"[bold red]{line}[/bold red]"
160
+ elif line.startswith("@"):
161
+ formatted_diff += f"[bold cyan]{line}[/bold cyan]"
162
+ else:
163
+ formatted_diff += line
164
+ console.print(formatted_diff)
165
+ else:
166
+ console.print("[dim]No changes detected - file content is identical[/dim]")
167
+ return {"success": False,"path": file_path,"message": "No changes to apply.","diff": "","changed": False,}
168
+ with open(file_path, "w", encoding="utf-8") as f:
169
+ f.write(modified_content)
170
+ return {"success": True,"path": file_path,"message": f"Applied {len(applied_replacements)} replacements to '{file_path}'","diff": diff_text,"changed": True,"replacements_applied": len(applied_replacements)}
171
+ except Exception as e:
172
+ console.print(f"[bold red]Error:[/bold red] {str(e)}")
173
+ return {"error": f"Error replacing in file '{path}': {str(e)}"}
174
+
175
+ @agent.tool
176
+ def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
177
+ console.log(f"🗑️ Deleting file [bold red]{file_path}[/bold red]")
178
+ file_path = os.path.abspath(file_path)
179
+ try:
180
+ if not os.path.exists(file_path):
181
+ return {"error": f"File '{file_path}' does not exist."}
182
+ if not os.path.isfile(file_path):
183
+ return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
184
+ os.remove(file_path)
185
+ return {"success": True,"path": file_path,"message": f"File '{file_path}' deleted successfully."}
186
+ except PermissionError:
187
+ return {"error": f"Permission denied to delete '{file_path}'."}
188
+ except FileNotFoundError:
321
189
  return {"error": f"File '{file_path}' does not exist."}
322
-
323
- # Check if it's a file (not a directory)
324
- if not os.path.isfile(file_path):
325
- return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
326
-
327
- # Attempt to delete the file
328
- os.remove(file_path)
329
-
330
- return {
331
- "success": True,
332
- "path": file_path,
333
- "message": f"File '{file_path}' deleted successfully.",
334
- }
335
- except PermissionError:
336
- return {"error": f"Permission denied to delete '{file_path}'."}
337
- except FileNotFoundError:
338
- # This should be caught by the initial check, but just in case
339
- return {"error": f"File '{file_path}' does not exist."}
340
- except Exception as e:
341
- return {"error": f"Error deleting file '{file_path}': {str(e)}"}
190
+ except Exception as e:
191
+ return {"error": f"Error deleting file '{file_path}': {str(e)}"}