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
@@ -29,7 +29,13 @@ except ImportError:
29
29
 
30
30
  # Import our queue-based console system
31
31
  try:
32
- from code_puppy.messaging import get_queue_console
32
+ from code_puppy.messaging import (
33
+ emit_error,
34
+ emit_info,
35
+ emit_success,
36
+ emit_warning,
37
+ get_queue_console,
38
+ )
33
39
 
34
40
  # Use queue console by default, but allow fallback
35
41
  NO_COLOR = bool(int(os.environ.get("CODE_PUPPY_NO_COLOR", "0")))
@@ -42,6 +48,19 @@ except ImportError:
42
48
  NO_COLOR = bool(int(os.environ.get("CODE_PUPPY_NO_COLOR", "0")))
43
49
  console = Console(no_color=NO_COLOR)
44
50
 
51
+ # Provide fallback emit functions
52
+ def emit_error(msg: str) -> None:
53
+ console.print(f"[bold red]{msg}[/bold red]")
54
+
55
+ def emit_info(msg: str) -> None:
56
+ console.print(msg)
57
+
58
+ def emit_success(msg: str) -> None:
59
+ console.print(f"[bold green]{msg}[/bold green]")
60
+
61
+ def emit_warning(msg: str) -> None:
62
+ console.print(f"[bold yellow]{msg}[/bold yellow]")
63
+
45
64
 
46
65
  def should_suppress_browser() -> bool:
