code-puppy 0.0.287__py3-none-any.whl → 0.0.323__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 +3 -1
- code_puppy/agents/agent_code_puppy.py +5 -4
- code_puppy/agents/agent_creator_agent.py +22 -18
- code_puppy/agents/agent_manager.py +2 -2
- code_puppy/agents/base_agent.py +496 -102
- code_puppy/callbacks.py +8 -0
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +795 -0
- code_puppy/command_line/add_model_menu.py +19 -16
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +269 -41
- code_puppy/command_line/colors_menu.py +515 -0
- code_puppy/command_line/command_handler.py +10 -24
- code_puppy/command_line/config_commands.py +106 -25
- code_puppy/command_line/core_commands.py +32 -20
- code_puppy/command_line/mcp/add_command.py +3 -16
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +15 -15
- code_puppy/command_line/mcp/custom_server_form.py +66 -5
- code_puppy/command_line/mcp/custom_server_installer.py +17 -17
- code_puppy/command_line/mcp/edit_command.py +15 -22
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/help_command.py +2 -2
- code_puppy/command_line/mcp/install_command.py +10 -14
- code_puppy/command_line/mcp/install_menu.py +2 -6
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +12 -10
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +6 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/model_settings_menu.py +53 -7
- code_puppy/command_line/motd.py +1 -1
- code_puppy/command_line/pin_command_completion.py +82 -7
- code_puppy/command_line/prompt_toolkit_completion.py +32 -9
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +217 -53
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/keymap.py +126 -0
- code_puppy/main.py +5 -745
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +63 -36
- code_puppy/mcp_/captured_stdio_server.py +1 -1
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +25 -5
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/messaging/__init__.py +184 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +3 -3
- code_puppy/messaging/messages.py +470 -0
- code_puppy/messaging/renderers.py +43 -141
- code_puppy/messaging/rich_renderer.py +900 -0
- code_puppy/messaging/spinner/console_spinner.py +39 -2
- code_puppy/model_factory.py +292 -53
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +19 -5
- code_puppy/plugins/__init__.py +152 -10
- code_puppy/plugins/chatgpt_oauth/config.py +20 -12
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +30 -13
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/config.py +15 -11
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
- code_puppy/plugins/claude_code_oauth/utils.py +6 -1
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/oauth_puppy_html.py +3 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -134
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +77 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +7 -5
- code_puppy/terminal_utils.py +126 -0
- code_puppy/tools/agent_tools.py +131 -70
- code_puppy/tools/browser/browser_control.py +10 -14
- code_puppy/tools/browser/browser_interactions.py +20 -28
- code_puppy/tools/browser/browser_locators.py +27 -29
- code_puppy/tools/browser/browser_navigation.py +9 -9
- code_puppy/tools/browser/browser_screenshot.py +12 -14
- code_puppy/tools/browser/browser_scripts.py +17 -29
- code_puppy/tools/browser/browser_workflows.py +24 -25
- code_puppy/tools/browser/camoufox_manager.py +22 -26
- code_puppy/tools/command_runner.py +410 -88
- code_puppy/tools/common.py +51 -38
- code_puppy/tools/file_modifications.py +98 -24
- code_puppy/tools/file_operations.py +113 -202
- code_puppy/version_checker.py +28 -13
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/METADATA +3 -8
- code_puppy-0.0.323.dist-info/RECORD +168 -0
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.287.dist-info/RECORD +0 -153
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,13 +12,14 @@ from pydantic_ai import RunContext
|
|
|
12
12
|
# ---------------------------------------------------------------------------
|
|
13
13
|
# Module-level helper functions (exposed for unit tests _and_ used as tools)
|
|
14
14
|
# ---------------------------------------------------------------------------
|
|
15
|
-
from code_puppy.messaging import (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
from code_puppy.messaging import ( # New structured messaging types
|
|
16
|
+
FileContentMessage,
|
|
17
|
+
FileEntry,
|
|
18
|
+
FileListingMessage,
|
|
19
|
+
GrepMatch,
|
|
20
|
+
GrepResultMessage,
|
|
21
|
+
get_message_bus,
|
|
20
22
|
)
|
|
21
|
-
from code_puppy.tools.common import generate_group_id
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# Pydantic models for tool return types
|
|
@@ -49,6 +50,7 @@ class MatchInfo(BaseModel):
|
|
|
49
50
|
|
|
50
51
|
class GrepOutput(BaseModel):
|
|
51
52
|
matches: List[MatchInfo]
|
|
53
|
+
error: str | None = None
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
def is_likely_home_directory(directory):
|
|
@@ -154,44 +156,24 @@ def _list_files(
|
|
|
154
156
|
results = []
|
|
155
157
|
directory = os.path.abspath(os.path.expanduser(directory))
|
|
156
158
|
|
|
157
|
-
#
|
|
159
|
+
# Plain text output for LLM consumption
|
|
158
160
|
output_lines = []
|
|
159
|
-
|
|
160
|
-
directory_listing_header = (
|
|
161
|
-
"\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]"
|
|
162
|
-
)
|
|
163
|
-
output_lines.append(directory_listing_header)
|
|
164
|
-
|
|
165
|
-
directory_info = f"\U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim](recursive={recursive})[/dim]\n"
|
|
166
|
-
output_lines.append(directory_info)
|
|
167
|
-
|
|
168
|
-
divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
|
|
169
|
-
output_lines.append(divider)
|
|
161
|
+
output_lines.append(f"DIRECTORY LISTING: {directory} (recursive={recursive})")
|
|
170
162
|
|
|
171
163
|
if not os.path.exists(directory):
|
|
172
|
-
error_msg =
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
output_lines.append(error_msg)
|
|
176
|
-
|
|
177
|
-
output_lines.append(divider)
|
|
178
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
164
|
+
error_msg = f"Error: Directory '{directory}' does not exist"
|
|
165
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
179
166
|
if not os.path.isdir(directory):
|
|
180
|
-
error_msg = f"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
output_lines.append(divider)
|
|
184
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
167
|
+
error_msg = f"Error: '{directory}' is not a directory"
|
|
168
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
185
169
|
|
|
186
170
|
# Smart home directory detection - auto-limit recursion for performance
|
|
187
171
|
# But allow recursion in tests (when context=None) or when explicitly requested
|
|
188
172
|
if context is not None and is_likely_home_directory(directory) and recursive:
|
|
189
173
|
if not is_project_directory(directory):
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
info_msg = f"[dim]💡 To force recursive listing in home directory, use list_files('{directory}', recursive=True) explicitly[/dim]"
|
|
194
|
-
output_lines.append(info_msg)
|
|
174
|
+
output_lines.append(
|
|
175
|
+
"Warning: Detected home directory - limiting to non-recursive listing for performance"
|
|
176
|
+
)
|
|
195
177
|
recursive = False
|
|
196
178
|
|
|
197
179
|
# Create a temporary ignore file with our ignore patterns
|
|
@@ -217,9 +199,8 @@ def _list_files(
|
|
|
217
199
|
|
|
218
200
|
if not rg_path and recursive:
|
|
219
201
|
# Only need ripgrep for recursive listings
|
|
220
|
-
error_msg = "
|
|
221
|
-
|
|
222
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
202
|
+
error_msg = "Error: ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
203
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
223
204
|
|
|
224
205
|
# Only use ripgrep for recursive listings
|
|
225
206
|
if recursive:
|
|
@@ -371,17 +352,11 @@ def _list_files(
|
|
|
371
352
|
# Skip entries we can't access
|
|
372
353
|
pass
|
|
373
354
|
except subprocess.TimeoutExpired:
|
|
374
|
-
error_msg =
|
|
375
|
-
|
|
376
|
-
)
|
|
377
|
-
output_lines.append(error_msg)
|
|
378
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
355
|
+
error_msg = "Error: List files command timed out after 30 seconds"
|
|
356
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
379
357
|
except Exception as e:
|
|
380
|
-
error_msg =
|
|
381
|
-
|
|
382
|
-
)
|
|
383
|
-
output_lines.append(error_msg)
|
|
384
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
358
|
+
error_msg = f"Error: Error during list files operation: {e}"
|
|
359
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
385
360
|
finally:
|
|
386
361
|
# Clean up the temporary ignore file
|
|
387
362
|
if ignore_file and os.path.exists(ignore_file):
|
|
@@ -431,59 +406,48 @@ def _list_files(
|
|
|
431
406
|
file_count = sum(1 for item in results if item.type == "file")
|
|
432
407
|
total_size = sum(item.size for item in results if item.type == "file")
|
|
433
408
|
|
|
434
|
-
# Build
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
output_lines.append(dir_header)
|
|
438
|
-
|
|
439
|
-
# Sort all items by path for consistent display
|
|
440
|
-
all_items = sorted(results, key=lambda x: x.path)
|
|
441
|
-
|
|
442
|
-
# Build file and directory tree representation
|
|
443
|
-
parent_dirs_with_content = set()
|
|
444
|
-
for item in all_items:
|
|
445
|
-
# Skip root directory entries with no path
|
|
409
|
+
# Build structured FileEntry objects for the UI
|
|
410
|
+
file_entries = []
|
|
411
|
+
for item in sorted(results, key=lambda x: x.path):
|
|
446
412
|
if item.type == "directory" and not item.path:
|
|
447
413
|
continue
|
|
414
|
+
file_entries.append(
|
|
415
|
+
FileEntry(
|
|
416
|
+
path=item.path,
|
|
417
|
+
type="dir" if item.type == "directory" else "file",
|
|
418
|
+
size=item.size,
|
|
419
|
+
depth=item.depth or 0,
|
|
420
|
+
)
|
|
421
|
+
)
|
|
448
422
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
prefix += "\u2514\u2500\u2500 "
|
|
460
|
-
else:
|
|
461
|
-
prefix += " "
|
|
423
|
+
# Emit structured message for the UI
|
|
424
|
+
file_listing_msg = FileListingMessage(
|
|
425
|
+
directory=directory,
|
|
426
|
+
files=file_entries,
|
|
427
|
+
recursive=recursive,
|
|
428
|
+
total_size=total_size,
|
|
429
|
+
dir_count=dir_count,
|
|
430
|
+
file_count=file_count,
|
|
431
|
+
)
|
|
432
|
+
get_message_bus().emit(file_listing_msg)
|
|
462
433
|
|
|
463
|
-
|
|
434
|
+
# Build plain text output for LLM consumption
|
|
435
|
+
for item in sorted(results, key=lambda x: x.path):
|
|
436
|
+
if item.type == "directory" and not item.path:
|
|
437
|
+
continue
|
|
464
438
|
name = os.path.basename(item.path) or item.path
|
|
465
|
-
|
|
466
|
-
# Add directory or file line with appropriate formatting
|
|
439
|
+
indent = " " * (item.depth or 0)
|
|
467
440
|
if item.type == "directory":
|
|
468
|
-
|
|
469
|
-
output_lines.append(dir_line)
|
|
441
|
+
output_lines.append(f"{indent}{name}/")
|
|
470
442
|
else:
|
|
471
|
-
icon = get_file_icon(item.path)
|
|
472
443
|
size_str = format_size(item.size)
|
|
473
|
-
|
|
474
|
-
output_lines.append(file_line)
|
|
475
|
-
|
|
476
|
-
# Add summary information
|
|
477
|
-
summary_header = "\n[bold cyan]Summary:[/bold cyan]"
|
|
478
|
-
output_lines.append(summary_header)
|
|
444
|
+
output_lines.append(f"{indent}{name} ({size_str})")
|
|
479
445
|
|
|
480
|
-
|
|
481
|
-
output_lines.append(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
output_lines.append(final_divider)
|
|
446
|
+
# Add summary
|
|
447
|
+
output_lines.append(
|
|
448
|
+
f"\nSummary: {dir_count} directories, {file_count} files ({format_size(total_size)} total)"
|
|
449
|
+
)
|
|
485
450
|
|
|
486
|
-
# Return the content string
|
|
487
451
|
return ListFileOutput(content="\n".join(output_lines))
|
|
488
452
|
|
|
489
453
|
|
|
@@ -495,15 +459,6 @@ def _read_file(
|
|
|
495
459
|
) -> ReadFileOutput:
|
|
496
460
|
file_path = os.path.abspath(os.path.expanduser(file_path))
|
|
497
461
|
|
|
498
|
-
# Generate group_id for this tool execution
|
|
499
|
-
group_id = generate_group_id("read_file", file_path)
|
|
500
|
-
|
|
501
|
-
# Build console message with optional parameters
|
|
502
|
-
console_msg = f"\n[bold white on blue] READ FILE [/bold white on blue] \U0001f4c2 [bold cyan]{file_path}[/bold cyan]"
|
|
503
|
-
if start_line is not None and num_lines is not None:
|
|
504
|
-
console_msg += f" [dim](lines {start_line}-{start_line + num_lines - 1})[/dim]"
|
|
505
|
-
emit_info(console_msg, message_group=group_id)
|
|
506
|
-
|
|
507
462
|
if not os.path.exists(file_path):
|
|
508
463
|
error_msg = f"File {file_path} does not exist"
|
|
509
464
|
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
@@ -552,6 +507,30 @@ def _read_file(
|
|
|
552
507
|
error="The file is massive, greater than 10,000 tokens which is dangerous to read entirely. Please read this file in chunks.",
|
|
553
508
|
num_tokens=0,
|
|
554
509
|
)
|
|
510
|
+
|
|
511
|
+
# Count total lines for the message
|
|
512
|
+
total_lines = content.count("\n") + (
|
|
513
|
+
1 if content and not content.endswith("\n") else 0
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Emit structured message for the UI
|
|
517
|
+
# Only include start_line/num_lines if they are valid positive integers
|
|
518
|
+
emit_start_line = (
|
|
519
|
+
start_line if start_line is not None and start_line >= 1 else None
|
|
520
|
+
)
|
|
521
|
+
emit_num_lines = (
|
|
522
|
+
num_lines if num_lines is not None and num_lines >= 1 else None
|
|
523
|
+
)
|
|
524
|
+
file_content_msg = FileContentMessage(
|
|
525
|
+
path=file_path,
|
|
526
|
+
content=content,
|
|
527
|
+
start_line=emit_start_line,
|
|
528
|
+
num_lines=emit_num_lines,
|
|
529
|
+
total_lines=total_lines,
|
|
530
|
+
num_tokens=num_tokens,
|
|
531
|
+
)
|
|
532
|
+
get_message_bus().emit(file_content_msg)
|
|
533
|
+
|
|
555
534
|
return ReadFileOutput(content=content, num_tokens=num_tokens)
|
|
556
535
|
except (FileNotFoundError, PermissionError):
|
|
557
536
|
# For backward compatibility with tests, return "FILE NOT FOUND" for these specific errors
|
|
@@ -601,14 +580,7 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
601
580
|
|
|
602
581
|
directory = os.path.abspath(os.path.expanduser(directory))
|
|
603
582
|
matches: List[MatchInfo] = []
|
|
604
|
-
|
|
605
|
-
# Generate group_id for this tool execution
|
|
606
|
-
group_id = generate_group_id("grep", f"{directory}_{search_string}")
|
|
607
|
-
|
|
608
|
-
emit_info(
|
|
609
|
-
f"\n[bold white on blue] GREP [/bold white on blue] \U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim]for '{search_string}'[/dim]",
|
|
610
|
-
message_group=group_id,
|
|
611
|
-
)
|
|
583
|
+
error_message: str | None = None
|
|
612
584
|
|
|
613
585
|
# Create a temporary ignore file with our ignore patterns
|
|
614
586
|
ignore_file = None
|
|
@@ -640,11 +612,10 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
640
612
|
break
|
|
641
613
|
|
|
642
614
|
if not rg_path:
|
|
643
|
-
|
|
644
|
-
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
645
|
-
message_group=group_id,
|
|
615
|
+
error_message = (
|
|
616
|
+
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
646
617
|
)
|
|
647
|
-
return GrepOutput(matches=[])
|
|
618
|
+
return GrepOutput(matches=[], error=error_message)
|
|
648
619
|
|
|
649
620
|
cmd = [
|
|
650
621
|
rg_path,
|
|
@@ -712,101 +683,43 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
712
683
|
# Skip lines that aren't valid JSON
|
|
713
684
|
continue
|
|
714
685
|
|
|
715
|
-
if not matches:
|
|
716
|
-
emit_warning(
|
|
717
|
-
f"No matches found for '{search_string}' in {directory}",
|
|
718
|
-
message_group=group_id,
|
|
719
|
-
)
|
|
720
|
-
else:
|
|
721
|
-
# Check if verbose output is enabled
|
|
722
|
-
from collections import defaultdict
|
|
723
|
-
|
|
724
|
-
from code_puppy.config import get_grep_output_verbose
|
|
725
|
-
|
|
726
|
-
matches_by_file = defaultdict(list)
|
|
727
|
-
for match in matches:
|
|
728
|
-
matches_by_file[match.file_path].append(match)
|
|
729
|
-
|
|
730
|
-
verbose = get_grep_output_verbose()
|
|
731
|
-
|
|
732
|
-
if verbose:
|
|
733
|
-
# Verbose mode: Show full output with line numbers and content
|
|
734
|
-
emit_info(
|
|
735
|
-
"\n[bold cyan]─────────────────────────────────────────────────────[/bold cyan]",
|
|
736
|
-
message_group=group_id,
|
|
737
|
-
)
|
|
738
|
-
|
|
739
|
-
for file_path in sorted(matches_by_file.keys()):
|
|
740
|
-
file_matches = matches_by_file[file_path]
|
|
741
|
-
emit_info(
|
|
742
|
-
f"\n[bold white]📄 {file_path}[/bold white] [dim]({len(file_matches)} match{'es' if len(file_matches) != 1 else ''})[/dim]",
|
|
743
|
-
message_group=group_id,
|
|
744
|
-
)
|
|
745
|
-
|
|
746
|
-
# Show each match with line number and content
|
|
747
|
-
for match in file_matches:
|
|
748
|
-
line = match.line_content
|
|
749
|
-
search_term = search_string.split()[-1]
|
|
750
|
-
if search_term.startswith("-"):
|
|
751
|
-
search_term = (
|
|
752
|
-
search_string.split()[0]
|
|
753
|
-
if search_string.split()
|
|
754
|
-
else search_string
|
|
755
|
-
)
|
|
756
|
-
|
|
757
|
-
# Case-insensitive highlighting
|
|
758
|
-
import re
|
|
759
|
-
|
|
760
|
-
highlighted_line = (
|
|
761
|
-
re.sub(
|
|
762
|
-
f"({re.escape(search_term)})",
|
|
763
|
-
r"[bold yellow on black]\1[/bold yellow on black]",
|
|
764
|
-
line,
|
|
765
|
-
flags=re.IGNORECASE,
|
|
766
|
-
)
|
|
767
|
-
if search_term and not search_term.startswith("-")
|
|
768
|
-
else line
|
|
769
|
-
)
|
|
770
|
-
|
|
771
|
-
emit_info(
|
|
772
|
-
f" [bold cyan]{match.line_number:4d}[/bold cyan] │ {highlighted_line}",
|
|
773
|
-
message_group=group_id,
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
emit_info(
|
|
777
|
-
"\n[bold cyan]─────────────────────────────────────────────────────[/bold cyan]",
|
|
778
|
-
message_group=group_id,
|
|
779
|
-
)
|
|
780
|
-
else:
|
|
781
|
-
# Concise mode (default): Show only file summaries
|
|
782
|
-
emit_info("", message_group=group_id)
|
|
783
|
-
for file_path in sorted(matches_by_file.keys()):
|
|
784
|
-
file_matches = matches_by_file[file_path]
|
|
785
|
-
emit_info(
|
|
786
|
-
f"[dim]📄 {file_path} ({len(file_matches)} match{'es' if len(file_matches) != 1 else ''})[/dim]",
|
|
787
|
-
message_group=group_id,
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
emit_success(
|
|
791
|
-
f"✓ Found [bold]{len(matches)}[/bold] match{'es' if len(matches) != 1 else ''} across [bold]{len(matches_by_file)}[/bold] file{'s' if len(matches_by_file) != 1 else ''}",
|
|
792
|
-
message_group=group_id,
|
|
793
|
-
)
|
|
794
|
-
|
|
795
686
|
except subprocess.TimeoutExpired:
|
|
796
|
-
|
|
687
|
+
error_message = "Grep command timed out after 30 seconds"
|
|
797
688
|
except FileNotFoundError:
|
|
798
|
-
|
|
799
|
-
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
800
|
-
message_group=group_id,
|
|
689
|
+
error_message = (
|
|
690
|
+
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
801
691
|
)
|
|
802
692
|
except Exception as e:
|
|
803
|
-
|
|
693
|
+
error_message = f"Error during grep operation: {e}"
|
|
804
694
|
finally:
|
|
805
695
|
# Clean up the temporary ignore file
|
|
806
696
|
if ignore_file and os.path.exists(ignore_file):
|
|
807
697
|
os.unlink(ignore_file)
|
|
808
698
|
|
|
809
|
-
|
|
699
|
+
# Build structured GrepMatch objects for the UI
|
|
700
|
+
grep_matches = [
|
|
701
|
+
GrepMatch(
|
|
702
|
+
file_path=m.file_path or "",
|
|
703
|
+
line_number=m.line_number or 1,
|
|
704
|
+
line_content=m.line_content or "",
|
|
705
|
+
)
|
|
706
|
+
for m in matches
|
|
707
|
+
]
|
|
708
|
+
|
|
709
|
+
# Count unique files searched (approximation based on matches)
|
|
710
|
+
unique_files = len(set(m.file_path for m in matches)) if matches else 0
|
|
711
|
+
|
|
712
|
+
# Emit structured message for the UI (only once, at the end)
|
|
713
|
+
grep_result_msg = GrepResultMessage(
|
|
714
|
+
search_term=search_string,
|
|
715
|
+
directory=directory,
|
|
716
|
+
matches=grep_matches,
|
|
717
|
+
total_matches=len(matches),
|
|
718
|
+
files_searched=unique_files,
|
|
719
|
+
)
|
|
720
|
+
get_message_bus().emit(grep_result_msg)
|
|
721
|
+
|
|
722
|
+
return GrepOutput(matches=matches, error=error_message)
|
|
810
723
|
|
|
811
724
|
|
|
812
725
|
def register_list_files(agent):
|
|
@@ -867,10 +780,8 @@ def register_list_files(agent):
|
|
|
867
780
|
recursive = False
|
|
868
781
|
result = _list_files(context, directory, recursive)
|
|
869
782
|
|
|
870
|
-
#
|
|
871
|
-
|
|
872
|
-
result.content, message_group=generate_group_id("list_files", directory)
|
|
873
|
-
)
|
|
783
|
+
# The structured FileListingMessage is already emitted by _list_files
|
|
784
|
+
# No need to emit again here
|
|
874
785
|
if warning:
|
|
875
786
|
result.error = warning
|
|
876
787
|
if (len(result.content)) > 200000:
|
code_puppy/version_checker.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
"""Version checking utilities for Code Puppy."""
|
|
2
|
+
|
|
1
3
|
import httpx
|
|
2
4
|
|
|
3
|
-
from code_puppy.
|
|
5
|
+
from code_puppy.messaging import emit_info, emit_success, emit_warning, get_message_bus
|
|
6
|
+
from code_puppy.messaging.messages import VersionCheckMessage
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def normalize_version(version_str):
|
|
@@ -15,25 +18,37 @@ def versions_are_equal(current, latest):
|
|
|
15
18
|
|
|
16
19
|
def fetch_latest_version(package_name):
|
|
17
20
|
try:
|
|
18
|
-
response = httpx.get(f"https://pypi.org/pypi/{package_name}/json")
|
|
19
|
-
response.raise_for_status()
|
|
21
|
+
response = httpx.get(f"https://pypi.org/pypi/{package_name}/json", timeout=5.0)
|
|
22
|
+
response.raise_for_status()
|
|
20
23
|
data = response.json()
|
|
21
24
|
return data["info"]["version"]
|
|
22
25
|
except Exception as e:
|
|
23
|
-
|
|
26
|
+
emit_warning(f"Error fetching version: {e}")
|
|
24
27
|
return None
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
def default_version_mismatch_behavior(current_version):
|
|
31
|
+
# Defensive: ensure current_version is never None
|
|
32
|
+
if current_version is None:
|
|
33
|
+
current_version = "0.0.0-unknown"
|
|
34
|
+
emit_warning("Could not detect current version, using fallback")
|
|
35
|
+
|
|
28
36
|
latest_version = fetch_latest_version("code-puppy")
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
update_available = bool(latest_version and latest_version != current_version)
|
|
39
|
+
|
|
40
|
+
# Emit structured version check message
|
|
41
|
+
version_msg = VersionCheckMessage(
|
|
42
|
+
current_version=current_version,
|
|
43
|
+
latest_version=latest_version or current_version,
|
|
44
|
+
update_available=update_available,
|
|
45
|
+
)
|
|
46
|
+
get_message_bus().emit(version_msg)
|
|
47
|
+
|
|
48
|
+
# Also emit plain text for legacy renderer
|
|
49
|
+
emit_info(f"Current version: {current_version}")
|
|
32
50
|
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
f"[bold yellow]A new version of code puppy is available: {latest_version}[/bold yellow]"
|
|
38
|
-
)
|
|
39
|
-
console.print("[bold green]Please consider updating![/bold green]")
|
|
51
|
+
if update_available:
|
|
52
|
+
emit_info(f"Latest version: {latest_version}")
|
|
53
|
+
emit_warning(f"A new version of code puppy is available: {latest_version}")
|
|
54
|
+
emit_success("Please consider updating!")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"synthetic-GLM-4.
|
|
2
|
+
"synthetic-GLM-4.7": {
|
|
3
3
|
"type": "custom_openai",
|
|
4
|
-
"name": "hf:zai-org/GLM-4.
|
|
4
|
+
"name": "hf:zai-org/GLM-4.7",
|
|
5
5
|
"custom_endpoint": {
|
|
6
6
|
"url": "https://api.synthetic.new/openai/v1/",
|
|
7
7
|
"api_key": "$SYN_API_KEY"
|
|
@@ -45,13 +45,15 @@
|
|
|
45
45
|
"type": "openai",
|
|
46
46
|
"name": "gpt-5.1",
|
|
47
47
|
"context_length": 272000,
|
|
48
|
-
"supported_settings": ["reasoning_effort", "verbosity"]
|
|
48
|
+
"supported_settings": ["reasoning_effort", "verbosity"],
|
|
49
|
+
"supports_xhigh_reasoning": false
|
|
49
50
|
},
|
|
50
51
|
"gpt-5.1-codex-api": {
|
|
51
52
|
"type": "openai",
|
|
52
53
|
"name": "gpt-5.1-codex",
|
|
53
54
|
"context_length": 272000,
|
|
54
|
-
"supported_settings": ["reasoning_effort"]
|
|
55
|
+
"supported_settings": ["reasoning_effort", "verbosity"],
|
|
56
|
+
"supports_xhigh_reasoning": true
|
|
55
57
|
},
|
|
56
58
|
"Cerebras-GLM-4.6": {
|
|
57
59
|
"type": "cerebras",
|
|
@@ -79,7 +81,7 @@
|
|
|
79
81
|
"type": "anthropic",
|
|
80
82
|
"name": "claude-opus-4-5",
|
|
81
83
|
"context_length": 200000,
|
|
82
|
-
"supported_settings": ["temperature", "extended_thinking", "budget_tokens"]
|
|
84
|
+
"supported_settings": ["temperature", "extended_thinking", "budget_tokens", "interleaved_thinking"]
|
|
83
85
|
},
|
|
84
86
|
"zai-glm-4.6-coding": {
|
|
85
87
|
"type": "zai_coding",
|
|
@@ -92,5 +94,17 @@
|
|
|
92
94
|
"name": "glm-4.6",
|
|
93
95
|
"context_length": 200000,
|
|
94
96
|
"supported_settings": ["temperature"]
|
|
97
|
+
},
|
|
98
|
+
"zai-glm-4.7-coding": {
|
|
99
|
+
"type": "zai_coding",
|
|
100
|
+
"name": "glm-4.7",
|
|
101
|
+
"context_length": 200000,
|
|
102
|
+
"supported_settings": ["temperature"]
|
|
103
|
+
},
|
|
104
|
+
"zai-glm-4.7-api": {
|
|
105
|
+
"type": "zai_api",
|
|
106
|
+
"name": "glm-4.7",
|
|
107
|
+
"context_length": 200000,
|
|
108
|
+
"supported_settings": ["temperature"]
|
|
95
109
|
}
|
|
96
110
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-puppy
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.323
|
|
4
4
|
Summary: Code generation agent
|
|
5
5
|
Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
|
|
6
6
|
Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
|
|
@@ -15,22 +15,18 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Topic :: Software Development :: Code Generators
|
|
17
17
|
Requires-Python: <3.14,>=3.11
|
|
18
|
-
Requires-Dist: bs4>=0.0.2
|
|
19
18
|
Requires-Dist: camoufox>=0.4.11
|
|
20
19
|
Requires-Dist: dbos>=2.5.0
|
|
21
|
-
Requires-Dist: fastapi>=0.
|
|
22
|
-
Requires-Dist: httpx-limiter>=0.3.0
|
|
20
|
+
Requires-Dist: fastapi>=0.111.0
|
|
23
21
|
Requires-Dist: httpx[http2]>=0.24.1
|
|
24
22
|
Requires-Dist: json-repair>=0.46.2
|
|
25
23
|
Requires-Dist: logfire>=0.7.1
|
|
26
24
|
Requires-Dist: openai>=1.99.1
|
|
27
|
-
Requires-Dist: pathspec>=0.11.0
|
|
28
25
|
Requires-Dist: playwright>=1.40.0
|
|
29
26
|
Requires-Dist: prompt-toolkit>=3.0.52
|
|
30
27
|
Requires-Dist: pydantic-ai==1.25.0
|
|
31
28
|
Requires-Dist: pydantic>=2.4.0
|
|
32
29
|
Requires-Dist: pyfiglet>=0.8.post1
|
|
33
|
-
Requires-Dist: pyjwt>=2.8.0
|
|
34
30
|
Requires-Dist: pytest-cov>=6.1.1
|
|
35
31
|
Requires-Dist: python-dotenv>=1.0.0
|
|
36
32
|
Requires-Dist: rapidfuzz>=3.13.0
|
|
@@ -38,8 +34,7 @@ Requires-Dist: rich>=13.4.2
|
|
|
38
34
|
Requires-Dist: ripgrep==14.1.0
|
|
39
35
|
Requires-Dist: ruff>=0.11.11
|
|
40
36
|
Requires-Dist: tenacity>=8.2.0
|
|
41
|
-
Requires-Dist:
|
|
42
|
-
Requires-Dist: uvicorn>=0.29.0
|
|
37
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
43
38
|
Description-Content-Type: text/markdown
|
|
44
39
|
|
|
45
40
|
<div align="center">
|