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
@@ -0,0 +1,470 @@
1
+ """Structured message models for Code Puppy's messaging system.
2
+
3
+ Pydantic models that decouple message content from presentation.
4
+ NO Rich markup or formatting should be embedded in any string fields.
5
+ Renderers decide how to display these structured messages.
6
+ """
7
+
8
+ from datetime import datetime, timezone
9
+ from enum import Enum
10
+ from typing import Dict, List, Literal, Optional, Union
11
+ from uuid import uuid4
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+ # =============================================================================
16
+ # Enums
17
+ # =============================================================================
18
+
19
+
20
+ class MessageLevel(str, Enum):
21
+ """Severity level for text messages."""
22
+
23
+ DEBUG = "debug"
24
+ INFO = "info"
25
+ WARNING = "warning"
26
+ ERROR = "error"
27
+ SUCCESS = "success"
28
+
29
+
30
+ class MessageCategory(str, Enum):
31
+ """Category of message for routing and rendering decisions."""
32
+
33
+ SYSTEM = "system"
34
+ TOOL_OUTPUT = "tool_output"
35
+ AGENT = "agent"
36
+ USER_INTERACTION = "user_interaction"
37
+ DIVIDER = "divider"
38
+
39
+
40
+ # =============================================================================
41
+ # Base Message
42
+ # =============================================================================
43
+
44
+
45
+ class BaseMessage(BaseModel):
46
+ """Base class for all structured messages with auto-generated id and timestamp."""
47
+
48
+ id: str = Field(
49
+ default_factory=lambda: str(uuid4()),
50
+ description="Unique identifier for this message instance",
51
+ )
52
+ timestamp: datetime = Field(
53
+ default_factory=lambda: datetime.now(timezone.utc),
54
+ description="When this message was created (UTC)",
55
+ )
56
+ category: MessageCategory = Field(
57
+ description="Category for routing and rendering decisions"
58
+ )
59
+ session_id: Optional[str] = Field(
60
+ default=None,
61
+ description="Session ID of the agent that emitted this message (for multi-agent tracking)",
62
+ )
63
+
64
+ model_config = {"frozen": False, "extra": "forbid"}
65
+
66
+
67
+ # =============================================================================
68
+ # Text Messages
69
+ # =============================================================================
70
+
71
+
72
+ class TextMessage(BaseMessage):
73
+ """Simple text message with a severity level. Text must be plain, no markup!"""
74
+
75
+ category: MessageCategory = MessageCategory.SYSTEM
76
+ level: MessageLevel = Field(description="Severity level of this message")
77
+ text: str = Field(description="Plain text content - NO Rich markup allowed")
78
+
79
+
80
+ # =============================================================================
81
+ # File Operation Messages
82
+ # =============================================================================
83
+
84
+
85
+ class FileEntry(BaseModel):
86
+ """A single file or directory entry in a listing."""
87
+
88
+ path: str = Field(description="Path to the file or directory")
89
+ type: Literal["file", "dir"] = Field(
90
+ description="Whether this is a file or directory"
91
+ )
92
+ size: int = Field(ge=0, description="Size in bytes (0 for directories)")
93
+ depth: int = Field(ge=0, description="Nesting depth from listing root")
94
+
95
+ model_config = {"frozen": True, "extra": "forbid"}
96
+
97
+
98
+ class FileListingMessage(BaseMessage):
99
+ """Result of a directory listing operation."""
100
+
101
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
102
+ directory: str = Field(description="Root directory that was listed")
103
+ files: List[FileEntry] = Field(
104
+ default_factory=list,
105
+ description="List of file and directory entries found",
106
+ )
107
+ recursive: bool = Field(description="Whether the listing was recursive")
108
+ total_size: int = Field(ge=0, description="Total size of all files in bytes")
109
+ dir_count: int = Field(ge=0, description="Number of directories found")
110
+ file_count: int = Field(ge=0, description="Number of files found")
111
+
112
+
113
+ class FileContentMessage(BaseMessage):
114
+ """Content of a file that was read, with metadata for partial reads."""
115
+
116
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
117
+ path: str = Field(description="Path to the file that was read")
118
+ content: str = Field(description="The file content (plain text)")
119
+ start_line: Optional[int] = Field(
120
+ default=None,
121
+ ge=1,
122
+ description="Starting line number if partial read (1-based)",
123
+ )
124
+ num_lines: Optional[int] = Field(
125
+ default=None,
126
+ ge=1,
127
+ description="Number of lines read if partial read",
128
+ )
129
+ total_lines: int = Field(ge=0, description="Total lines in the file")
130
+ num_tokens: int = Field(ge=0, description="Estimated token count of content")
131
+
132
+
133
+ class GrepMatch(BaseModel):
134
+ """A single match from a grep/search operation."""
135
+
136
+ file_path: str = Field(description="Path to file containing this match")
137
+ line_number: int = Field(ge=1, description="Line number (1-based)")
138
+ line_content: str = Field(description="Full line content containing the match")
139
+
140
+ model_config = {"frozen": True, "extra": "forbid"}
141
+
142
+
143
+ class GrepResultMessage(BaseMessage):
144
+ """Results from a grep/search operation with matches and statistics."""
145
+
146
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
147
+ search_term: str = Field(description="The search pattern used")
148
+ directory: str = Field(description="Root directory that was searched")
149
+ matches: List[GrepMatch] = Field(
150
+ default_factory=list,
151
+ description="List of matches found",
152
+ )
153
+ total_matches: int = Field(ge=0, description="Total number of matches")
154
+ files_searched: int = Field(ge=0, description="Number of files searched")
155
+ verbose: bool = Field(
156
+ default=False,
157
+ description="Whether to show verbose output with line content",
158
+ )
159
+
160
+
161
+ # =============================================================================
162
+ # Diff/Modification Messages
163
+ # =============================================================================
164
+
165
+
166
+ class DiffLine(BaseModel):
167
+ """A single line in a diff output."""
168
+
169
+ line_number: int = Field(ge=0, description="Line number for this diff line")
170
+ type: Literal["add", "remove", "context"] = Field(description="Type of diff line")
171
+ content: str = Field(description="The line content")
172
+
173
+ model_config = {"frozen": True, "extra": "forbid"}
174
+
175
+
176
+ class DiffMessage(BaseMessage):
177
+ """A file modification with diff information for rendering."""
178
+
179
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
180
+ path: str = Field(description="Path to the modified file")
181
+ operation: Literal["create", "modify", "delete"] = Field(
182
+ description="Type of file operation"
183
+ )
184
+ old_content: Optional[str] = Field(
185
+ default=None,
186
+ description="Previous file content (None for create)",
187
+ )
188
+ new_content: Optional[str] = Field(
189
+ default=None,
190
+ description="New file content (None for delete)",
191
+ )
192
+ diff_lines: List[DiffLine] = Field(
193
+ default_factory=list,
194
+ description="Individual diff lines for rendering",
195
+ )
196
+
197
+
198
+ # =============================================================================
199
+ # Shell Messages
200
+ # =============================================================================
201
+
202
+
203
+ class ShellStartMessage(BaseMessage):
204
+ """Notification that a shell command has started execution."""
205
+
206
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
207
+ command: str = Field(description="The shell command being executed")
208
+ cwd: Optional[str] = Field(
209
+ default=None, description="Working directory for the command"
210
+ )
211
+ timeout: int = Field(default=60, description="Timeout in seconds")
212
+
213
+
214
+ class ShellLineMessage(BaseMessage):
215
+ """A single line of shell command output with ANSI preservation."""
216
+
217
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
218
+ line: str = Field(description="The output line (may contain ANSI codes)")
219
+ stream: Literal["stdout", "stderr"] = Field(
220
+ default="stdout", description="Which output stream this line came from"
221
+ )
222
+
223
+
224
+ class ShellOutputMessage(BaseMessage):
225
+ """Output from a shell command execution with stdout, stderr, and timing."""
226
+
227
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
228
+ command: str = Field(description="The shell command that was executed")
229
+ stdout: str = Field(default="", description="Standard output from the command")
230
+ stderr: str = Field(default="", description="Standard error from the command")
231
+ exit_code: int = Field(description="Process exit code (0 = success)")
232
+ duration_seconds: float = Field(
233
+ ge=0,
234
+ description="How long the command took to execute",
235
+ )
236
+
237
+
238
+ # =============================================================================
239
+ # Agent Messages
240
+ # =============================================================================
241
+
242
+
243
+ class AgentReasoningMessage(BaseMessage):
244
+ """Agent's reasoning and planned next steps. Plain text only!"""
245
+
246
+ category: MessageCategory = MessageCategory.AGENT
247
+ reasoning: str = Field(description="The agent's current reasoning/thought process")
248
+ next_steps: Optional[str] = Field(
249
+ default=None,
250
+ description="Planned next actions (optional)",
251
+ )
252
+
253
+
254
+ class AgentResponseMessage(BaseMessage):
255
+ """A response from the agent. Use is_markdown flag for markdown content."""
256
+
257
+ category: MessageCategory = MessageCategory.AGENT
258
+ content: str = Field(description="The response content")
259
+ is_markdown: bool = Field(
260
+ default=False,
261
+ description="Whether content should be rendered as markdown",
262
+ )
263
+
264
+
265
+ class SubAgentInvocationMessage(BaseMessage):
266
+ """Message for sub-agent invocation header/status. Used by invoke_agent tool."""
267
+
268
+ category: MessageCategory = MessageCategory.AGENT
269
+ agent_name: str = Field(description="Name of the agent being invoked")
270
+ session_id: str = Field(description="Session ID for the invocation")
271
+ prompt: str = Field(description="The prompt being sent to the agent")
272
+ is_new_session: bool = Field(
273
+ description="Whether this is a new session or continuation"
274
+ )
275
+ message_count: int = Field(
276
+ default=0, description="Number of messages in history (for continuation)"
277
+ )
278
+
279
+
280
+ class SubAgentResponseMessage(BaseMessage):
281
+ """Response from a sub-agent invocation. Rendered as markdown."""
282
+
283
+ category: MessageCategory = MessageCategory.AGENT
284
+ agent_name: str = Field(description="Name of the agent that responded")
285
+ session_id: str = Field(description="Session ID for the invocation")
286
+ response: str = Field(description="The agent's response content")
287
+ message_count: int = Field(
288
+ default=0, description="Number of messages now in session history"
289
+ )
290
+
291
+
292
+ # =============================================================================
293
+ # User Interaction Messages (Agent → User)
294
+ # =============================================================================
295
+
296
+
297
+ class UserInputRequest(BaseMessage):
298
+ """Request for text input from the user."""
299
+
300
+ category: MessageCategory = MessageCategory.USER_INTERACTION
301
+ prompt_id: str = Field(description="Unique ID for matching responses to requests")
302
+ prompt_text: str = Field(description="The prompt to display to the user")
303
+ default_value: Optional[str] = Field(
304
+ default=None,
305
+ description="Default value to use if user provides no input",
306
+ )
307
+ input_type: Literal["text", "password"] = Field(
308
+ default="text",
309
+ description="Type of input field (password hides input)",
310
+ )
311
+
312
+
313
+ class ConfirmationRequest(BaseMessage):
314
+ """Request for user confirmation with options and optional feedback."""
315
+
316
+ category: MessageCategory = MessageCategory.USER_INTERACTION
317
+ prompt_id: str = Field(description="Unique ID for matching responses to requests")
318
+ title: str = Field(description="Title/headline for the confirmation")
319
+ description: str = Field(
320
+ description="Detailed description of what's being confirmed"
321
+ )
322
+ options: List[str] = Field(
323
+ default_factory=lambda: ["Yes", "No"],
324
+ description="Available options to choose from",
325
+ )
326
+ allow_feedback: bool = Field(
327
+ default=False,
328
+ description="Whether to allow free-form feedback in addition to selection",
329
+ )
330
+
331
+
332
+ class SelectionRequest(BaseMessage):
333
+ """Request for user to select from a list of options."""
334
+
335
+ category: MessageCategory = MessageCategory.USER_INTERACTION
336
+ prompt_id: str = Field(description="Unique ID for matching responses to requests")
337
+ prompt_text: str = Field(description="Prompt text to display")
338
+ options: List[str] = Field(description="List of options to choose from")
339
+ allow_cancel: bool = Field(
340
+ default=True,
341
+ description="Whether the user can cancel without selecting",
342
+ )
343
+
344
+
345
+ # =============================================================================
346
+ # Control Messages
347
+ # =============================================================================
348
+
349
+
350
+ class SpinnerControl(BaseMessage):
351
+ """Control message for spinner/progress indicator."""
352
+
353
+ category: MessageCategory = MessageCategory.SYSTEM
354
+ action: Literal["start", "stop", "update", "pause", "resume"] = Field(
355
+ description="What action to take on the spinner"
356
+ )
357
+ spinner_id: str = Field(description="Unique identifier for this spinner")
358
+ text: Optional[str] = Field(
359
+ default=None,
360
+ description="Text to display with the spinner (for start/update)",
361
+ )
362
+
363
+
364
+ class DividerMessage(BaseMessage):
365
+ """Visual divider/separator between sections."""
366
+
367
+ category: MessageCategory = MessageCategory.DIVIDER
368
+ style: Literal["light", "heavy", "double"] = Field(
369
+ default="light",
370
+ description="Visual style hint for the divider",
371
+ )
372
+
373
+
374
+ # =============================================================================
375
+ # Status Messages
376
+ # =============================================================================
377
+
378
+
379
+ class StatusPanelMessage(BaseMessage):
380
+ """A status panel with key-value fields for structured status info."""
381
+
382
+ category: MessageCategory = MessageCategory.SYSTEM
383
+ title: str = Field(description="Title for the status panel")
384
+ fields: Dict[str, str] = Field(
385
+ default_factory=dict,
386
+ description="Key-value pairs to display",
387
+ )
388
+
389
+
390
+ class VersionCheckMessage(BaseMessage):
391
+ """Result of a version check against PyPI or similar."""
392
+
393
+ category: MessageCategory = MessageCategory.SYSTEM
394
+ current_version: str = Field(description="Currently installed version")
395
+ latest_version: str = Field(description="Latest available version")
396
+ update_available: bool = Field(description="Whether an update is available")
397
+
398
+
399
+ # =============================================================================
400
+ # Union Type for Type Checking
401
+ # =============================================================================
402
+
403
+ # All concrete message types (excludes BaseMessage itself)
404
+ AnyMessage = Union[
405
+ TextMessage,
406
+ FileListingMessage,
407
+ FileContentMessage,
408
+ GrepResultMessage,
409
+ DiffMessage,
410
+ ShellStartMessage,
411
+ ShellLineMessage,
412
+ ShellOutputMessage,
413
+ AgentReasoningMessage,
414
+ AgentResponseMessage,
415
+ SubAgentInvocationMessage,
416
+ SubAgentResponseMessage,
417
+ UserInputRequest,
418
+ ConfirmationRequest,
419
+ SelectionRequest,
420
+ SpinnerControl,
421
+ DividerMessage,
422
+ StatusPanelMessage,
423
+ VersionCheckMessage,
424
+ ]
425
+ """Union of all message types for type checking."""
426
+
427
+
428
+ # =============================================================================
429
+ # Export all public symbols
430
+ # =============================================================================
431
+
432
+ __all__ = [
433
+ # Enums
434
+ "MessageLevel",
435
+ "MessageCategory",
436
+ # Base
437
+ "BaseMessage",
438
+ # Text
439
+ "TextMessage",
440
+ # File operations
441
+ "FileEntry",
442
+ "FileListingMessage",
443
+ "FileContentMessage",
444
+ "GrepMatch",
445
+ "GrepResultMessage",
446
+ # Diff/Modification
447
+ "DiffLine",
448
+ "DiffMessage",
449
+ # Shell
450
+ "ShellStartMessage",
451
+ "ShellLineMessage",
452
+ "ShellOutputMessage",
453
+ # Agent
454
+ "AgentReasoningMessage",
455
+ "AgentResponseMessage",
456
+ "SubAgentInvocationMessage",
457
+ "SubAgentResponseMessage",
458
+ # User interaction
459
+ "UserInputRequest",
460
+ "ConfirmationRequest",
461
+ "SelectionRequest",
462
+ # Control
463
+ "SpinnerControl",
464
+ "DividerMessage",
465
+ # Status
466
+ "StatusPanelMessage",
467
+ "VersionCheckMessage",
468
+ # Union type
469
+ "AnyMessage",
470
+ ]