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.
Files changed (110) hide show
  1. code_puppy/__init__.py +3 -1
  2. code_puppy/agents/agent_code_puppy.py +5 -4
  3. code_puppy/agents/agent_creator_agent.py +22 -18
  4. code_puppy/agents/agent_manager.py +2 -2
  5. code_puppy/agents/base_agent.py +496 -102
  6. code_puppy/callbacks.py +8 -0
  7. code_puppy/chatgpt_codex_client.py +283 -0
  8. code_puppy/cli_runner.py +795 -0
  9. code_puppy/command_line/add_model_menu.py +19 -16
  10. code_puppy/command_line/attachments.py +10 -5
  11. code_puppy/command_line/autosave_menu.py +269 -41
  12. code_puppy/command_line/colors_menu.py +515 -0
  13. code_puppy/command_line/command_handler.py +10 -24
  14. code_puppy/command_line/config_commands.py +106 -25
  15. code_puppy/command_line/core_commands.py +32 -20
  16. code_puppy/command_line/mcp/add_command.py +3 -16
  17. code_puppy/command_line/mcp/base.py +0 -3
  18. code_puppy/command_line/mcp/catalog_server_installer.py +15 -15
  19. code_puppy/command_line/mcp/custom_server_form.py +66 -5
  20. code_puppy/command_line/mcp/custom_server_installer.py +17 -17
  21. code_puppy/command_line/mcp/edit_command.py +15 -22
  22. code_puppy/command_line/mcp/handler.py +7 -2
  23. code_puppy/command_line/mcp/help_command.py +2 -2
  24. code_puppy/command_line/mcp/install_command.py +10 -14
  25. code_puppy/command_line/mcp/install_menu.py +2 -6
  26. code_puppy/command_line/mcp/list_command.py +2 -2
  27. code_puppy/command_line/mcp/logs_command.py +174 -65
  28. code_puppy/command_line/mcp/remove_command.py +2 -2
  29. code_puppy/command_line/mcp/restart_command.py +7 -2
  30. code_puppy/command_line/mcp/search_command.py +16 -10
  31. code_puppy/command_line/mcp/start_all_command.py +16 -6
  32. code_puppy/command_line/mcp/start_command.py +12 -10
  33. code_puppy/command_line/mcp/status_command.py +4 -5
  34. code_puppy/command_line/mcp/stop_all_command.py +5 -1
  35. code_puppy/command_line/mcp/stop_command.py +6 -4
  36. code_puppy/command_line/mcp/test_command.py +2 -2
  37. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  38. code_puppy/command_line/model_settings_menu.py +53 -7
  39. code_puppy/command_line/motd.py +1 -1
  40. code_puppy/command_line/pin_command_completion.py +82 -7
  41. code_puppy/command_line/prompt_toolkit_completion.py +32 -9
  42. code_puppy/command_line/session_commands.py +11 -4
  43. code_puppy/config.py +217 -53
  44. code_puppy/error_logging.py +118 -0
  45. code_puppy/gemini_code_assist.py +385 -0
  46. code_puppy/keymap.py +126 -0
  47. code_puppy/main.py +5 -745
  48. code_puppy/mcp_/__init__.py +17 -0
  49. code_puppy/mcp_/blocking_startup.py +63 -36
  50. code_puppy/mcp_/captured_stdio_server.py +1 -1
  51. code_puppy/mcp_/config_wizard.py +4 -4
  52. code_puppy/mcp_/dashboard.py +15 -6
  53. code_puppy/mcp_/managed_server.py +25 -5
  54. code_puppy/mcp_/manager.py +65 -0
  55. code_puppy/mcp_/mcp_logs.py +224 -0
  56. code_puppy/mcp_/registry.py +6 -6
  57. code_puppy/messaging/__init__.py +184 -2
  58. code_puppy/messaging/bus.py +610 -0
  59. code_puppy/messaging/commands.py +167 -0
  60. code_puppy/messaging/markdown_patches.py +57 -0
  61. code_puppy/messaging/message_queue.py +3 -3
  62. code_puppy/messaging/messages.py +470 -0
  63. code_puppy/messaging/renderers.py +43 -141
  64. code_puppy/messaging/rich_renderer.py +900 -0
  65. code_puppy/messaging/spinner/console_spinner.py +39 -2
  66. code_puppy/model_factory.py +292 -53
  67. code_puppy/model_utils.py +57 -48
  68. code_puppy/models.json +19 -5
  69. code_puppy/plugins/__init__.py +152 -10
  70. code_puppy/plugins/chatgpt_oauth/config.py +20 -12
  71. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
  72. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
  73. code_puppy/plugins/chatgpt_oauth/test_plugin.py +30 -13
  74. code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
  75. code_puppy/plugins/claude_code_oauth/config.py +15 -11
  76. code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
  77. code_puppy/plugins/claude_code_oauth/utils.py +6 -1
  78. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  79. code_puppy/plugins/oauth_puppy_html.py +3 -0
  80. code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -134
  81. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  82. code_puppy/plugins/shell_safety/register_callbacks.py +77 -3
  83. code_puppy/prompts/codex_system_prompt.md +310 -0
  84. code_puppy/pydantic_patches.py +131 -0
  85. code_puppy/session_storage.py +2 -1
  86. code_puppy/status_display.py +7 -5
  87. code_puppy/terminal_utils.py +126 -0
  88. code_puppy/tools/agent_tools.py +131 -70
  89. code_puppy/tools/browser/browser_control.py +10 -14
  90. code_puppy/tools/browser/browser_interactions.py +20 -28
  91. code_puppy/tools/browser/browser_locators.py +27 -29
  92. code_puppy/tools/browser/browser_navigation.py +9 -9
  93. code_puppy/tools/browser/browser_screenshot.py +12 -14
  94. code_puppy/tools/browser/browser_scripts.py +17 -29
  95. code_puppy/tools/browser/browser_workflows.py +24 -25
  96. code_puppy/tools/browser/camoufox_manager.py +22 -26
  97. code_puppy/tools/command_runner.py +410 -88
  98. code_puppy/tools/common.py +51 -38
  99. code_puppy/tools/file_modifications.py +98 -24
  100. code_puppy/tools/file_operations.py +113 -202
  101. code_puppy/version_checker.py +28 -13
  102. {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
  103. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/METADATA +3 -8
  104. code_puppy-0.0.323.dist-info/RECORD +168 -0
  105. code_puppy/tui_state.py +0 -55
  106. code_puppy-0.0.287.dist-info/RECORD +0 -153
  107. {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
  108. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
  109. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
  110. {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
- emit_error,
17
- emit_info,
18
- emit_success,
19
- emit_warning,
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
- # Build string representation
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
- f"[red bold]Error:[/red bold] Directory '{directory}' does not exist"
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"[red bold]Error:[/red bold] '{directory}' is not a directory"
181
- output_lines.append(error_msg)
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
- warning_msg = "[yellow bold]Warning:[/yellow bold] 🏠 Detected home directory - limiting to non-recursive listing for performance"
191
- output_lines.append(warning_msg)
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 = "[red bold]Error:[/red bold] ripgrep (rg) not found. Please install ripgrep to use this tool."
221
- output_lines.append(error_msg)
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
- "[red bold]Error:[/red bold] List files command timed out after 30 seconds"
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
- f"[red bold]Error:[/red bold] Error during list files operation: {e}"
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 the directory header section
435
- dir_name = os.path.basename(directory) or directory
436
- dir_header = f"\U0001f4c1 [bold blue]{dir_name}[/bold blue]"
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
- # Track parent directories that contain files/dirs
450
- if os.sep in item.path:
451
- parent_path = os.path.dirname(item.path)
452
- parent_dirs_with_content.add(parent_path)
453
-
454
- # Calculate indentation depth based on path separators
455
- depth = item.path.count(os.sep) + 1 if item.path else 0
456
- prefix = ""
457
- for d in range(depth):
458
- if d == depth - 1:
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
- # Get the display name (basename) of the item
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
- dir_line = f"{prefix}\U0001f4c1 [bold blue]{name}/[/bold blue]"
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
- file_line = f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]"
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
- summary_line = f"\U0001f4c1 [blue]{dir_count} directories[/blue], \U0001f4c4 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]"
481
- output_lines.append(summary_line)
482
-
483
- final_divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
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
- emit_error(
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
- emit_error("Grep command timed out after 30 seconds", message_group=group_id)
687
+ error_message = "Grep command timed out after 30 seconds"
797
688
  except FileNotFoundError:
798
- emit_error(
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
- emit_error(f"Error during grep operation: {e}", message_group=group_id)
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
- return GrepOutput(matches=matches)
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
- # Emit the content directly to ensure it's displayed to the user
871
- emit_info(
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:
@@ -1,6 +1,9 @@
1
+ """Version checking utilities for Code Puppy."""
2
+
1
3
  import httpx
2
4
 
3
- from code_puppy.tools.common import console
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() # Raise an error for bad responses
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
- print(f"Error fetching version: {e}")
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
- # Always print the current version
31
- console.print(f"Current version: {current_version}")
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 latest_version and latest_version != current_version:
34
- # Show both versions and update message when they're different
35
- console.print(f"Latest version: {latest_version}")
36
- console.print(
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.6": {
2
+ "synthetic-GLM-4.7": {
3
3
  "type": "custom_openai",
4
- "name": "hf:zai-org/GLM-4.6",
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.287
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.110.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: termcolor>=3.1.0
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">