47
66
  """Check if browsers should be suppressed (headless mode).
@@ -782,9 +801,7 @@ def format_diff_with_colors(diff_text: str) -> Text:
782
801
 
783
802
  # Always use beautiful syntax highlighting!
784
803
  if not PYGMENTS_AVAILABLE:
785
- console.print(
786
- "[yellow]Warning: Pygments not available, diffs will look plain[/yellow]"
787
- )
804
+ emit_warning("Pygments not available, diffs will look plain")
788
805
  # Return plain text as fallback
789
806
  return Text(diff_text)
790
807
 
@@ -1088,10 +1105,10 @@ def get_user_approval(
1088
1105
  time.sleep(0.3) # Let spinners fully stop
1089
1106
 
1090
1107
  # Display panel
1091
- console = Console()
1092
- console.print()
1093
- console.print(panel)
1094
- console.print()
1108
+ local_console = Console()
1109
+ emit_info("")
1110
+ local_console.print(panel)
1111
+ emit_info("")
1095
1112
 
1096
1113
  # Flush and buffer before selector
1097
1114
  sys.stdout.flush()
@@ -1122,8 +1139,8 @@ def get_user_approval(
1122
1139
  else:
1123
1140
  # User wants to provide feedback
1124
1141
  confirmed = False
1125
- console.print()
1126
- console.print(f"[bold cyan]Tell {puppy_name} what to change:[/bold cyan]")
1142
+ emit_info("")
1143
+ emit_info(f"Tell {puppy_name} what to change:")
1127
1144
  user_feedback = Prompt.ask(
1128
1145
  "[bold green]➤[/bold green]",
1129
1146
  default="",
@@ -1133,7 +1150,7 @@ def get_user_approval(
1133
1150
  user_feedback = None
1134
1151
 
1135
1152
  except (KeyboardInterrupt, EOFError):
1136
- console.print("\n[bold red]⊗ Cancelled by user[/bold red]")
1153
+ emit_error("Cancelled by user")
1137
1154
  confirmed = False
1138
1155
 
1139
1156
  finally:
@@ -1142,9 +1159,9 @@ def get_user_approval(
1142
1159
  # Force Rich console to reset display state to prevent artifacts
1143
1160
  try:
1144
1161
  # Clear Rich's internal display state to prevent artifacts
1145
- console.file.write("\r") # Return to start of line
1146
- console.file.write("\x1b[K") # Clear current line
1147
- console.file.flush()
1162
+ local_console.file.write("\r") # Return to start of line
1163
+ local_console.file.write("\x1b[K") # Clear current line
1164
+ local_console.file.flush()
1148
1165
  except Exception:
1149
1166
  pass
1150
1167
 
@@ -1153,17 +1170,15 @@ def get_user_approval(
1153
1170
  sys.stderr.flush()
1154
1171
 
1155
1172
  # Show result BEFORE resuming spinners (no puppy litter!)
1156
- console.print()
1173
+ emit_info("")
1157
1174
  if not confirmed:
1158
1175
  if user_feedback:
1159
- console.print("[bold red]✗ Rejected with feedback![/bold red]")
1160
- console.print(
1161
- f'[bold yellow]📝 Telling {puppy_name}: "{user_feedback}"[/bold yellow]'
1162
- )
1176
+ emit_error("Rejected with feedback!")
1177
+ emit_warning(f'Telling {puppy_name}: "{user_feedback}"')
1163
1178
  else:
1164
- console.print("[bold red]✗ Rejected.[/bold red]")
1179
+ emit_error("Rejected.")
1165
1180
  else:
1166
- console.print("[bold green]✓ Approved![/bold green]")
1181
+ emit_success("Approved!")
1167
1182
 
1168
1183
  # NOW resume spinners after showing the result
1169
1184
  try:
@@ -1258,10 +1273,10 @@ async def get_user_approval_async(
1258
1273
  await asyncio.sleep(0.3) # Let spinners fully stop
1259
1274
 
1260
1275
  # Display panel
1261
- console = Console()
1262
- console.print()
1263
- console.print(panel)
1264
- console.print()
1276
+ local_console = Console()
1277
+ emit_info("")
1278
+ local_console.print(panel)
1279
+ emit_info("")
1265
1280
 
1266
1281
  # Flush and buffer before selector
1267
1282
  sys.stdout.flush()
@@ -1292,8 +1307,8 @@ async def get_user_approval_async(
1292
1307
  else:
1293
1308
  # User wants to provide feedback
1294
1309
  confirmed = False
1295
- console.print()
1296
- console.print(f"[bold cyan]Tell {puppy_name} what to change:[/bold cyan]")
1310
+ emit_info("")
1311
+ emit_info(f"Tell {puppy_name} what to change:")
1297
1312
  user_feedback = Prompt.ask(
1298
1313
  "[bold green]➤[/bold green]",
1299
1314
  default="",
@@ -1303,7 +1318,7 @@ async def get_user_approval_async(
1303
1318
  user_feedback = None
1304
1319
 
1305
1320
  except (KeyboardInterrupt, EOFError):
1306
- console.print("\n[bold red]⊗ Cancelled by user[/bold red]")
1321
+ emit_error("Cancelled by user")
1307
1322
  confirmed = False
1308
1323
 
1309
1324
  finally:
@@ -1312,9 +1327,9 @@ async def get_user_approval_async(
1312
1327
  # Force Rich console to reset display state to prevent artifacts
1313
1328
  try:
1314
1329
  # Clear Rich's internal display state to prevent artifacts
1315
- console.file.write("\r") # Return to start of line
1316
- console.file.write("\x1b[K") # Clear current line
1317
- console.file.flush()
1330
+ local_console.file.write("\r") # Return to start of line
1331
+ local_console.file.write("\x1b[K") # Clear current line
1332
+ local_console.file.flush()
1318
1333
  except Exception:
1319
1334
  pass
1320
1335
 
@@ -1323,17 +1338,15 @@ async def get_user_approval_async(
1323
1338
  sys.stderr.flush()
1324
1339
 
1325
1340
  # Show result BEFORE resuming spinners (no puppy litter!)
1326
- console.print()
1341
+ emit_info("")
1327
1342
  if not confirmed:
1328
1343
  if user_feedback:
1329
- console.print("[bold red]✗ Rejected with feedback![/bold red]")
1330
- console.print(
1331
- f'[bold yellow]📝 Telling {puppy_name}: "{user_feedback}"[/bold yellow]'
1332
- )
1344
+ emit_error("Rejected with feedback!")
1345
+ emit_warning(f'Telling {puppy_name}: "{user_feedback}"')
1333
1346
  else:
1334
- console.print("[bold red]✗ Rejected.[/bold red]")
1347
+ emit_error("Rejected.")
1335
1348
  else:
1336
- console.print("[bold green]✓ Approved![/bold green]")
1349
+ emit_success("Approved!")
1337
1350
 
1338
1351
  # NOW resume spinners after showing the result
1339
1352
  try:
@@ -21,9 +21,14 @@ from pydantic import BaseModel
21
21
  from pydantic_ai import RunContext
22
22
 
23
23
  from code_puppy.callbacks import on_delete_file, on_edit_file
24
- from code_puppy.messaging import emit_error, emit_info, emit_warning
24
+ from code_puppy.messaging import ( # Structured messaging types
25
+ DiffLine,
26
+ DiffMessage,
27
+ emit_error,
28
+ emit_warning,
29
+ get_message_bus,
30
+ )
25
31
  from code_puppy.tools.common import _find_best_window, generate_group_id
26
- from code_puppy.tools.common import format_diff_with_colors as _colorize_diff
27
32
 
28
33
 
29
34
  def _create_rejection_response(file_path: str) -> Dict[str, Any]:
@@ -91,10 +96,78 @@ class ContentPayload(BaseModel):
91
96
  EditFilePayload = Union[DeleteSnippetPayload, ReplacementsPayload, ContentPayload]
92
97
 
93
98
 
94
- def _print_diff(diff_text: str, message_group: str | None = None) -> None:
95
- """Pretty-print *diff_text* with colour-coding.
99
+ def _parse_diff_lines(diff_text: str) -> List[DiffLine]:
100
+ """Parse unified diff text into structured DiffLine objects.
96
101
 
97
- Skips printing if the diff was already shown during permission approval.
102
+ Args:
103
+ diff_text: Raw unified diff text
104
+
105
+ Returns:
106
+ List of DiffLine objects with line numbers and types
107
+ """
108
+ if not diff_text or not diff_text.strip():
109
+ return []
110
+
111
+ diff_lines = []
112
+ line_number = 0
113
+
114
+ for line in diff_text.splitlines():
115
+ # Determine line type based on diff markers
116
+ if line.startswith("+") and not line.startswith("+++"):
117
+ line_type = "add"
118
+ line_number += 1
119
+ content = line[1:] # Remove the + prefix
120
+ elif line.startswith("-") and not line.startswith("---"):
121
+ line_type = "remove"
122
+ line_number += 1
123
+ content = line[1:] # Remove the - prefix
124
+ elif line.startswith("@@"):
125
+ # Parse hunk header to get line number
126
+ # Format: @@ -start,count +start,count @@
127
+ import re
128
+
129
+ match = re.search(r"@@ -\d+(?:,\d+)? \+(\d+)", line)
130
+ if match:
131
+ line_number = (
132
+ int(match.group(1)) - 1
133
+ ) # Will be incremented on next line
134
+ line_type = "context"
135
+ content = line
136
+ elif line.startswith("---") or line.startswith("+++"):
137
+ # File headers - treat as context
138
+ line_type = "context"
139
+ content = line
140
+ else:
141
+ line_type = "context"
142
+ line_number += 1
143
+ content = line
144
+
145
+ diff_lines.append(
146
+ DiffLine(
147
+ line_number=max(1, line_number),
148
+ type=line_type,
149
+ content=content,
150
+ )
151
+ )
152
+
153
+ return diff_lines
154
+
155
+
156
+ def _emit_diff_message(
157
+ file_path: str,
158
+ operation: str,
159
+ diff_text: str,
160
+ old_content: str | None = None,
161
+ new_content: str | None = None,
162
+ ) -> None:
163
+ """Emit a structured DiffMessage for UI display.
164
+
165
+ Args:
166
+ file_path: Path to the file being modified
167
+ operation: One of 'create', 'modify', 'delete'
168
+ diff_text: Raw unified diff text
169
+ old_content: Original file content (optional)
170
+ new_content: New file content (optional)
98
171
  """
