code-puppy 0.0.59__py3-none-any.whl → 0.0.61__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/agent.py +15 -4
- code_puppy/agent_prompts.py +2 -1
- code_puppy/command_line/meta_command_handler.py +17 -12
- code_puppy/command_line/prompt_toolkit_completion.py +43 -36
- code_puppy/model_factory.py +0 -108
- code_puppy/models.json +4 -31
- code_puppy/tools/command_runner.py +7 -2
- code_puppy/tools/common.py +51 -0
- code_puppy/tools/file_modifications.py +137 -130
- code_puppy/tools/file_operations.py +19 -28
- code_puppy/tools/ts_code_map.py +393 -0
- {code_puppy-0.0.59.data → code_puppy-0.0.61.data}/data/code_puppy/models.json +4 -31
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/METADATA +3 -1
- code_puppy-0.0.61.dist-info/RECORD +28 -0
- code_puppy/tools/code_map.py +0 -92
- code_puppy-0.0.59.dist-info/RECORD +0 -28
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/licenses/LICENSE +0 -0
|
@@ -205,145 +205,152 @@ def _write_to_file(
|
|
|
205
205
|
return {"error": str(exc), "diff": ""}
|
|
206
206
|
|
|
207
207
|
|
|
208
|
-
def
|
|
209
|
-
|
|
208
|
+
def delete_snippet_from_file(
|
|
209
|
+
context: RunContext, file_path: str, snippet: str
|
|
210
|
+
) -> Dict[str, Any]:
|
|
211
|
+
console.log(f"🗑️ Deleting snippet from file [bold red]{file_path}[/bold red]")
|
|
212
|
+
res = _delete_snippet_from_file(context, file_path, snippet)
|
|
213
|
+
diff = res.get("diff", "")
|
|
214
|
+
if diff:
|
|
215
|
+
_print_diff(diff)
|
|
216
|
+
return res
|
|
210
217
|
|
|
211
|
-
def delete_snippet_from_file(
|
|
212
|
-
context: RunContext, file_path: str, snippet: str
|
|
213
|
-
) -> Dict[str, Any]:
|
|
214
|
-
console.log(f"🗑️ Deleting snippet from file [bold red]{file_path}[/bold red]")
|
|
215
|
-
res = _delete_snippet_from_file(context, file_path, snippet)
|
|
216
|
-
diff = res.get("diff", "")
|
|
217
|
-
if diff:
|
|
218
|
-
_print_diff(diff)
|
|
219
|
-
return res
|
|
220
|
-
|
|
221
|
-
def write_to_file(
|
|
222
|
-
context: RunContext, path: str, content: str, overwrite: bool
|
|
223
|
-
) -> Dict[str, Any]:
|
|
224
|
-
console.log(f"✏️ Writing file [bold blue]{path}[/bold blue]")
|
|
225
|
-
res = _write_to_file(context, path, content, overwrite=overwrite)
|
|
226
|
-
diff = res.get("diff", "")
|
|
227
|
-
if diff:
|
|
228
|
-
_print_diff(diff)
|
|
229
|
-
return res
|
|
230
|
-
|
|
231
|
-
def replace_in_file(
|
|
232
|
-
context: RunContext, path: str, replacements: List[Dict[str, str]]
|
|
233
|
-
) -> Dict[str, Any]:
|
|
234
|
-
console.log(f"♻️ Replacing text in [bold yellow]{path}[/bold yellow]")
|
|
235
|
-
res = _replace_in_file(context, path, replacements)
|
|
236
|
-
diff = res.get("diff", "")
|
|
237
|
-
if diff:
|
|
238
|
-
_print_diff(diff)
|
|
239
|
-
return res
|
|
240
218
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
219
|
+
def write_to_file(
|
|
220
|
+
context: RunContext, path: str, content: str, overwrite: bool
|
|
221
|
+
) -> Dict[str, Any]:
|
|
222
|
+
console.log(f"✏️ Writing file [bold blue]{path}[/bold blue]")
|
|
223
|
+
res = _write_to_file(context, path, content, overwrite=overwrite)
|
|
224
|
+
diff = res.get("diff", "")
|
|
225
|
+
if diff:
|
|
226
|
+
_print_diff(diff)
|
|
227
|
+
return res
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def replace_in_file(
|
|
231
|
+
context: RunContext, path: str, replacements: List[Dict[str, str]]
|
|
232
|
+
) -> Dict[str, Any]:
|
|
233
|
+
console.log(f"♻️ Replacing text in [bold yellow]{path}[/bold yellow]")
|
|
234
|
+
res = _replace_in_file(context, path, replacements)
|
|
235
|
+
diff = res.get("diff", "")
|
|
236
|
+
if diff:
|
|
237
|
+
_print_diff(diff)
|
|
238
|
+
return res
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _edit_file(context: RunContext, path: str, diff: str) -> Dict[str, Any]:
|
|
242
|
+
"""
|
|
243
|
+
Unified file editing tool that can:
|
|
244
|
+
- Create/write a new file when the target does not exist (using raw content or a JSON payload with a "content" key)
|
|
245
|
+
- Replace text within an existing file via a JSON payload with "replacements" (delegates to internal replace logic)
|
|
246
|
+
- Delete a snippet from an existing file via a JSON payload with "delete_snippet"
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
path : str
|
|
250
|
+
Path to the target file (relative or absolute)
|
|
251
|
+
diff : str
|
|
252
|
+
Either:
|
|
253
|
+
* Raw file content (for file creation)
|
|
254
|
+
* A JSON string with one of the following shapes:
|
|
255
|
+
{"content": "full file contents", "overwrite": true}
|
|
256
|
+
{"replacements": [ {"old_str": "foo", "new_str": "bar"}, ... ] }
|
|
257
|
+
{"delete_snippet": "text to remove"}
|
|
258
|
+
The function auto-detects the payload type and routes to the appropriate internal helper.
|
|
259
|
+
"""
|
|
260
|
+
console.print("\n[bold white on blue] EDIT FILE [/bold white on blue]")
|
|
261
|
+
file_path = os.path.abspath(path)
|
|
262
|
+
try:
|
|
263
|
+
parsed_payload = json.loads(diff)
|
|
264
|
+
except json.JSONDecodeError:
|
|
285
265
|
try:
|
|
286
|
-
if isinstance(parsed_payload, dict):
|
|
287
|
-
if "delete_snippet" in parsed_payload:
|
|
288
|
-
snippet = parsed_payload["delete_snippet"]
|
|
289
|
-
return delete_snippet_from_file(context, file_path, snippet)
|
|
290
|
-
if "replacements" in parsed_payload:
|
|
291
|
-
replacements = parsed_payload["replacements"]
|
|
292
|
-
return replace_in_file(context, file_path, replacements)
|
|
293
|
-
if "content" in parsed_payload:
|
|
294
|
-
content = parsed_payload["content"]
|
|
295
|
-
overwrite = bool(parsed_payload.get("overwrite", False))
|
|
296
|
-
file_exists = os.path.exists(file_path)
|
|
297
|
-
if file_exists and not overwrite:
|
|
298
|
-
return {
|
|
299
|
-
"success": False,
|
|
300
|
-
"path": file_path,
|
|
301
|
-
"message": f"File '{file_path}' exists. Set 'overwrite': true to replace.",
|
|
302
|
-
"changed": False,
|
|
303
|
-
}
|
|
304
|
-
return write_to_file(context, file_path, content, overwrite)
|
|
305
|
-
return write_to_file(context, file_path, diff, overwrite=False)
|
|
306
|
-
except Exception as e:
|
|
307
266
|
console.print(
|
|
308
|
-
"[bold
|
|
267
|
+
"[bold yellow] JSON Parsing Failed! TRYING TO REPAIR! [/bold yellow]"
|
|
309
268
|
)
|
|
310
|
-
|
|
269
|
+
parsed_payload = json.loads(repair_json(diff))
|
|
270
|
+
console.print("[bold white on blue] SUCCESS - WOOF! [/bold white on blue]")
|
|
271
|
+
except Exception as e:
|
|
272
|
+
console.print(f"[bold red] Unable to parse diff [/bold red] -- {str(e)}")
|
|
311
273
|
return {
|
|
312
274
|
"success": False,
|
|
313
275
|
"path": file_path,
|
|
314
|
-
"message": f"
|
|
276
|
+
"message": f"Unable to parse diff JSON -- {str(e)}",
|
|
315
277
|
"changed": False,
|
|
278
|
+
"diff": "",
|
|
279
|
+
}
|
|
280
|
+
try:
|
|
281
|
+
if isinstance(parsed_payload, dict):
|
|
282
|
+
if "delete_snippet" in parsed_payload:
|
|
283
|
+
snippet = parsed_payload["delete_snippet"]
|
|
284
|
+
return delete_snippet_from_file(context, file_path, snippet)
|
|
285
|
+
if "replacements" in parsed_payload:
|
|
286
|
+
replacements = parsed_payload["replacements"]
|
|
287
|
+
return replace_in_file(context, file_path, replacements)
|
|
288
|
+
if "content" in parsed_payload:
|
|
289
|
+
content = parsed_payload["content"]
|
|
290
|
+
overwrite = bool(parsed_payload.get("overwrite", False))
|
|
291
|
+
file_exists = os.path.exists(file_path)
|
|
292
|
+
if file_exists and not overwrite:
|
|
293
|
+
return {
|
|
294
|
+
"success": False,
|
|
295
|
+
"path": file_path,
|
|
296
|
+
"message": f"File '{file_path}' exists. Set 'overwrite': true to replace.",
|
|
297
|
+
"changed": False,
|
|
298
|
+
}
|
|
299
|
+
return write_to_file(context, file_path, content, overwrite)
|
|
300
|
+
return write_to_file(context, file_path, diff, overwrite=False)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
console.print(
|
|
303
|
+
"[bold red] Unable to route file modification tool call to sub-tool [/bold red]"
|
|
304
|
+
)
|
|
305
|
+
console.print(str(e))
|
|
306
|
+
return {
|
|
307
|
+
"success": False,
|
|
308
|
+
"path": file_path,
|
|
309
|
+
"message": f"Something went wrong in file editing: {str(e)}",
|
|
310
|
+
"changed": False,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
|
|
315
|
+
console.log(f"🗑️ Deleting file [bold red]{file_path}[/bold red]")
|
|
316
|
+
file_path = os.path.abspath(file_path)
|
|
317
|
+
try:
|
|
318
|
+
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
|
319
|
+
res = {"error": f"File '{file_path}' does not exist.", "diff": ""}
|
|
320
|
+
else:
|
|
321
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
322
|
+
original = f.read()
|
|
323
|
+
diff_text = "".join(
|
|
324
|
+
difflib.unified_diff(
|
|
325
|
+
original.splitlines(keepends=True),
|
|
326
|
+
[],
|
|
327
|
+
fromfile=f"a/{os.path.basename(file_path)}",
|
|
328
|
+
tofile=f"b/{os.path.basename(file_path)}",
|
|
329
|
+
n=3,
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
os.remove(file_path)
|
|
333
|
+
res = {
|
|
334
|
+
"success": True,
|
|
335
|
+
"path": file_path,
|
|
336
|
+
"message": f"File '{file_path}' deleted successfully.",
|
|
337
|
+
"changed": True,
|
|
338
|
+
"diff": diff_text,
|
|
316
339
|
}
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
_log_error("Unhandled exception in delete_file", exc)
|
|
342
|
+
res = {"error": str(exc), "diff": ""}
|
|
343
|
+
_print_diff(res.get("diff", ""))
|
|
344
|
+
return res
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def register_file_modifications_tools(agent):
|
|
348
|
+
"""Attach file-editing tools to *agent* with mandatory diff rendering."""
|
|
349
|
+
|
|
350
|
+
@agent.tool(retries=5)
|
|
351
|
+
def edit_file(context: RunContext, path: str, diff: str) -> Dict[str, Any]:
|
|
352
|
+
return _edit_file(context, path, diff)
|
|
317
353
|
|
|
318
|
-
@agent.tool
|
|
354
|
+
@agent.tool(retries=5)
|
|
319
355
|
def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
|
|
320
|
-
|
|
321
|
-
file_path = os.path.abspath(file_path)
|
|
322
|
-
try:
|
|
323
|
-
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
|
324
|
-
res = {"error": f"File '{file_path}' does not exist.", "diff": ""}
|
|
325
|
-
else:
|
|
326
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
327
|
-
original = f.read()
|
|
328
|
-
diff_text = "".join(
|
|
329
|
-
difflib.unified_diff(
|
|
330
|
-
original.splitlines(keepends=True),
|
|
331
|
-
[],
|
|
332
|
-
fromfile=f"a/{os.path.basename(file_path)}",
|
|
333
|
-
tofile=f"b/{os.path.basename(file_path)}",
|
|
334
|
-
n=3,
|
|
335
|
-
)
|
|
336
|
-
)
|
|
337
|
-
os.remove(file_path)
|
|
338
|
-
res = {
|
|
339
|
-
"success": True,
|
|
340
|
-
"path": file_path,
|
|
341
|
-
"message": f"File '{file_path}' deleted successfully.",
|
|
342
|
-
"changed": True,
|
|
343
|
-
"diff": diff_text,
|
|
344
|
-
}
|
|
345
|
-
except Exception as exc:
|
|
346
|
-
_log_error("Unhandled exception in delete_file", exc)
|
|
347
|
-
res = {"error": str(exc), "diff": ""}
|
|
348
|
-
_print_diff(res.get("diff", ""))
|
|
349
|
-
return res
|
|
356
|
+
return _delete_file(context, file_path)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# file_operations.py
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import os
|
|
4
4
|
from typing import Any, Dict, List
|
|
5
5
|
|
|
@@ -10,33 +10,7 @@ from code_puppy.tools.common import console
|
|
|
10
10
|
# ---------------------------------------------------------------------------
|
|
11
11
|
# Module-level helper functions (exposed for unit tests _and_ used as tools)
|
|
12
12
|
# ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
"**/node_modules/**",
|
|
15
|
-
"**/.git/**",
|
|
16
|
-
"**/__pycache__/**",
|
|
17
|
-
"**/.DS_Store",
|
|
18
|
-
"**/.env",
|
|
19
|
-
"**/.venv/**",
|
|
20
|
-
"**/venv/**",
|
|
21
|
-
"**/.idea/**",
|
|
22
|
-
"**/.vscode/**",
|
|
23
|
-
"**/dist/**",
|
|
24
|
-
"**/build/**",
|
|
25
|
-
"**/*.pyc",
|
|
26
|
-
"**/*.pyo",
|
|
27
|
-
"**/*.pyd",
|
|
28
|
-
"**/*.so",
|
|
29
|
-
"**/*.dll",
|
|
30
|
-
"**/*.exe",
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def should_ignore_path(path: str) -> bool:
|
|
35
|
-
"""Return True if *path* matches any pattern in IGNORE_PATTERNS."""
|
|
36
|
-
for pattern in IGNORE_PATTERNS:
|
|
37
|
-
if fnmatch.fnmatch(path, pattern):
|
|
38
|
-
return True
|
|
39
|
-
return False
|
|
13
|
+
from code_puppy.tools.common import should_ignore_path
|
|
40
14
|
|
|
41
15
|
|
|
42
16
|
def _list_files(
|
|
@@ -306,3 +280,20 @@ def register_file_operations_tools(agent):
|
|
|
306
280
|
context: RunContext, search_string: str, directory: str = "."
|
|
307
281
|
) -> List[Dict[str, Any]]:
|
|
308
282
|
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
|