99
172
  # Check if diff was already shown during permission prompt
100
173
  try:
@@ -108,22 +181,21 @@ def _print_diff(diff_text: str, message_group: str | None = None) -> None:
108
181
  clear_diff_shown_flag()
109
182
  return
110
183
  except ImportError:
111
- pass # Permission handler not available, show diff anyway
112
-
113
- emit_info(
114
- "[bold cyan]\n── DIFF ────────────────────────────────────────────────[/bold cyan]",
115
- message_group=message_group,
116
- )
184
+ pass # Permission handler not available, emit anyway
117
185
 
118
- # Apply color formatting to diff lines
119
- formatted_diff = _colorize_diff(diff_text)
186
+ if not diff_text or not diff_text.strip():
187
+ return
120
188
 
121
- emit_info(formatted_diff, highlight=False, message_group=message_group)
189
+ diff_lines = _parse_diff_lines(diff_text)
122
190
 
123
- emit_info(
124
- "[bold cyan]───────────────────────────────────────────────────────[/bold cyan]",
125
- message_group=message_group,
191
+ diff_msg = DiffMessage(
192
+ path=file_path,
193
+ operation=operation,
194
+ old_content=old_content,
195
+ new_content=new_content,
196
+ diff_lines=diff_lines,
126
197
  )
198
+ get_message_bus().emit(diff_msg)
127
199
 
128
200
 
129
201
  def _log_error(
@@ -339,7 +411,7 @@ def delete_snippet_from_file(
339
411
  )
340
412
  diff = res.get("diff", "")
341
413
  if diff:
342
- _print_diff(diff, message_group=message_group)
414
+ _emit_diff_message(file_path, "modify", diff)
343
415
  return res
344
416
 
345
417
 
@@ -369,7 +441,9 @@ def write_to_file(
369
441
  )
370
442
  diff = res.get("diff", "")
371
443
  if diff:
372
- _print_diff(diff, message_group=message_group)
444
+ # Determine operation type based on whether file existed
445
+ operation = "modify" if overwrite else "create"
446
+ _emit_diff_message(path, operation, diff, new_content=content)
373
447
  return res
374
448
 
375
449
 
@@ -396,7 +470,7 @@ def replace_in_file(
396
470
  res = _replace_in_file(context, path, replacements, message_group=message_group)
397
471
  diff = res.get("diff", "")
398
472
  if diff:
399
- _print_diff(diff, message_group=message_group)
473
+ _emit_diff_message(path, "modify", diff)
400
474
  return res
401
475
 
402
476
 
@@ -440,9 +514,6 @@ def _edit_file(
440
514
  if group_id is None:
441
515
  group_id = generate_group_id("edit_file", file_path)
442
516
 
443
- emit_info(
444
- "\n[bold white on blue] EDIT FILE [/bold white on blue]", message_group=group_id
445
- )
446
517
  try:
447
518
  if isinstance(payload, DeleteSnippetPayload):
448
519
  return delete_snippet_from_file(
@@ -548,7 +619,10 @@ def _delete_file(
548
619
  except Exception as exc:
549
620
  _log_error("Unhandled exception in delete_file", exc)
550
621
  res = {"error": str(exc), "diff": ""}
551
- _print_diff(res.get("diff", ""), message_group=message_group)
622
+
623
+ diff = res.get("diff", "")
624
+ if diff:
625
+ _emit_diff_message(file_path, "delete", diff)
552
626
  return res
553
627
 
554
628